diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6042bb5..e8b9629 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,11 +193,15 @@ jobs: - name: Install Linux build deps run: | sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libcurl4-openssl-dev + sudo apt-get install -y curl clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libcurl4-openssl-dev - uses: subosito/flutter-action@v2 with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu - name: Set Build Version shell: bash run: | @@ -234,6 +238,10 @@ jobs: with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc - name: Set Build Version shell: bash run: | @@ -270,6 +278,10 @@ jobs: with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-darwin,aarch64-apple-darwin - name: Set Build Version shell: bash run: | @@ -306,6 +318,10 @@ jobs: with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios - name: Set Build Version shell: bash run: | @@ -342,6 +358,12 @@ jobs: with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android + - name: Install cargo-ndk + run: cargo install cargo-ndk --locked - name: Set Build Version shell: bash run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69520ea..90ca562 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,15 @@ jobs: - name: Install Linux build deps run: | sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libcurl4-openssl-dev dpkg-dev + sudo apt-get install -y curl clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libcurl4-openssl-dev dpkg-dev - uses: subosito/flutter-action@v2 with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu - name: Set Build Version shell: bash run: | @@ -96,6 +100,10 @@ jobs: with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc - name: Set Build Version shell: bash run: | @@ -179,6 +187,12 @@ jobs: with: channel: stable cache: true + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android + - name: Install cargo-ndk + run: cargo install cargo-ndk --locked - name: Set Build Version shell: bash run: | diff --git a/.gitignore b/.gitignore index a202b2b..a1ad742 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ app.*.map.json /android/app/release sentry.properties +.output.txt +.sentry-native/ diff --git a/.junie/memory/errors.md b/.junie/memory/errors.md new file mode 100644 index 0000000..e69de29 diff --git a/.junie/memory/feedback.md b/.junie/memory/feedback.md new file mode 100644 index 0000000..e69de29 diff --git a/.junie/memory/language.json b/.junie/memory/language.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.junie/memory/language.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.junie/memory/memory.version b/.junie/memory/memory.version new file mode 100644 index 0000000..f398a20 --- /dev/null +++ b/.junie/memory/memory.version @@ -0,0 +1 @@ +3.0 \ No newline at end of file diff --git a/.junie/memory/tasks.md b/.junie/memory/tasks.md new file mode 100644 index 0000000..e69de29 diff --git a/.junie/plans/codebase-cleanup-plan.md b/.junie/plans/codebase-cleanup-plan.md new file mode 100644 index 0000000..9db81fe --- /dev/null +++ b/.junie/plans/codebase-cleanup-plan.md @@ -0,0 +1,226 @@ +--- +sessionId: session-260421-180911-1hmr +isActive: true +--- + +# Overview + +## Codebase Review & Cleanup Plan + +### Project State +Wimsy is a cross-platform XMPP client built in Flutter/Dart. The codebase is generally well-structured, with clear separation of concerns in most areas. Tests pass cleanly on the `quic` branch (`145` Flutter tests, `64` xmpp_stone tests, `flutter analyze` clean). + +The QUIC work is functionally complete but the two largest files — `lib/main.dart` (6514 lines) and `lib/xmpp/xmpp_service.dart` (7952 lines) — have grown into monoliths that make navigation, testing, and future changes increasingly difficult. + +### Key Problems Found + + Area | Problem | Risk | +---|---|---| + `main.dart` size | 6514 lines; UI + business logic + account management + notification handling all in one file | Hard to navigate; discourages adding tests | + `xmpp_service.dart` size | 7952 lines; everything from connection setup to MAM to file transfer to AV calls | High coupling; untestable internals | + `SharedPreferences` access | Called 14 times throughout `main.dart` in ad-hoc one-shot calls, often in widget callbacks | Scattered preference keys; easy to mistype; no central source of truth | + `ChatMessage` copy pattern | Every mutation (merge, correction, reaction) manually recopies all 28+ fields in a long `ChatMessage(...)` constructor call — repeated in `mam_merge_engine.dart`, `chat_message_mutations.dart`, and `xmpp_service.dart` | Error-prone; fields added to `ChatMessage` must also be added to every copy site | + Notification ID collisions | `id: DateTime.now().millisecondsSinceEpoch.remainder(1 << 31)` — same JID can get two notifications with different IDs in rapid succession, preventing grouped dismiss | Minor but real bug | + `Log.logLevel = LogLevel.VERBOSE` in production `main()` | Verbose XMPP XML logging left on by default for all users | Wasted I/O; leaks stanza content to device logs | + Missing `copyWith` on `ChatMessage` | The model has no `copyWith` method despite being copied in many places | Maintenance burden; risk of field omission bugs | + `_WimsyHomeState` build methods | `_buildLogin` and `_buildClient` are inline in the 6514-line file rather than extracted to separate screen widgets | Harder to test and read | + `doap.xml` XEP list | Needs verification that newly implemented XEPs (QUIC/XEP-0467, updated SASL2 pipelining) are reflected | Compliance documentation out of date | + + +# Technical Design + +## Technical Design + +### 1. Add `copyWith` to `ChatMessage` + +**File:** `lib/models/chat_message.dart` + +Add a `copyWith({...})` method with all optional parameters defaulting to the current value. This eliminates the current pattern of manually listing all 28+ fields in mutation sites. + +```dart +ChatMessage copyWith({ + String? from, + String? to, + String? body, + ... + Object? mamId = _sentinel, // use sentinel to distinguish null-clear from absent +}) { ... } +``` + +Callers in `mam_merge_engine.dart`, `chat_message_mutations.dart`, and `xmpp_service.dart` are updated to use `copyWith`. + +### 2. Centralise `SharedPreferences` keys and access + +**New file:** `lib/storage/preferences_service.dart` + +Create a `PreferencesService` class that holds all preference key constants and provides typed getters/setters. Existing callers (`main.dart`, `_WimsyHomeState`, `_PinSetupScreen`, etc.) are updated to use it. + +```dart +class PreferencesService { + static const _sentryOptInKey = 'sentry_opt_in'; + static const _pinIgnoredKey = 'wimsy_pin_ignored'; + static const _lastJidKey = 'wimsy_last_jid'; + // ... all other keys + + final SharedPreferences _prefs; + PreferencesService(this._prefs); + + bool get sentryOptIn => _prefs.getBool(_sentryOptInKey) ?? false; + Future setSentryOptIn(bool value) => _prefs.setBool(_sentryOptInKey, value); + // ... +} +``` + +This removes the 14 scattered `SharedPreferences.getInstance()` calls across `main.dart`. + +### 3. Fix notification ID stability + +**File:** `lib/main.dart`, `_WimsyHomeState._handleIncomingMessage` + +Derive the notification ID deterministically from the sender JID (e.g. `bareJid.hashCode.abs() % (1 << 31)`) so that rapid messages from the same contact update the same notification rather than creating new ones. + +### 4. Disable verbose XMPP logging in production builds + +**File:** `lib/main.dart`, `main()` + +Change: +```dart +Log.logLevel = LogLevel.VERBOSE; +Log.logXmpp = true; +``` +To only enable verbose logging in debug/profile mode: +```dart +assert(() { + Log.logLevel = LogLevel.VERBOSE; + Log.logXmpp = true; + return true; +}()); +``` + +### 5. Extract `LoginScreen` widget from `_WimsyHomeState._buildLogin` + +**New file:** `lib/login_screen.dart` + +Extract the ~1100-line `_buildLogin` method (login form, endpoint discovery, account fields, advanced options) into a standalone `LoginScreen` stateful widget with its own state class. Pass `XmppService` and `StorageService` down. + +### 6. Extract `XmppFileTransferHandler` from `XmppService` + +**New file:** `lib/xmpp/xmpp_file_transfer.dart` + +All methods and state related to IBB and Jingle file transfer (the `_FileTransferSession` class, `_fileTransferSessions`, send/receive/cancel methods) can be extracted to a dedicated handler class, reducing `xmpp_service.dart` by ~600 lines. + +### 7. Update `doap.xml` + +Verify and update XEP entries: +- XEP-0467 (QUIC transport) — new on this branch +- XEP-0388 SASL2 with IAP pipelining — extended +- Any other XEPs touched by recent commits + +### Architecture Diagram + +```mermaid +graph TD + main["lib/main.dart"] --> WimsyApp + WimsyApp --> Gatekeeper + Gatekeeper --> LoginScreen["LoginScreen (new file)"] + Gatekeeper --> WimsyHome + WimsyHome --> ChatScreen["ChatScreen (existing)"] + ChatScreen --> XmppService + XmppService --> FileTransfer["XmppFileTransferHandler (new file)"] + XmppService --> MAM["MamCoordinator"] + XmppService --> PEP["PepManager"] + WimsyApp --> PreferencesService["PreferencesService (new file)"] + PreferencesService --> SharedPrefs["SharedPreferences"] +``` + + +# Prioritised Changes + +## Prioritised Changes + +Changes are ordered by effort and risk: + +### High priority (safety & correctness) + +1. **Disable verbose logging in release builds** (`main()`) — trivial one-liner; prevents leaking XMPP stanza contents to device logs in production. +2. **Fix notification ID stability** — use a deterministic hash of bareJid instead of `DateTime.now()` to prevent notification stacking for the same sender. +3. **Add `copyWith` to `ChatMessage`** and replace manual copy blocks — reduces the risk of field omission bugs as `ChatMessage` grows. + +### Medium priority (maintainability) + +4. **Centralise `SharedPreferences` into `PreferencesService`** — removes 14 scattered `getInstance()` calls and consolidates all key strings. +5. **Extract `LoginScreen`** from `_WimsyHomeState` — significant reduction in `main.dart` size; no behavioural change. +6. **Extract `XmppFileTransferHandler`** from `XmppService` — reduces the 7952-line service; encapsulates IBB/Jingle transfer session state. + +### Lower priority (polish) + +7. **Update `doap.xml`** with new XEP-0467 and any updated SASL2 entries. +8. **Enable additional lint rules** in `analysis_options.yaml`. + +### Out of scope for this cleanup +- Splitting `xmpp_service.dart` further beyond the file-transfer extraction (MAM handler, call session handler) — useful long-term but large scope. +- Extracting `ChatScreen` — even larger refactor, separate effort. +- State management migration (the `ChangeNotifier`/`AnimatedBuilder` pattern is working fine). +- New features or protocol work (see `doc/todo.md` and `doc/roadmap.md`). + + +# Testing + +## Testing + +### Validation Approach +- All existing tests (`flutter test`, `dart test` in `vendor/xmpp_stone`) must continue to pass after each stage. +- `flutter analyze` must remain clean. +- The `copyWith` implementation and mutation-site updates should be covered by the existing tests in `test/chat_message_mutations_test.dart`, `test/mam_merge_engine_test.dart`, etc. + +### New Tests to Add +- `test/preferences_service_test.dart` — unit tests for `PreferencesService` getters/setters using a mock `SharedPreferences`. +- A test for the deterministic notification ID function (pure function, easy to unit test). + +### Regression Checks +- After extracting `LoginScreen`: `test/widget_test.dart` smoke test should still pass (it exercises the login path). +- After `copyWith`: run `test/chat_message_test.dart` and `test/chat_message_mutations_test.dart` to confirm no regressions. + + +# Delivery Steps + +### Step 1: Fix verbose logging and notification ID stability in lib/main.dart +Two quick correctness issues in `lib/main.dart` are resolved and committed. + +- In `main()`: guard `Log.logLevel = LogLevel.VERBOSE` and `Log.logXmpp = true` behind `assert(...)` so verbose XMPP logging is only active in debug/profile builds, never in release. +- In `_handleIncomingMessage` and `_handleIncomingRoomMessage`: replace `DateTime.now().millisecondsSinceEpoch.remainder(1 << 31)` with a deterministic ID derived from `bareJid.hashCode.abs() % (1 << 31)` to prevent notification stacking for the same sender. +- Run `flutter analyze` and `flutter test` to confirm no regressions; commit with detailed message. + +### Step 2: Add `copyWith` to `ChatMessage` and replace all manual copy sites +`ChatMessage` gains a `copyWith` method and every manual full-constructor copy block in the codebase is replaced with it. + +- Add `ChatMessage copyWith({...})` to `lib/models/chat_message.dart` covering all 28+ fields; use a sentinel-object pattern for nullable fields so callers can explicitly clear them. +- Replace manual copy blocks in `lib/xmpp/mam_merge_engine.dart` (two sites). +- Replace manual copy blocks in `lib/xmpp/chat_message_mutations.dart` (two sites). +- Replace any manual copy blocks found in `lib/xmpp/xmpp_service.dart`. +- Run `flutter analyze`, `flutter test` (especially `chat_message_mutations_test.dart`, `mam_merge_engine_test.dart`); commit. + +### Step 3: Introduce `PreferencesService` and centralise all SharedPreferences access +`PreferencesService` in `lib/storage/preferences_service.dart` consolidates the 14 scattered `SharedPreferences.getInstance()` call sites currently spread across `lib/main.dart`. + +- Create `lib/storage/preferences_service.dart` with all key constants and typed getters/setters (`sentryOptIn`, `pinIgnored`, `lastJid`, `audioInputId`, `videoInputId`, etc.). +- Initialise `PreferencesService` once at app startup in `main()` and pass it down to the widgets that need it. +- Replace all 14 inline call sites in `lib/main.dart` with `PreferencesService` API calls. +- Add `test/preferences_service_test.dart` with basic get/set unit tests using a fake/mock `SharedPreferences`. +- Run `flutter analyze` and `flutter test`; commit. + +### Step 4: Extract `LoginScreen` from `_WimsyHomeState` into `lib/login_screen.dart` +The login and account-setup portion of `_WimsyHomeState` (~1100 lines) is moved to a standalone widget, substantially reducing `lib/main.dart`. + +- Create `lib/login_screen.dart` with a `LoginScreen` stateful widget containing the account form, endpoint discovery logic, advanced options panel, and connect button. +- Move relevant `TextEditingController` fields, `_handleConnect`, `_scheduleEndpointDiscovery`, `_discoverEndpoint`, and `_buildLogin` body into `LoginScreen`'s state class. +- Update `_WimsyHomeState.build` / `_Gatekeeper` to instantiate `LoginScreen` instead of calling `_buildLogin`. +- Verify `test/widget_test.dart` smoke test still passes; run `flutter analyze`; commit. + +### Step 5: Extract `XmppFileTransferHandler` from `XmppService` and update `doap.xml` +IBB/Jingle file-transfer session management (~600 lines) is extracted from `lib/xmpp/xmpp_service.dart` into its own class, and `doap.xml` is updated for newly implemented XEPs. + +- Create `lib/xmpp/xmpp_file_transfer.dart` containing `XmppFileTransferHandler`, the `_FileTransferSession` model class, and all send, receive, accept, decline, and cancel methods for IBB and Jingle file transfer. +- Wire `XmppFileTransferHandler` into `XmppService` as a collaborating object, keeping all existing public method signatures on `XmppService` intact (thin delegation wrappers where needed). +- Update `doap.xml`: add XEP-0467 (QUIC transport for XMPP); review and update the XEP-0388 SASL2 / IAP pipelining entry. +- Run `flutter analyze` and `flutter test`; commit. \ No newline at end of file diff --git a/apply-shape.sh b/apply-shape.sh new file mode 100644 index 0000000..cebb62c --- /dev/null +++ b/apply-shape.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: apply-shape.sh +# e.g. apply-shape.sh 10mbit 50ms 1% +# +# Applies bandwidth/latency/loss shaping ONLY to the XMPP/QUIC flow +# (UDP/$PORT to/from $IPV4 / $IPV6) plus ICMP/ICMPv6 echo to/from +# the same host (so `ping` can be used to trivially test the shaping). +# All other traffic on $IFACE is left untouched. + +BW="${1:?Bandwidth required (e.g. 10mbit)}" +LAT="${2:?Latency required (e.g. 50ms)}" +LOSS="${3:?Loss required (e.g. 1%)}" + +source shape-cfg.sh + +echo "[*] Loading IFB module" +modprobe ifb || true +ip link add ifb0 type ifb 2>/dev/null || true +ip link set ifb0 up + +echo "[*] Clearing old qdiscs" +tc qdisc del dev "$IFACE" root 2>/dev/null || true +tc qdisc del dev "$IFACE" ingress 2>/dev/null || true +tc qdisc del dev ifb0 root 2>/dev/null || true + +############################################### +# OUTBOUND SHAPING (egress on $IFACE) +############################################### +# +# Default class 1:1 is an UNSHAPED bypass at line rate, so unmatched +# traffic is not affected. Only filters explicitly steer traffic into +# the shaped class 1:10. + +echo "[*] Setting outbound shaping on $IFACE" + +tc qdisc add dev "$IFACE" root handle 1: htb default 1 + +# 1:1 = bypass (no rate limit, no netem) +tc class add dev "$IFACE" parent 1: classid 1:1 htb rate 10gbit ceil 10gbit + +# 1:10 = shaped class for XMPP/QUIC + ICMP echo to host +tc class add dev "$IFACE" parent 1: classid 1:10 htb rate "$BW" ceil "$BW" +tc qdisc add dev "$IFACE" parent 1:10 handle 10: netem delay "$LAT" loss "$LOSS" + +# --- IPv6 --- + +# IPv6 UDP/$PORT → $IPV6 (XMPP/QUIC) +tc filter add dev "$IFACE" protocol ipv6 parent 1: prio 1 u32 \ + match ip6 protocol 17 0xff \ + match ip6 dport "$PORT" 0xffff \ + match ip6 dst "$IPV6" \ + flowid 1:10 + +# IPv6 ICMPv6 → $IPV6 (ping, for trivial testing) +# next-header 58 = ICMPv6 +tc filter add dev "$IFACE" protocol ipv6 parent 1: prio 3 u32 \ + match ip6 protocol 58 0xff \ + match ip6 dst "$IPV6" \ + flowid 1:10 + +# --- IPv4 --- + +# IPv4 UDP/$PORT → $IPV4 (XMPP/QUIC) +tc filter add dev "$IFACE" protocol ip parent 1: prio 2 u32 \ + match ip protocol 17 0xff \ + match ip dport "$PORT" 0xffff \ + match ip dst "$IPV4" \ + flowid 1:10 + +# IPv4 ICMP → $IPV4 (ping, for trivial testing) +tc filter add dev "$IFACE" protocol ip parent 1: prio 4 u32 \ + match ip protocol 1 0xff \ + match ip dst "$IPV4" \ + flowid 1:10 + +############################################### +# INBOUND SHAPING (ingress → IFB) +############################################### +# +# Only the matched flows (XMPP/QUIC + ICMP echo from the host) are +# redirected to ifb0. Everything else stays on the normal ingress path +# and is not touched by the shaper. + +echo "[*] Redirecting matched ingress to IFB" + +tc qdisc add dev "$IFACE" handle ffff: ingress + +# IPv6 UDP/$PORT ← $IPV6 (XMPP/QUIC) +tc filter add dev "$IFACE" parent ffff: protocol ipv6 prio 1 u32 \ + match ip6 protocol 17 0xff \ + match ip6 sport "$PORT" 0xffff \ + match ip6 src "$IPV6" \ + action mirred egress redirect dev ifb0 + +# IPv6 ICMPv6 ← $IPV6 (ping replies / inbound echo) +tc filter add dev "$IFACE" parent ffff: protocol ipv6 prio 3 u32 \ + match ip6 protocol 58 0xff \ + match ip6 src "$IPV6" \ + action mirred egress redirect dev ifb0 + +# IPv4 UDP/$PORT ← $IPV4 (XMPP/QUIC) +tc filter add dev "$IFACE" parent ffff: protocol ip prio 2 u32 \ + match ip protocol 17 0xff \ + match ip sport "$PORT" 0xffff \ + match ip src "$IPV4" \ + action mirred egress redirect dev ifb0 + +# IPv4 ICMP ← $IPV4 (ping replies / inbound echo) +tc filter add dev "$IFACE" parent ffff: protocol ip prio 4 u32 \ + match ip protocol 1 0xff \ + match ip src "$IPV4" \ + action mirred egress redirect dev ifb0 + +echo "[*] Setting inbound shaping on ifb0" + +# Everything that arrives on ifb0 is, by construction, the matched +# flow, so a single shaped default class is sufficient. +tc qdisc add dev ifb0 root handle 2: htb default 20 +tc class add dev ifb0 parent 2: classid 2:20 htb rate "$BW" ceil "$BW" +tc qdisc add dev ifb0 parent 2:20 handle 20: netem delay "$LAT" loss "$LOSS" + +echo "[*] Done. Active shaping:" +tc -s qdisc show dev "$IFACE" +tc -s qdisc show dev ifb0 diff --git a/clear-shape.sh b/clear-shape.sh new file mode 100644 index 0000000..109965c --- /dev/null +++ b/clear-shape.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +source shape-cfg.sh + +echo "[*] Using $IFACE" + +tc qdisc del dev "$IFACE" root 2>/dev/null || true +tc qdisc del dev "$IFACE" ingress 2>/dev/null || true +tc qdisc del dev ifb0 root 2>/dev/null || true + +echo "Shaping removed." diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/doap.xml b/doap.xml index 3f7319e..5eca332 100644 --- a/doap.xml +++ b/doap.xml @@ -235,7 +235,8 @@ - partial + complete + Includes SCRAM authentication, user-agent binding, stream resumption inline, and IAP config-version pipelining. @@ -280,6 +281,12 @@ complete + + + + partial + + diff --git a/doc/plan-quic.md b/doc/plan-quic.md new file mode 100644 index 0000000..8c65f19 --- /dev/null +++ b/doc/plan-quic.md @@ -0,0 +1,145 @@ +# QUIC Transport Plan (Single Reliable Channel) + +## Objective +Add QUIC transport for XMPP on IO platforms using `flutter_quic`, with one reliable bidirectional channel for the XML stream. + +## Scope +In scope for this phase: +- QUIC endpoint discovery via SRV: `_xmpp-client._quic.`. +- QUIC transport implementation for mobile/desktop (non-web). +- One reliable channel carrying the full XMPP XML stream. +- Automatic transport selection based on SRV records. +- Connection fallback to existing TCP transport path when QUIC fails. +- Tests for discovery, endpoint planning, and connection fallback behavior. + +Out of scope for this phase: +- Web support. +- Multi-channel QUIC usage. +- Partial reliability / datagram use. +- XEP-level changes beyond transport. +- UI transport toggles. + +## Confirmed Decisions +- ALPN: `xmpp-client` (per XEP-0467 baseline). +- Selection mode: automatic from SRV, no UI toggle/opt-in gate. +- Preference: QUIC SRV should be preferred over TCP SRV. +- Fallback: if QUIC fails, fall back to TCP and continue through TCP SRV failover list. + +## Current Baseline +- `XmppService.connect(...)` resolves SRV candidates and builds TCP endpoint plans. +- `Connection` currently opens WebSocket or TCP via `XmppWebSocket` abstraction. +- Transport handling is centralized enough to add a new endpoint class + attempt loop. + +## Design Principles +- Keep QUIC additive and low-risk: no behavior changes for non-QUIC-capable servers. +- Preserve robust fallback: QUIC failure should not block TCP SRV failover. +- Reuse existing parser, buffering, reconnection, and feature negotiation logic. + +## Proposed Architecture + +### 1. Endpoint and account model +- Add `XmppQuicEndpoint` (host, port, tlsHost). +- Add account fields: + - `List? quicEndpoints` +- Keep existing `tcpEndpoints` and websocket config untouched. + +### 2. SRV discovery and preference +- Extend SRV lookup to query `_xmpp-client._quic.`. +- Keep existing `_xmpps-client._tcp` and `_xmpp-client._tcp` lookups. +- Build separate ordered candidate lists per transport type. +- Connection attempt order: + 1. QUIC candidates (ordered by priority/weight) + 2. TCP candidates (existing SRV-based ordering/failover) + +### 3. Connection planning in `XmppService` +- Build and assign both `quicEndpoints` and `tcpEndpoints` from SRV results. +- For hosts with no SRV data, keep current fallback behavior. +- Keep websocket path unchanged when explicitly selected (or on web). + +### 4. QUIC transport adapter +- Add QUIC-backed transport class using `flutter_quic`: + - connect with ALPN `xmpp-client` + - open one reliable bidirectional stream + - map incoming bytes to existing response handling + - write outbound XML payloads + - close cleanly +- Preserve existing buffering behavior above transport. + +### 5. Integrate into `Connection.openSocket()` +- Attempt QUIC endpoints first (if present). +- On QUIC endpoint failure, try next QUIC endpoint. +- If all QUIC endpoints fail, continue with existing TCP endpoint loop. +- Keep per-endpoint logging and aggregated failure context. + +### 6. Reconnection behavior +- Reconnection reuses same preference order (QUIC first, TCP fallback). +- No separate UI/runtime mode switching needed for this phase. + +## Phased Implementation + +### Phase 1: Data model updates +Files: +- `vendor/xmpp_stone/lib/src/account/XmppAccountSettings.dart` +- `lib/storage/account_record.dart` (if account transport data is persisted) + +Deliverable: +- Account can carry QUIC endpoint candidates. + +### Phase 2: SRV lookup extension +Files: +- `lib/xmpp/srv_lookup_native.dart` +- `lib/xmpp/srv_lookup_stub.dart` +- `lib/xmpp/srv_target.dart` (or new QUIC target type) + +Deliverable: +- QUIC SRV candidates resolved and ordered. + +### Phase 3: Endpoint planning in service +Files: +- `lib/xmpp/xmpp_service.dart` +- new helper (e.g. `lib/xmpp/quic_endpoint_plan.dart`) + +Deliverable: +- Service prepares QUIC-first then TCP fallback endpoint plans. + +### Phase 4: QUIC transport implementation +Files: +- new transport adapter under `vendor/xmpp_stone/lib/src/connection/`. +- `vendor/xmpp_stone/lib/src/Connection.dart` attempt loop updates. + +Deliverable: +- XMPP stream can run over single-channel QUIC. + +### Phase 5: Tests +Add tests in: +- `test/` for SRV parsing/planning (including QUIC priority). +- `vendor/xmpp_stone/test/` for connection fallback sequencing. + +Minimum test cases: +- QUIC SRV discovered and preferred over TCP SRV. +- QUIC connection failure falls back to TCP endpoint 1 then endpoint 2. +- QUIC unavailable still uses current TCP/websocket behavior. +- Buffered writes still coalesce correctly over QUIC transport adapter. + +### Phase 6: Validation +Required checks before commit: +- `flutter analyze` +- `flutter test` +- `cd vendor/xmpp_stone && dart test` + +## Open Questions (With Suggested Defaults) +1. Should QUIC and TCP SRV priorities be merged into a single cross-transport priority table, or keep transport buckets with hard QUIC-first preference? +Suggested default: Keep transport buckets and hard QUIC-first preference (as requested), then priority/weight inside each bucket. + +2. QUIC handshake timeout before fallback? +Suggested default: 3 seconds per QUIC endpoint attempt before moving on. + +3. Certificate validation behavior for QUIC? +Suggested default: Match existing TLS hostname/cert checks used for TCP/TLS. + +## Definition of Done +- Client can establish XMPP session over QUIC (single reliable channel) on IO platforms. +- SRV discovery supports `_xmpp-client._quic.`. +- QUIC SRV is preferred; QUIC failures fall back to TCP SRV failover. +- Existing non-QUIC behavior remains unchanged. +- Test and analysis suite passes. diff --git a/doc/startup-fetch-review.md b/doc/startup-fetch-review.md new file mode 100644 index 0000000..0b82e61 --- /dev/null +++ b/doc/startup-fetch-review.md @@ -0,0 +1,734 @@ +# Startup Fetch Review: Displayed sync, MAM/archiving, vCard/Avatar caches + +Date: 2026-05-01 + +This document reviews how Wimsy interacts on startup with: + +* XEP-0490 Message Displayed Synchronization (MDS) +* XEP-0313 MAM (catch-up and backfill) +* XEP-0084 PEP user-avatar metadata + data +* XEP-0153 vCard-temp avatar updates +* The local message / avatar cache (Hive via `StorageService`) + +It identifies places where the client refetches data unnecessarily on every +connect and proposes concrete fixes (or, where a fix is not currently +possible, explains why). + +The review is based on a `flutter run` startup trace (`wimsy.log`) plus +inspection of: + +* `lib/xmpp/xmpp_service.dart` +* `lib/xmpp/mam_coordinator.dart`, `mam_query_planner.dart`, + `mam_cursor_store.dart`, `mam_merge_engine.dart` +* `lib/pep/pep_manager.dart`, `pep_caps_manager.dart` +* `lib/storage/storage_service.dart` + +--- + +## TL;DR — biggest startup wins + +| # | Symptom | Where | Effort | Saved traffic | +|---|---------|-------|--------|---------------| +| 1 | vCard `` fetched for every roster entry on every connect, even when we already have the photo bytes and hash | `_ensureContact` → `_requestVcardAvatar`, `lib/xmpp/xmpp_service.dart:6741` | Low | N × (roster size) IQ + payload (a vCard photo can be tens of KB) | +| 2 | vCard fetched again per presence even when the advertised `` hash matches the cached one | `_handleVcardPresenceUpdate`, `lib/xmpp/xmpp_service.dart:7694` | Low | One IQ per online roster contact each time presence is re-broadcast | +| 3 | MDS published items requested via PubSub IQ on every Ready, despite caching `displayed_sync` in Hive and despite the server pushing +notify | `_setupDisplayedSync`, `lib/xmpp/xmpp_service.dart:5186` | Low | One IQ + payload per startup | +| 4 | Per-chat MAM catch-up step is fired for every chat with cached messages, regardless of whether MDS displayed-id already matched a known local message (i.e. we are demonstrably up to date) | `_primeMamSync`, `lib/xmpp/xmpp_service.dart:7541` | Medium | One MAM `` per cached chat, each producing a page of forwarded messages | +| 5 | "Displayed sync miss" → we drop the displayed marker silently and never narrow the catch-up; next time MDS arrives we still cannot match | `_applyDisplayedStateForChat`, `lib/xmpp/xmpp_service.dart:5460` | Medium | Repeated MAM pages on every startup for the same chat | +| 6 | Two duplicate self vCard fetches at session start (one in initial flush, one explicit) and `enable carbons:2` is sent twice | observed in log around `12:20:21–12:20:22` | Low | A handful of redundant IQs | + +Items 1–3 are pure waste. Items 4–5 are the structural ones that explain why +"Displayed sync miss" log lines appear after every restart for chats with +many unread messages, and why we end up pulling a new MAM page on every +connect. + +--- + +## 1. Displayed Sync (XEP-0490) — current behaviour + +### What we do today + +* On `XmppConnectionState.Ready` we call `_setupDisplayedSync()` + (`xmpp_service.dart:5186`). This sends an IQ: + + ```xml + + + + + + ``` + + i.e. it fetches **all** items from the displayed node every connect. + +* The result populates `_displayedStanzaIdByChat` and is persisted to + `displayed_sync` in `StorageService` via `storeDisplayedSync` — + `_displayedStanzaIdByChat` is also seeded from storage on app start. + +* For each item we call `_applyDisplayedStateForChat`, which walks the + in-memory message list for that chat looking for the `stanza-id`. If + found, we mark MAM catch-up complete for that chat. If not found, we log + "Displayed sync miss" (see `wimsy.log`) and **return false** without + taking further action. + +* PubSub event messages (``) for MDS are also + handled by `_handleDisplayedSyncEvent`, so live updates from carbons / + +notify already work. + +### Problems + +1. **Always-on full fetch.** We already cache the displayed map on disk, + yet every connect we still query the entire `urn:xmpp:mds:displayed:0` + node. There are two cheaper alternatives: + + * **Subscribe to MDS via +notify** (the spec defines this). The server + will push `` items at our session and we don't need to poll. + We currently do not advertise `urn:xmpp:mds:displayed:0+notify` in + our `disco#info` features, and we do not subscribe via Carbons. We do + however receive PEP events (we already handle them), so the simplest + fix is just to add the `+notify` feature to our caps form, like we + do for `urn:xmpp:avatar:metadata+notify`. + * **Compare with the cached map** — if the published item count is + small (one item per chat) we still get a full PubSub items result on + IQ; we can skip the IQ when our cached map is non-empty and rely on + +notify for catch-up. + +2. **Misses are terminal.** When `_applyDisplayedStateForChat` cannot find + the displayed `stanza-id` in the local message list, it logs a miss + and gives up. As a consequence: + + * The MAM catch-up tracker for that chat is never marked complete from + MDS, so the catch-up loop will keep running on every startup until + the user actually opens the chat and we backfill. + * The displayed marker is forgotten silently, so the next time MDS + pushes the same `id` we will short-circuit at + `_displayedStanzaIdByChat[id] == stanzaId` and never retry the match. + * This is exactly what we see in the log: + `Displayed sync miss for chat=xsf@muc.xmpp.org … messages=25 + knownStanzaIds=[…5 ids…]` — we have only 25 cached messages and the + server's displayed marker points further back. + +### Recommendations + +* **R1.1 (easy).** Skip the explicit `_setupDisplayedSync` IQ when + `_displayedStanzaIdByChat` was successfully restored from disk; fall + back to the IQ only when the cache is empty. +* **R1.2 (easy).** Advertise `urn:xmpp:mds:displayed:0+notify` in our + caps disco form and rely on PubSub auto-subscription. This eliminates + the IQ entirely on subsequent restarts. +* **R1.3 (medium).** On a "Displayed sync miss": + * Persist the unresolved `(chatJid, stanzaId)` pair to disk + (e.g. `displayed_sync_pending`). + * On the next MAM page that contains this `stanza-id`, resolve and + remove the pending entry, then set the displayed timestamp and clear + the catch-up flag. + * Drive a single bounded MAM query toward this `stanza-id` (use it as + `before` and stop) instead of firing the generic catch-up step. + +This last point is the key structural fix: it converts an unbounded +"catch-up since latest known mam-id" into a precise "fetch up to the +displayed marker", which is exactly what MDS is for. + +--- + +## 2. MAM catch-up on connect + +### What we do today + +In `_primeMamSync()` (`xmpp_service.dart:7541`), once the connection is +Ready we iterate over every cached DM chat and every bookmark and: + +* If the chat has cached messages → call `_startMamCatchUp(jid, …)`, + which posts a MAM `` using `MamQueryPlanner.catchUp` + (`afterId=`). This is correct behaviour — we ask for + messages newer than what we already have. +* If the chat has no cached messages → call + `_requestRoomMam(roomJid, max:25, before:'')` for rooms or + `_requestMamInitial(jid)` for DMs. + +`MamCursorStore` throttles repeats inside a session (5s for catch-up, +30s for backfill) and keeps `prependOffset`/timer state. Cursors per chat +are correctly persisted via `_seededMessageJids` and `latestMamIdFor`, +so we are not re-pulling history we already have. + +### Where this is wasteful + +* The catch-up step fires **for every cached chat**, including chats the + user has not opened in months and is unlikely to open this session. + For a typical roster with dozens of contacts/MUCs this is dozens of + parallel `` requests at startup. +* Even when MDS already tells us "the last message you displayed in + chat X is stanza Y" and we have Y in our local cache, we still go and + ask MAM for newer messages. That part is correct in principle (we + could legitimately have new messages), but it means we are essentially + doing a fan-out poll on every connect. +* For MUCs we additionally do a fan-out of `` and chat-state / + caps disco, and then immediately ask the MUC's MAM. We already see in + the log that `summit@muc.xmpp.org` is dragging in a long backlog + (`queryid="VPNAXKAHR"`) on the aux QUIC stream, blocking other work. + +### Recommendations + +* **R2.1 (medium).** Defer per-chat MAM catch-up until the user actually + opens the chat (or makes the chat list visible), keeping only: + * A single global "last seen" anchor per account, similar to how + Conversations does it: query MAM with `start=` (or a + server-side bounded period like 24h), get one merged stream, then + update per-chat anchors as messages come in. + * Plus a per-chat cap of e.g. 25 messages on the first time the chat + is opened. + + We have most of the machinery already (`MamCursorStore`, + `mergeMamIdsIntoExisting`, `latestMamIdFor`); the missing piece is a + single global `last_mam_id_seen` and a corresponding initial query at + connect time. + +* **R2.2 (low).** When MDS displayed-id matches a local message *and* the + message's MAM id equals `latestMamIdFor(chat)`, skip the catch-up + query for that chat entirely — we already know we are caught up to + the displayed marker. (Today we set the displayed timestamp and clear + the pending flag, but `_primeMamSync` still iterates and fires + catch-up.) + +* **R2.3 (low).** Make `_primeMamSync` skip chats that the user has not + opened in the current session unless the user has explicitly enabled + "fetch all on startup" in preferences. + +* **R2.4 (low).** Sort/throttle the connect-time fan-out: bookmark joins + trigger MUC presence + MAM + caps disco; we should batch caps disco + hits via a single bounded queue (we already do this for vCards via + `_vcardRequests`). A tiny token-bucket limiter would smooth this out. + +### Why we cannot do "no MAM at all" today + +Without persisting a `last_mam_id_seen` per account in `StorageService` +we do not have a single global anchor. Today the per-chat anchors are +necessary because messages can arrive in chats we never explicitly +loaded (e.g. via carbons while offline). We can build R2.1, but it +requires adding one new key to `StorageService` and one new query path. + +--- + +## 3. Avatar caching — PEP user-avatar (XEP-0084) + +### What we do today + +`PepManager` (`lib/pep/pep_manager.dart`) is the well-behaved bit: + +* On boot we seed `_metadataByJid` and `_avatarBlobs` from + `StorageService.loadAvatarMetadata()` / `loadAvatarBlobs()`. +* `requestMetadataIfMissing(bareJid)` is a no-op when we already have + metadata for that JID. +* `requestAvatarData(bareJid, hash)` is a no-op when we already have + the blob for that hash. +* PubSub event messages with `urn:xmpp:avatar:metadata` update the cache + and request the data only when the hash is new. + +This is correct. The only minor improvements possible: + +* **R3.1 (very low).** We never expire stale blobs that are no longer + referenced by any metadata entry. For long-lived installs this is a + small but real disk leak. A periodic GC pass over `_avatarBlobs` + versus `_metadataByJid` would fix it. +* **R3.2 (low).** We do not subscribe (``) to anyone other + than ourselves. PEP +notify is enough for contacts who advertise our + caps via PEP, but for non-presence-subscribed contacts (open MUCs + etc.) we will pay a one-shot metadata IQ when we first see them. This + is unavoidable without a presence subscription. + +### Why some PEP avatar IQs *do* fail in the log + +The log shows several `` PEP avatar +metadata responses (e.g. for `test1@dave.cridland.net`, `test2@…`). That +is expected: those JIDs publish a vCard avatar but no PEP one. We +*should* cache the 404 too, otherwise we will retry every connect. + +* **R3.3 (low).** Treat `item-not-found` on + `urn:xmpp:avatar:metadata` as "no PEP avatar" and store a sentinel in + `_metadataByJid` (e.g. an `AvatarMetadata` with empty `hash`) so + `requestMetadataIfMissing` short-circuits next startup. + +--- + +## 4. Avatar caching — vCard-temp (XEP-0153) + +This is the worst offender on startup. + +### What we do today + +`xmpp_service.dart:7604–7653` defines `_requestVcardAvatar` / +`_requestVcardDetails`. Triggers: + +* `_ensureContact` calls `_requestVcardAvatar(entry.jid)` for every + **new** roster entry (line 6741). However, on first connect after app + start, *every* roster entry is a "new" entry as far as in-memory + `_contacts` is concerned (we seed contacts from disk before + `_setupRoster` runs, so this is only fine if we persist contacts; we + do — but we still re-issue the vCard fetch elsewhere). +* `_handleVcardPresenceUpdate` calls `_requestVcardAvatar` whenever a + presence stanza carries ``. The + guard at line 7724 is good (skip when hash matches and bytes + cached) — but the line 7727 `_vcardAvatarState[bareJid] = hash;` is + applied **before** we know whether bytes are cached, and the next call + to `_requestVcardAvatar` is unconditional. +* `_setupRoster`-equivalent paths in `xmpp_service.dart:1286, 1453, + 2339, 6741` all call `_requestVcardAvatar` / `_requestVcardDetails` + without consulting `_vcardAvatarBytes` or `_vcardAvatarState`. +* The self vCard is fetched again at line 1036 + (`_requestVcardDetails(_currentUserBareJid!, preferName: true)`) on + every Ready — duplicating any roster-driven self fetch. + +The persistence story is fine: `storeVcardAvatar`, +`storeVcardAvatarState` keep both bytes and the hash in Hive, and we +seed both maps at boot via `_seedVcardAvatars` / `_seedVcardAvatarState`. + +So we already *have* the data. We just don't *use* it as a guard. + +### Recommendations + +* **R4.1 (high impact, low effort).** Add a guard at the top of + `_requestVcardDetails`: + + ```dart + if (!preferName && + _vcardAvatarBytes.containsKey(bareJid) && + (_vcardAvatarState[bareJid] ?? '').isNotEmpty && + _vcardAvatarState[bareJid] != _vcardNoAvatar) { + return; // Already have bytes + hash; vCard advertises only avatar here. + } + ``` + + i.e. only fetch the vCard when we do not have cached bytes for the + current advertised hash, or when we genuinely need the name + (`preferName == true`). + +* **R4.2 (very low effort).** In `_handleVcardPresenceUpdate`: + + ```dart + if (existing == hash && _vcardAvatarBytes.containsKey(bareJid)) { + return; // already there + } + ``` + + is already present and correct. The bug is that *other* call-sites + (line 6741, etc.) bypass this gate. Centralising the cache check in + `_requestVcardDetails` (R4.1) covers them all. + +* **R4.3 (low effort).** Cache "this JID has no vCard" + (`InvalidVCard`) across restarts. Today `_vcardUnavailable` is a + process-only `Set`, so we will retry next startup. Persist + it (e.g. `storeVcardAvatarState(jid, _vcardNoAvatar)` already does + the equivalent for missing photos; extend it to missing vCards + altogether by adding a separate sentinel or storing a tag in + `vcardAvatarState`). + +* **R4.4 (low).** Coalesce duplicate self vCard fetches: `_setupRoster` + may re-add self to the contacts map, and we explicitly fetch it again + at line 1036. A single `_vcardRequests` guard already prevents two + outstanding requests, but we are still issuing one we don't need. Add + the same R4.1 guard. + +--- + +## 5. Other startup multipliers seen in the log + +These are not strictly cache misses but they magnify the impact of the +above: + +* **Carbons enabled twice.** `enable xmlns="urn:xmpp:carbons:2"` is sent + in two different code paths during initial flush (`KNBYYPMLT` and + `VSBVMXXRT` in the log). Harmless but wasteful. Track a + `_carbonsRequested` flag. +* **Caps disco fan-out.** Every distinct `node#ver` from MUC presence + triggers a disco#info IQ. We do cache caps locally (`pep_caps_manager` + reads features), but we do not currently use a shared, persistent + caps cache à la Entity Capabilities (XEP-0115) verifier. Persisting + caps results across restarts (keyed by `node#ver`) would eliminate the + flurry of `disco#info` IQs visible in the log. +* **Reactions PEP node fetched on every Ready** (`UJXJYDOXP` in the + log). Same +notify treatment as MDS would solve it. + +--- + +## 6. Things we *cannot* avoid without more information + +Some of the startup chatter is simply structural and cannot be reduced +without protocol-level changes or new server hints: + +1. **MUC presence broadcast on join.** When we re-enter a MUC we will + receive presence for every occupant. That is the protocol; we cannot + suppress it. The cost is in caps disco (which we *can* cache; see + above) and in MUC avatar updates (vCard-temp photos in MUC presence) + — those are addressable by R4.1. +2. **Roster fetch.** The roster IQ is needed for subscription state; we + already use `ver=` (the log shows + ``) and the server + correctly returns an empty result when we are up to date. So this + one is already optimal. +3. **MAM "tail" query for chats with no local cache.** If we have + nothing to anchor against, we have to ask the server for the most + recent N. Nothing to fix here. +4. **Initial server stream features negotiation.** Out of scope for this + review. SASL2 / Bind 2 / FAST would amortise this and is being + tracked in `doc/plan-sasl2.md`. + +--- + +## 7. Concrete plan / TODO + +Tackling in roughly priority order: + +1. **R4.1 — DONE.** Centralised vCard cache guard added to + `_requestVcardDetails` (`lib/xmpp/xmpp_service.dart`). The decision + logic was extracted as `shouldFetchVcardForCache` in + `lib/xmpp/vcard_utils.dart` and is exercised by unit tests in + `test/vcard_utils_test.dart`. `_handleVcardPresenceUpdate` now passes + the freshly advertised `` hash through to the guard so a hash + change still triggers a refetch. _Impact: removes most of the vCard + storm at connect._ +2. **R3.3 — DONE.** `AvatarMetadata.noPepAvatar()` sentinel added in + `lib/models/avatar_metadata.dart` (with a guarded `fromMap` round-trip + so the sentinel survives across restarts even though `bytes` is `-1`). + `PepManager` now tracks pending `urn:xmpp:avatar:metadata` GET IQs + and, on a non-`RESULT` reply (e.g. ``), persists the sentinel via + `StorageService.storeAvatarMetadata`. `requestMetadataIfMissing` + short-circuits when the sentinel is in-cache. `avatarBytesFor` / + `avatarHashFor` treat sentinel entries as "no avatar". Real metadata + events overwrite the sentinel. New tests in + `test/pep_manager_test.dart` cover all four code paths. +3. **R1.1 — DONE.** `_setupDisplayedSync` now early-returns when the + in-memory `_displayedStanzaIdByChat` map was successfully restored + from disk. The decision lives in `shouldFetchDisplayedSyncBootstrap` + (`lib/xmpp/startup_fetch_helpers.dart`) and is exercised by + `test/startup_fetch_helpers_test.dart`. A `force` flag preserves the + ability to manually re-pull the entire MDS state when desired. +4. **R1.2 — DONE.** On audit it turned out + `urn:xmpp:mds:displayed:0+notify` was already present in + `vendor/xmpp_stone/lib/src/features/servicediscovery/ServiceDiscoverySupport.dart` + (alongside `urn:xmpp:avatar:metadata+notify`). The Section 1 narrative + above ("we currently do not advertise") was incorrect at the time the + review was written. To prevent silent regressions, a regression + suite `test/service_discovery_features_test.dart` now asserts that + the +notify entries (and other core disco features) remain in the + list. `doap.xml` already lists XEP-0490 as supported. +5. **R1.3 — DONE (partially).** A new on-disk key + `displayed_sync_pending` is added to `StorageService` + (`loadDisplayedSyncPending` / `storeDisplayedSyncPending`, + wiped alongside the existing displayed_sync map by + `clearDisplayedSync`). `_applyDisplayedStateForChat` now records the + unresolved (chatJid, stanzaId) pair in an in-memory + `_displayedSyncPending` map (seeded from disk in `attachStorage` and + cleared everywhere `_displayedStanzaIdByChat` was). A new + `_resolveDisplayedSyncPending` helper is invoked from every + message-append path (DM and MUC, including the MAM-prepend path) + and re-runs the matcher when the new message's stanza-id matches. + On a successful match the displayed timestamp is set and MAM catch-up + is marked complete for the chat — exactly as if MDS had matched on + first sight. + + Tests in `test/displayed_sync_pending_test.dart` cover the storage + round-trip and that `clearDisplayedSync` wipes both maps. + + _Not done in this commit:_ driving a single bounded MAM query + towards the persisted `stanza-id` (using it as `before=`). The + resolver above is sufficient for the most common case (the missing + message comes back through the existing per-chat catch-up, or a + later live message) and avoids changing MAM query routing in this + batch. The bounded-query optimisation would land cleanly as a + subsequent change, on top of the persistence introduced here. +6. **R2.2 — DONE.** `_primeMamSync` now consults + `shouldFetchMamCatchUpForChat` (`lib/xmpp/startup_fetch_helpers.dart`, + already shipped with R1.1) for every cached DM and every bookmarked + MUC; if the persisted displayed marker matches the stanza-id of the + newest local message, the per-chat MAM `` is skipped + entirely. A small new helper + `_stanzaIdAtLatestMamId(bareJid, {isRoom})` looks up the stanza-id of + the message at the latest MAM id, with a `null` fallback whenever + that information isn't available (in which case the catch-up still + fires, exactly as before). +7. **R5 / caps cache — DONE.** New persisted Entity Capabilities cache + in `StorageService` (key `entity_caps`, + `loadEntityCaps`/`storeEntityCaps`/`clearEntityCaps`). `PepCapsManager` + accepts an optional `StorageService` (passed in by `xmpp_service.dart`), + seeds `_capsFeatures` from disk in its constructor, and persists every + successful `disco#info` result. When MUC presence advertises a + `node#ver` we already cached, the disco IQ is short-circuited and the + features become immediately available via `featuresForBareJid`. New + tests in `test/pep_caps_manager_test.dart` cover (a) persistence on + disco result, (b) seed-from-disk skipping the disco IQ, and (c) + unknown `node#ver` still triggering disco even with a seeded cache. +8. **R2.1 — DONE.** Added the persisted global anchor + `last_mam_id_seen` to `StorageService` + (`loadLastMamIdSeen` / `storeLastMamIdSeen` / `clearLastMamIdSeen`). + `XmppService` seeds `_lastMamIdSeen` from disk in `attachStorage`, + exposes it via the new `lastMamIdSeen` getter, and bumps it from + every message-append path (DM, MUC tail, MUC MAM-prepend) via the + new private helper `_bumpLastMamIdSeen` (a lexicographic + compare-and-swap). New tests in `test/last_mam_id_seen_test.dart` + cover the storage round-trip. + + The unified catch-up query is now also implemented: `_primeMamSync` + issues a single `queryById(afterId: _lastMamIdSeen)` against the + user's own server archive (no `to=` JID) when the anchor is present, + replacing the O(N) per-chat DM fan-out with a single IQ. The server + returns all missed DM messages in one paginated stream; the existing + `_addMessage` routing dispatches each forwarded message to the + correct chat by JID. On completion the RSM `` id from the + `` result advances `_lastMamIdSeen`. When no anchor exists + (fresh install) the per-chat fan-out is used as a fallback. MUC + archives remain per-room (protocol requirement). New tests in + `test/unified_mam_catchup_test.dart` (6 tests covering `` + parsing) and `vendor/xmpp_stone/test/mam_query_xml_test.dart` + (1 test for the unified IQ XML shape). +9. **R3.1 — DONE.** `PepManager.gcUnreferencedAvatarBlobs()` evicts any + cached blob whose hash is not referenced by a current non-sentinel + `_metadataByJid` entry. The pass is invoked once from the constructor + immediately after the on-disk seeds are loaded, and is safe to call + ad-hoc (returns the eviction count). `StorageService` gained a small + `replaceAvatarBlobs(Map)` helper to persist the trimmed set in one + write. New tests in `test/pep_manager_test.dart` cover the eviction + semantics and idempotency on a clean cache. + +Each of the above can ship as its own PR with tests: + +* `MamCursorStore` already has a deterministic `now`/`schedule` seam, + so R2.2 is unit-testable without a network. +* `PepManager` is constructor-injectable with a mock `Connection`; the + +notify and 404 paths are easily testable. +* The vCard cache guard (R4.1) needs a unit test that asserts no IQ is + emitted when bytes for the advertised hash are already cached, and + that an IQ *is* emitted when the advertised hash differs. + +--- + +## Log review — 2026-05-01 13:40 session (`wimsy.log`) + +This section answers three questions about the log captured on 2026-05-01. + +### 1. Have the startup-fetch-review fixes taken effect? + +**No — the log predates all nine commits.** Every fix (R4.1 through R3.1) +was committed during the same session in which this log was captured, so +the binary that produced the log did not include any of them. The evidence +is clear: + +* Two "Displayed sync miss" lines appear at `13:40:16` — R1.1 (skip MDS + bootstrap IQ when cache is seeded) and R1.3 (persist pending markers) + were not active. +* 31 `` GET IQs are sent in the first ~5 seconds + after Ready — R4.1 (cache-guard) was not active. +* `urn:xmpp:avatar:metadata` is polled for every roster contact at Ready + (10+ IQs in the initial flush) — R3.3 (negative-cache sentinel) was not + active. +* Per-chat MAM catch-up queries fire for every bookmarked MUC regardless + of MDS state — R2.2 (skip when MDS proves up-to-date) was not active. + +A fresh log taken after a hot-restart with the new code should show: no +"Displayed sync miss" lines (or at most one per chat that has genuinely +new messages), no vCard storm, no avatar-metadata poll at Ready, and +significantly fewer MAM queries. + +### 2. Additional issues visible in this log + +**a) Double roster IQ at Ready** + +In the single Ready flush at `13:40:23.562` we send *two* roster IQs +back-to-back: + +``` +YIVVNRAHB ← bare, no ver= +MBVSUKUKH ← versioned +``` + +The server returns the full roster for `YIVVNRAHB` (ignoring the `ver=` +attribute because it is absent) and an empty result for `MBVSUKUKH` +(because the version matches). The bare IQ is redundant — we already have +a cached version token so we should only send the versioned form. This is +a minor bug in the Ready flush assembly: the bare IQ is likely emitted by +one code path and the versioned one by another, both firing at the same +time. + +**b) Carbons enabled twice** + +`enable xmlns="urn:xmpp:carbons:2"` is sent at two distinct points: + +1. `13:40:22.768` — `NJJOJDNQI` — during the post-bind feature negotiation + phase (the `ConnectionNegotiatorManager` carbons negotiator). +2. `13:40:23.562` — `HFWGKSCKB` — inside the Ready flush, as part of the + same batch that sends the roster IQ and presence. + +The second enable is harmless (the server just returns a result) but wastes +a round-trip. The Ready flush should check whether carbons were already +enabled by the negotiator and skip the redundant IQ. + +**c) `urn:xmpp:avatar:metadata` polled for every roster contact at Ready** + +Even though we advertise `urn:xmpp:avatar:metadata+notify` (confirmed by +R1.2 audit), the Ready flush still sends an explicit +`` IQ to every +roster contact that has a PEP node. With +notify in our caps, the server +will push any changed metadata to us automatically; the poll is only needed +for contacts whose server does not support PEP +notify. The current code +appears to poll unconditionally. A guard similar to R1.1 (skip when we +already have cached metadata and the contact's server supports +notify) +would eliminate this fan-out. This is a lower-priority item than the vCard +storm because the payloads are small, but with a large roster it adds up. + +### 3. The disconnect at the end + +The disconnect is **clean and server-initiated due to the idle timeout**. + +Evidence: + +* The first stream features stanza (pre-auth, `13:40:20`) advertises + `360` — the server will close the stream + after 360 seconds of inactivity. +* The connection became Ready at `13:40:23` and the initial burst of IQs + and MAM pages completed by roughly `13:40:40`. After that the log shows + only incoming MUC presence and vCard responses — no outbound traffic. +* At `13:41:08` (approximately 45 seconds after Ready, well within the + 360-second window) the log records `QUIC connection closed cleanly (no + error reported)` followed immediately by + `XmppConnectionState.ForcefullyClosed` and a reconnect schedule. + +The 45-second gap is shorter than the 360-second idle limit, which means +the server closed the connection for a reason *other* than the idle timer. +The most likely cause is that the Openfire server closed the QUIC +connection at the QUIC transport layer (a `CONNECTION_CLOSE` frame with +no application error), possibly because it considers the QUIC session +idle once the initial stream of stanzas drains, or because of a +server-side session management policy. The `QUIC connection closed cleanly` +message confirms there was no TLS or QUIC error — this is a graceful +`CONNECTION_CLOSE`. + +**What to do:** The reconnection manager correctly schedules a reconnect +(`delay=3847ms`). However, to avoid this happening in production we should +send a periodic XMPP ping (XEP-0199) to keep the QUIC connection alive. +The server already advertises `urn:xmpp:ping` in its disco features. A +ping every 60–90 seconds (well under the 360-second idle limit) would +prevent the server from closing the connection. This is separate from the +startup-fetch work and should be tracked as its own issue. + +--- + +## Log review — 2026-05-01 15:34 session (`wimsy.log`, updated) + +This section reviews the log captured at 15:34 on 2026-05-01, taken after +`flutter run -d linux` with the latest code (all nine R4.1–R3.1 commits +present). + +### 1. Have the startup-fetch-review fixes taken effect? + +**Partially — but a critical re-seed bug prevents most guards from working +on reconnect.** + +The log shows **six connection cycles** (Ready at 14:34:49, 14:35:46, +14:36:37, 14:37:37, 14:38:34, 14:39:26) caused by repeated server-initiated +QUIC disconnects. Per-cycle counts: + +| Cycle | vCard IQs | avatar:metadata IQs | MAM queries | +|-------|-----------|---------------------|-------------| +| 1 | 16 | 10 | 39 | +| 2 | 23 | 7 | 123 | +| 3 | 8 | 7 | 119 | +| 4 | 11 | 7 | 78 | +| 5 | 9 | 11 | 66 | +| 6 | 14 | 10 | 113 | + +Every cycle re-fetches vCards and avatar metadata, and re-runs MAM +catch-up queries. The root cause is a **re-seed bug**: + +* `_vcardAvatarBytes`, `_vcardAvatarState`, and `_mamCursorStore` are + cleared in the disconnect handler (`_safeClose`, around line 1165). +* They are seeded from disk only in `attachStorage` (line 453), which is + called **once at startup**, not on reconnect. +* The Ready handler (`XmppConnectionState.Ready` branch, line 1044) never + calls `_seedVcardAvatars` / `_seedVcardAvatarState` / re-seeds the MAM + cursor store. +* As a result, R4.1's `shouldFetchVcardForCache` guard sees an empty cache + on every reconnect and allows all vCard IQs through; R2.2's MAM skip + similarly has no cursor data to consult. + +**Fix (new item R6):** Add re-seed calls at the top of the Ready handler, +mirroring `attachStorage`: + +```dart +// In the XmppConnectionState.Ready branch, before _setupRoster() etc.: +if (_storage != null) { + _seedVcardAvatars(_storage!.loadVcardAvatars()); + _seedVcardAvatarState(_storage!.loadVcardAvatarState()); + // MAM cursor store is re-seeded via MamCoordinator.loadFromStorage() +} +``` + +`PepManager` is unaffected — `_setupPep` creates a fresh instance on every +reconnect and its constructor loads from `StorageService`, so it correctly +re-seeds itself. + +**What is working correctly:** + +* The "Displayed sync miss" lines (lines 79–80) fire *before* the first + Ready (line 171), from the disk-seed path. R1.3 persistence is working — + the stanza-ids are genuinely absent from the local cache (new messages + arrived while offline), which is the expected behaviour. +* `PepCapsManager` (R5) re-seeds on every `_setupPep` call — no caps + disco fan-out visible. +* The double roster IQ and double carbons-enable from the previous log + review are still present (not yet fixed). + +### 2. The repeated disconnects + +The server closes the QUIC connection every 45–60 seconds, consistently +mid-MAM-stream (the disconnect at 14:35:30 occurs while a large burst of +MAM pages is still arriving on `quic-aux-15`). This is **not** an idle +timeout — the 30-second foreground keepalive ping is correctly configured +in `StreamManagmentModule` (`_pingIntervalForeground = Duration(seconds: 30)`) +and fires at Ready. The server advertises `360`. + +The pattern — clean `QUIC connection closed cleanly (no error reported)` +mid-stream — points to an **Openfire QUIC session data or stream limit**: +the server appears to close the QUIC connection once a per-session byte or +stream count is reached, regardless of XMPP-level activity. One cycle also +shows `QUIC connection closed (could not query close reason: +DroppableDisposedException)`, suggesting the Rust QUIC layer is being torn +down before the close reason can be read. + +**What to do:** This is an Openfire server-side issue (likely a bug or +misconfiguration in its QUIC implementation). The client correctly +reconnects each time. Longer term, SASL2/Bind2 resumption would make +reconnects cheaper. For now, the reconnect loop is the correct behaviour. + +### 3. Summary of remaining work + +| Item | Status | Notes | +|------|--------|-------| +| R4.1 vCard cache guard | ⚠️ Partial | Guard logic correct; broken by re-seed bug (R6) | +| R3.3 PEP negative cache | ✅ Working | PepManager re-seeds on reconnect | +| R1.1 MDS bootstrap skip | ✅ Working | Fires correctly from disk cache | +| R1.2 MDS +notify disco | ✅ Working | Confirmed in caps | +| R1.3 Displayed sync pending | ✅ Working | Persists and resolves correctly | +| R2.2 MAM skip when MDS up-to-date | ⚠️ Partial | Broken by re-seed bug (R6) | +| R5 Caps cache | ✅ Working | Re-seeds via PepCapsManager constructor | +| R2.1 Global MAM anchor | ✅ Done | Anchor persisted; unified DM catch-up query implemented | +| R3.1 Avatar blob GC | ✅ Working | Runs at construction | +| **R6 Re-seed on reconnect** | ❌ Not done | New item; fixes R4.1 and R2.2 regressions | + +--- + +## Appendix: log evidence (excerpts from `wimsy.log`, 2026-05-01) + +* `Displayed sync miss for chat=xsf@muc.xmpp.org stanzaId=32d798f6-… + messages=25 knownStanzaIds=[5 ids]` — the local cache is too short to + resolve the displayed marker; we then re-pull MAM rather than + asking for "messages up to the displayed `stanza-id`". +* Multiple `` blasted + to every roster entry within ~50ms of becoming Ready. +* `enable xmlns="urn:xmpp:carbons:2"` sent twice (`KNBYYPMLT` and + `VSBVMXXRT`). +* `urn:xmpp:avatar:metadata` 404s for `test1@…`, `test2@…`, + `test-dino@…` — these will repeat on every restart until R3.3 is + applied. +* `summit@muc.xmpp.org` MAM page (`queryid="VPNAXKAHR"`) is a long + stream of forwarded historical messages running for several seconds + on the QUIC aux stream — this is exactly the kind of fan-out R2.1 is + designed to defer. diff --git a/lib/login_screen.dart b/lib/login_screen.dart new file mode 100644 index 0000000..d6ee942 --- /dev/null +++ b/lib/login_screen.dart @@ -0,0 +1,585 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:xmpp_stone/xmpp_stone.dart'; + +import 'storage/account_record.dart'; +import 'storage/preferences_service.dart'; +import 'storage/storage_service.dart'; +import 'xmpp/alt_connection.dart'; +import 'xmpp/ws_endpoint.dart'; +import 'xmpp/xmpp_service.dart'; + +/// Standalone login / account-setup screen. +/// +/// Reads the last-used account from [StorageService] and [PreferencesService], +/// presents the connection form (JID, password, advanced options), and calls +/// [XmppService.connect] when the user taps Connect. +class LoginScreen extends StatefulWidget { + const LoginScreen({ + super.key, + required this.service, + required this.storage, + required this.preferences, + }); + + final XmppService service; + final StorageService storage; + final PreferencesService preferences; + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final TextEditingController _jidController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _hostController = TextEditingController(); + final TextEditingController _portController = TextEditingController( + text: '5222', + ); + final TextEditingController _resourceController = TextEditingController( + text: 'wimsy', + ); + final TextEditingController _wsEndpointController = TextEditingController(); + final TextEditingController _wsProtocolsController = TextEditingController(); + + bool _loadedAccount = false; + bool _rememberPassword = false; + bool _useWebSocket = kIsWeb; + bool _useDirectTls = false; + /// Whether to attempt plain TCP (`_xmpp-client._tcp` SRV records and the + /// non-TLS fallback). When false, plain-TCP SRV records are ignored. + bool _useTcp = true; + /// Whether to attempt QUIC transport (XEP-0467) when available. + bool _useQuic = true; + bool _advancedOptionsExpanded = false; + bool _endpointDiscoveryBusy = false; + String? _endpointDiscoveryMessage; + String? _lastEndpointDiscoveryDomain; + Timer? _endpointDiscoveryDebounce; + + @override + void initState() { + super.initState(); + _loadAccount(); + } + + @override + void dispose() { + _endpointDiscoveryDebounce?.cancel(); + _jidController.dispose(); + _passwordController.dispose(); + _hostController.dispose(); + _portController.dispose(); + _resourceController.dispose(); + _wsEndpointController.dispose(); + _wsProtocolsController.dispose(); + super.dispose(); + } + + Future _loadAccount() async { + final cachedJid = widget.preferences.lastJid; + final account = AccountRecord.fromMap(widget.storage.loadAccount()); + if (!mounted) { + return; + } + setState(() { + if (cachedJid != null && cachedJid.isNotEmpty) { + _jidController.text = cachedJid; + } + if (account != null) { + _jidController.text = account.jid; + _rememberPassword = account.rememberPassword; + if (_rememberPassword) { + _passwordController.text = account.password; + } else { + _passwordController.clear(); + } + _hostController.text = account.host; + _portController.text = account.port.toString(); + _resourceController.text = account.resource; + _useWebSocket = kIsWeb ? true : account.useWebSocket; + _useDirectTls = kIsWeb ? false : account.directTls; + _useTcp = kIsWeb ? true : account.useTcp; + _useQuic = account.useQuic; + _wsEndpointController.text = account.wsEndpoint; + if (account.wsProtocols.isNotEmpty) { + _wsProtocolsController.text = account.wsProtocols.join(', '); + } else { + _wsProtocolsController.clear(); + } + } + _loadedAccount = true; + }); + } + + void _handleConnect() { + final port = int.tryParse(_portController.text.trim()) ?? 5222; + final useWebSocket = kIsWeb || _useWebSocket; + final useDirectTls = kIsWeb ? false : _useDirectTls; + final useTcp = kIsWeb ? true : _useTcp; + final wsProtocols = _wsProtocolsController.text + .split(',') + .map((entry) => entry.trim()) + .where((entry) => entry.isNotEmpty) + .toList(); + final account = AccountRecord( + jid: _jidController.text.trim(), + password: _rememberPassword ? _passwordController.text : '', + host: _hostController.text.trim(), + port: port, + resource: _resourceController.text.trim().isEmpty + ? 'wimsy' + : _resourceController.text.trim(), + rememberPassword: _rememberPassword, + useWebSocket: useWebSocket, + directTls: useDirectTls, + wsEndpoint: _wsEndpointController.text.trim(), + wsProtocols: wsProtocols, + useQuic: _useQuic, + useTcp: useTcp, + ); + widget.storage.storeAccount(account.toMap()); + unawaited(widget.preferences.setLastJid(account.jid)); + widget.service.connect( + jid: account.jid, + password: _passwordController.text, + resource: account.resource, + host: account.host, + port: port, + useWebSocket: useWebSocket, + directTls: useDirectTls, + wsEndpoint: account.wsEndpoint, + wsProtocols: wsProtocols, + useQuic: _useQuic, + useTcp: useTcp, + ); + } + + void _scheduleEndpointDiscovery(String jid, {bool immediate = false}) { + if (!kIsWeb && !_useWebSocket) { + return; + } + final trimmed = jid.trim(); + if (trimmed.isEmpty) { + if (_endpointDiscoveryMessage != null) { + setState(() { + _endpointDiscoveryMessage = null; + _endpointDiscoveryBusy = false; + }); + } + return; + } + _endpointDiscoveryDebounce?.cancel(); + if (immediate) { + unawaited(_discoverEndpoint(trimmed)); + return; + } + _endpointDiscoveryDebounce = Timer( + const Duration(milliseconds: 700), + () => _discoverEndpoint(trimmed), + ); + } + + Future _discoverEndpoint(String jid) async { + final parsed = Jid.fromFullJid(jid); + if (!parsed.isValid()) { + return; + } + final domain = _domainFromBareJid(parsed.userAtDomain); + if (domain.isEmpty) { + return; + } + if (_endpointDiscoveryBusy && _lastEndpointDiscoveryDomain == domain) { + return; + } + if (_lastEndpointDiscoveryDomain == domain && + _wsEndpointController.text.trim().isNotEmpty) { + return; + } + setState(() { + _endpointDiscoveryBusy = true; + _endpointDiscoveryMessage = 'Discovering WebSocket endpoint…'; + }); + _lastEndpointDiscoveryDomain = domain; + final discovered = await discoverWebSocketEndpoint(domain); + if (!mounted) { + return; + } + if (discovered != null) { + final parsedEndpoint = parseWsEndpoint(discovered.toString()); + if (parsedEndpoint != null) { + _wsEndpointController.text = parsedEndpoint.uri.toString(); + } + setState(() { + _endpointDiscoveryBusy = false; + _endpointDiscoveryMessage = + 'WebSocket endpoint discovered for $domain.'; + }); + return; + } + setState(() { + _endpointDiscoveryBusy = false; + _endpointDiscoveryMessage = + 'Could not discover a WebSocket endpoint for $domain. Enter one in advanced options.'; + _advancedOptionsExpanded = true; + }); + } + + String _domainFromBareJid(String bareJid) { + final parts = bareJid.split('@'); + return parts.length == 2 ? parts[1] : ''; + } + + Widget _buildAdvancedOptions(XmppService service, ThemeData theme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: service.isConnecting + ? null + : () { + setState(() { + _advancedOptionsExpanded = !_advancedOptionsExpanded; + }); + }, + icon: Icon( + _advancedOptionsExpanded ? Icons.expand_less : Icons.expand_more, + ), + label: Text( + _advancedOptionsExpanded + ? 'Hide advanced options' + : 'Show advanced options', + ), + ), + if (_advancedOptionsExpanded) ...[ + const SizedBox(height: 4), + Row( + children: [ + Expanded( + flex: 2, + child: TextField( + controller: _hostController, + enabled: !service.isConnecting, + decoration: const InputDecoration( + labelText: 'Host (optional)', + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _portController, + enabled: !service.isConnecting, + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: 'Port'), + ), + ), + ], + ), + const SizedBox(height: 12), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: const Text('Direct TLS (XEP-0368)'), + subtitle: const Text( + 'Uses direct TLS when the server advertises it via SRV. ' + 'When off, _xmpps-client._tcp SRV records are ignored.', + ), + value: _useDirectTls, + onChanged: service.isConnecting || kIsWeb + ? null + : (value) { + if (value == null) { + return; + } + setState(() { + _useDirectTls = value; + }); + }, + ), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: const Text('Plain TCP'), + subtitle: const Text( + 'Allows plain TCP connections via _xmpp-client._tcp SRV. ' + 'When off, plain-TCP SRV records are ignored and no plain-TCP ' + 'connection will be attempted.', + ), + value: kIsWeb ? true : _useTcp, + onChanged: service.isConnecting || kIsWeb + ? null + : (value) { + if (value == null) { + return; + } + setState(() { + _useTcp = value; + }); + }, + ), + const SizedBox(height: 12), + TextField( + controller: _resourceController, + enabled: !service.isConnecting, + decoration: const InputDecoration(labelText: 'Resource'), + ), + const SizedBox(height: 12), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: const Text('Use QUIC transport (XEP-0467)'), + subtitle: const Text( + 'Enables QUIC when the server advertises it via SRV. Disable to isolate QUIC issues.', + ), + value: kIsWeb ? false : _useQuic, + onChanged: service.isConnecting || kIsWeb + ? null + : (value) { + if (value == null) { + return; + } + setState(() { + _useQuic = value; + }); + }, + ), + const SizedBox(height: 12), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: const Text('Use WebSocket transport'), + subtitle: kIsWeb + ? const Text('Required for web builds.') + : const Text('Useful for testing server WebSocket support.'), + value: kIsWeb ? true : _useWebSocket, + onChanged: service.isConnecting || kIsWeb + ? null + : (value) { + if (value == null) { + return; + } + setState(() { + _useWebSocket = value; + }); + }, + ), + if (_useWebSocket || kIsWeb) ...[ + const SizedBox(height: 12), + TextField( + controller: _wsEndpointController, + enabled: !service.isConnecting, + decoration: const InputDecoration( + labelText: 'WebSocket endpoint', + hintText: 'wss://host/xmpp-websocket', + ), + ), + const SizedBox(height: 12), + TextField( + controller: _wsProtocolsController, + enabled: !service.isConnecting, + decoration: const InputDecoration( + labelText: 'WebSocket subprotocols (optional)', + hintText: 'xmpp, stanza', + ), + ), + ], + ], + ], + ); + } + + @override + Widget build(BuildContext context) { + final service = widget.service; + final theme = Theme.of(context); + final size = MediaQuery.of(context).size; + final stateLabel = service.lastConnectionState?.name ?? 'Idle'; + + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFF7E7CE), Color(0xFFE3F0F1), Color(0xFFFDFBF7)], + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: size.width > 640 ? 520 : double.infinity, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wimsy', + style: theme.textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w300, + letterSpacing: 1.2, + color: const Color(0xFFA97BFF), + fontFamily: 'SF Pro Display', + fontFamilyFallback: const [ + 'Helvetica Neue', + 'Arial', + 'Roboto', + ], + ), + ), + const SizedBox(height: 8), + Text( + 'A modern XMPP client built for secure servers.', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 24), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 24, + offset: const Offset(0, 12), + ), + ], + ), + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Connect', style: theme.textTheme.titleLarge), + const SizedBox(height: 16), + TextField( + controller: _jidController, + enabled: !service.isConnecting, + decoration: const InputDecoration( + labelText: 'JID', + hintText: 'user@domain', + ), + onChanged: (value) => + _scheduleEndpointDiscovery(value), + onEditingComplete: () => _scheduleEndpointDiscovery( + _jidController.text.trim(), + immediate: true, + ), + ), + if (_endpointDiscoveryMessage != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + if (_endpointDiscoveryBusy) + const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + if (_endpointDiscoveryBusy) + const SizedBox(width: 8), + Expanded( + child: Text( + _endpointDiscoveryMessage!, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ], + const SizedBox(height: 12), + TextField( + controller: _passwordController, + enabled: !service.isConnecting, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Password', + ), + ), + const SizedBox(height: 12), + _buildAdvancedOptions(service, theme), + const SizedBox(height: 12), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: const Text( + 'Remember password on this device', + ), + value: _rememberPassword, + onChanged: service.isConnecting + ? null + : (value) { + if (value == null) { + return; + } + setState(() { + _rememberPassword = value; + }); + }, + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: FilledButton( + onPressed: service.isConnecting + ? null + : _handleConnect, + child: Text( + service.isConnecting + ? 'Connecting...' + : 'Connect', + ), + ), + ), + ], + ), + if (!_loadedAccount) ...[ + const SizedBox(height: 12), + Text( + 'Unlocking storage...', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 12), + Text( + 'Status: ${service.status.name} · $stateLabel', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + if (service.errorMessage != null) ...[ + const SizedBox(height: 16), + Text( + service.errorMessage!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 923766e..e8ca603 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,28 +12,24 @@ import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:xmpp_stone/xmpp_stone.dart'; import 'package:url_launcher/url_launcher.dart'; import 'av/call_session.dart'; +import 'login_screen.dart'; import 'models/chat_message.dart'; import 'models/contact_entry.dart'; import 'models/room_entry.dart'; import 'notifications/notification_service.dart'; -import 'storage/account_record.dart'; +import 'storage/preferences_service.dart'; import 'storage/storage_service.dart'; import 'xmpp/xmpp_service.dart'; import 'xmpp/jid_discovery.dart'; -import 'xmpp/alt_connection.dart'; -import 'xmpp/ws_endpoint.dart'; import 'background/foreground_task_handler.dart'; import 'utils/xep0392_color.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -const String _sentryOptInKey = 'sentry_opt_in'; -const String _pinIgnoredKey = 'wimsy_pin_ignored'; const List _defaultReactionOptions = [ '👍', '❤️', @@ -43,18 +39,28 @@ const List _defaultReactionOptions = [ '👎', ]; +@pragma('vm:entry-point') +void startCallback() { + FlutterForegroundTask.setTaskHandler(WimsyForegroundTaskHandler()); +} + Future main() async { WidgetsFlutterBinding.ensureInitialized(); - Log.logLevel = LogLevel.VERBOSE; - Log.logXmpp = true; - final prefs = await SharedPreferences.getInstance(); - final optIn = prefs.getBool(_sentryOptInKey) ?? false; - await _startApp(sentryEnabled: optIn); + assert(() { + Log.logLevel = LogLevel.VERBOSE; + Log.logXmpp = true; + return true; + }()); + final prefs = await PreferencesService.load(); + await _startApp(sentryEnabled: prefs.sentryOptIn, preferences: prefs); } const bool _isFlutterTest = bool.fromEnvironment('FLUTTER_TEST'); -Future _startApp({required bool sentryEnabled}) async { +Future _startApp({ + required bool sentryEnabled, + required PreferencesService preferences, +}) async { if (sentryEnabled) { await SentryFlutter.init( (options) { @@ -66,31 +72,35 @@ Future _startApp({required bool sentryEnabled}) async { Connection.errorReporter = (error, stackTrace) { Sentry.captureException(error, stackTrace: stackTrace); }; - runApp(SentryWidget(child: const WimsyApp())); + runApp(SentryWidget(child: WimsyApp(preferences: preferences))); }, ); return; } Connection.errorReporter = null; - runApp(const WimsyApp()); + runApp(WimsyApp(preferences: preferences)); } Future _enableSentryAndRestart() async { if (Sentry.isEnabled) { return; } - await _startApp(sentryEnabled: true); + final prefs = await PreferencesService.load(); + await _startApp(sentryEnabled: true, preferences: prefs); } Future _restartWithoutSentry() async { if (!Sentry.isEnabled) { return; } - await _startApp(sentryEnabled: false); + final prefs = await PreferencesService.load(); + await _startApp(sentryEnabled: false, preferences: prefs); } class WimsyApp extends StatefulWidget { - const WimsyApp({super.key}); + const WimsyApp({super.key, required this.preferences}); + + final PreferencesService preferences; @override State createState() => _WimsyAppState(); @@ -101,6 +111,7 @@ class _WimsyAppState extends State with WidgetsBindingObserver { final StorageService _storage = StorageService(); final NotificationService _notifications = NotificationService(); final Connectivity _connectivity = Connectivity(); + PreferencesService get _preferences => widget.preferences; StreamSubscription>? _connectivitySubscription; bool _appIsForeground = true; late final Future _initFuture; @@ -162,7 +173,7 @@ class _WimsyAppState extends State with WidgetsBindingObserver { } final title = _service.displayNameFor(bareJid); _notifications.showMessage( - id: DateTime.now().millisecondsSinceEpoch.remainder(1 << 31), + id: bareJid.hashCode.abs() % (1 << 31), title: title, body: message.body, chatJid: bareJid, @@ -179,7 +190,7 @@ class _WimsyAppState extends State with WidgetsBindingObserver { } final title = '$roomJid • ${message.from}'; _notifications.showMessage( - id: DateTime.now().millisecondsSinceEpoch.remainder(1 << 31), + id: roomJid.hashCode.abs() % (1 << 31), title: title, body: message.body, chatJid: roomJid, @@ -283,13 +294,12 @@ class _WimsyAppState extends State with WidgetsBindingObserver { ), ); - FlutterForegroundTask.setTaskHandler(WimsyForegroundTaskHandler()); - final running = await FlutterForegroundTask.isRunningService; if (!running) { await FlutterForegroundTask.startService( notificationTitle: 'Wimsy is running', notificationText: 'Keeping your XMPP session connected.', + callback: startCallback, ); } } @@ -351,6 +361,7 @@ class _WimsyAppState extends State with WidgetsBindingObserver { service: _service, storage: _storage, notifications: _notifications, + preferences: _preferences, ); }, ), @@ -365,11 +376,13 @@ class _Gatekeeper extends StatefulWidget { required this.service, required this.storage, required this.notifications, + required this.preferences, }); final XmppService service; final StorageService storage; final NotificationService notifications; + final PreferencesService preferences; @override State<_Gatekeeper> createState() => _GatekeeperState(); @@ -390,14 +403,13 @@ class _GatekeeperState extends State<_Gatekeeper> { final hasPin = await widget.storage.hasPin(); var pinIgnored = false; if (hasPin) { - final prefs = await SharedPreferences.getInstance(); - pinIgnored = prefs.getBool(_pinIgnoredKey) ?? false; + pinIgnored = widget.preferences.pinIgnored; if (pinIgnored) { try { await widget.storage.unlock('0000'); } catch (_) { pinIgnored = false; - await prefs.setBool(_pinIgnoredKey, false); + await widget.preferences.setPinIgnored(false); } } } @@ -418,10 +430,10 @@ class _GatekeeperState extends State<_Gatekeeper> { } if (!_hasPin) { return _PinSetupScreen( + preferences: widget.preferences, onPinSet: (pin, {required ignored}) async { await widget.storage.setupPin(pin); - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_pinIgnoredKey, ignored); + await widget.preferences.setPinIgnored(ignored); if (!mounted) { return; } @@ -435,6 +447,7 @@ class _GatekeeperState extends State<_Gatekeeper> { if (!widget.storage.isUnlocked) { return _PinUnlockScreen( pinIgnored: _pinIgnored, + preferences: widget.preferences, onUnlocked: (pin) async { await widget.storage.unlock(pin); if (!mounted) { @@ -448,6 +461,7 @@ class _GatekeeperState extends State<_Gatekeeper> { service: widget.service, storage: widget.storage, notifications: widget.notifications, + preferences: widget.preferences, ); } } @@ -458,42 +472,24 @@ class WimsyHome extends StatefulWidget { required this.service, required this.storage, required this.notifications, + required this.preferences, }); final XmppService service; final StorageService storage; final NotificationService notifications; + final PreferencesService preferences; @override State createState() => _WimsyHomeState(); } class _WimsyHomeState extends State { - final TextEditingController _jidController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _hostController = TextEditingController(); - final TextEditingController _portController = TextEditingController( - text: '5222', - ); - final TextEditingController _resourceController = TextEditingController( - text: 'wimsy', - ); - final TextEditingController _wsEndpointController = TextEditingController(); - final TextEditingController _wsProtocolsController = TextEditingController(); final TextEditingController _messageController = TextEditingController(); final FocusNode _messageFocusNode = FocusNode(); final ScrollController _messageScrollController = ScrollController(); final Map _lastReadAtByChat = {}; - bool _loadedAccount = false; bool _clearingCache = false; - bool _rememberPassword = false; - bool _useWebSocket = kIsWeb; - bool _useDirectTls = false; - Timer? _endpointDiscoveryDebounce; - bool _advancedOptionsExpanded = false; - bool _endpointDiscoveryBusy = false; - String? _endpointDiscoveryMessage; - String? _lastEndpointDiscoveryDomain; Timer? _typingDebounce; Timer? _idleTimer; ChatState? _lastSentChatState; @@ -538,7 +534,6 @@ class _WimsyHomeState extends State { _seedMessages(); _seedRoomMessages(); _loadMediaPreferences(); - _loadAccount(); } Future _seedRoster() async { @@ -561,45 +556,9 @@ class _WimsyHomeState extends State { widget.service.seedBookmarks(bookmarks); } - Future _loadAccount() async { - final prefs = await SharedPreferences.getInstance(); - final cachedJid = prefs.getString('wimsy_last_jid'); - final account = AccountRecord.fromMap(widget.storage.loadAccount()); - if (!mounted) { - return; - } - setState(() { - if (cachedJid != null && cachedJid.isNotEmpty) { - _jidController.text = cachedJid; - } - if (account != null) { - _jidController.text = account.jid; - _rememberPassword = account.rememberPassword; - if (_rememberPassword) { - _passwordController.text = account.password; - } else { - _passwordController.clear(); - } - _hostController.text = account.host; - _portController.text = account.port.toString(); - _resourceController.text = account.resource; - _useWebSocket = kIsWeb ? true : account.useWebSocket; - _useDirectTls = kIsWeb ? false : account.directTls; - _wsEndpointController.text = account.wsEndpoint; - if (account.wsProtocols.isNotEmpty) { - _wsProtocolsController.text = account.wsProtocols.join(', '); - } else { - _wsProtocolsController.clear(); - } - } - _loadedAccount = true; - }); - } - Future _loadMediaPreferences() async { - final prefs = await SharedPreferences.getInstance(); - final audioInput = prefs.getString('wimsy_audio_input'); - final videoInput = prefs.getString('wimsy_video_input'); + final audioInput = widget.preferences.audioInputId; + final videoInput = widget.preferences.videoInputId; if (audioInput != null && audioInput.isNotEmpty) { widget.service.selectAudioInput(audioInput); } @@ -612,18 +571,10 @@ class _WimsyHomeState extends State { void dispose() { _typingDebounce?.cancel(); _idleTimer?.cancel(); - _endpointDiscoveryDebounce?.cancel(); _messageScrollController.removeListener(_handleScrollPosition); HardwareKeyboard.instance.removeHandler(_handleHardwareKey); _messageFocusNode.dispose(); _messageScrollController.dispose(); - _jidController.dispose(); - _passwordController.dispose(); - _hostController.dispose(); - _portController.dispose(); - _resourceController.dispose(); - _wsEndpointController.dispose(); - _wsProtocolsController.dispose(); _messageController.dispose(); super.dispose(); } @@ -662,199 +613,17 @@ class _WimsyHomeState extends State { builder: (context, _) { final service = widget.service; if (!service.isConnected) { - return _buildLogin(context, service); + return LoginScreen( + service: service, + storage: widget.storage, + preferences: widget.preferences, + ); } return _buildClient(context, service); }, ); } - Widget _buildLogin(BuildContext context, XmppService service) { - final theme = Theme.of(context); - final size = MediaQuery.of(context).size; - final stateLabel = service.lastConnectionState?.name ?? 'Idle'; - - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFFF7E7CE), Color(0xFFE3F0F1), Color(0xFFFDFBF7)], - ), - ), - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: size.width > 640 ? 520 : double.infinity, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wimsy', - style: theme.textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.w300, - letterSpacing: 1.2, - color: const Color(0xFFA97BFF), - fontFamily: 'SF Pro Display', - fontFamilyFallback: const [ - 'Helvetica Neue', - 'Arial', - 'Roboto', - ], - ), - ), - const SizedBox(height: 8), - Text( - 'A modern XMPP client built for secure servers.', - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 24), - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 24, - offset: const Offset(0, 12), - ), - ], - ), - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Connect', style: theme.textTheme.titleLarge), - const SizedBox(height: 16), - TextField( - controller: _jidController, - enabled: !service.isConnecting, - decoration: const InputDecoration( - labelText: 'JID', - hintText: 'user@domain', - ), - onChanged: (value) => - _scheduleEndpointDiscovery(value), - onEditingComplete: () => _scheduleEndpointDiscovery( - _jidController.text.trim(), - immediate: true, - ), - ), - if (_endpointDiscoveryMessage != null) ...[ - const SizedBox(height: 6), - Row( - children: [ - if (_endpointDiscoveryBusy) - const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ), - if (_endpointDiscoveryBusy) - const SizedBox(width: 8), - Expanded( - child: Text( - _endpointDiscoveryMessage!, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ], - const SizedBox(height: 12), - TextField( - controller: _passwordController, - enabled: !service.isConnecting, - obscureText: true, - decoration: const InputDecoration( - labelText: 'Password', - ), - ), - const SizedBox(height: 12), - _buildAdvancedOptions(service, theme), - const SizedBox(height: 12), - CheckboxListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: const Text('Remember password on this device'), - value: _rememberPassword, - onChanged: service.isConnecting - ? null - : (value) { - if (value == null) { - return; - } - setState(() { - _rememberPassword = value; - }); - }, - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: FilledButton( - onPressed: service.isConnecting - ? null - : _handleConnect, - child: Text( - service.isConnecting - ? 'Connecting...' - : 'Connect', - ), - ), - ), - ], - ), - if (!_loadedAccount) ...[ - const SizedBox(height: 12), - Text( - 'Unlocking storage...', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - const SizedBox(height: 12), - Text( - 'Status: ${service.status.name} · $stateLabel', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - if (service.errorMessage != null) ...[ - const SizedBox(height: 16), - Text( - service.errorMessage!, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.error, - ), - ), - ], - ], - ), - ), - ], - ), - ), - ), - ), - ), - ); - } Widget _buildClient(BuildContext context, XmppService service) { return LayoutBuilder( @@ -867,8 +636,25 @@ class _WimsyHomeState extends State { appBar: AppBar( title: Text('Signed in as ${service.currentUserBareJid ?? ''}'), actions: [ + if (service.quicRttHistory.isNotEmpty) ...[ + _QuicStatsGraph( + label: 'RTT', + data: service.quicRttHistory, + color: Colors.blue, + unit: 'ms', + ), + const SizedBox(width: 8), + _QuicStatsGraph( + label: 'Loss', + data: service.quicLossHistory, + color: Colors.red, + unit: '', + ), + const SizedBox(width: 16), + ], _PresenceMenu( service: service, + preferences: widget.preferences, onClearCacheExit: _clearingCache ? null : _confirmClearCacheAndExit, @@ -1747,46 +1533,6 @@ class _WimsyHomeState extends State { ); } - void _handleConnect() { - final port = int.tryParse(_portController.text.trim()) ?? 5222; - final useWebSocket = kIsWeb || _useWebSocket; - final useDirectTls = kIsWeb ? false : _useDirectTls; - final wsProtocols = _wsProtocolsController.text - .split(',') - .map((entry) => entry.trim()) - .where((entry) => entry.isNotEmpty) - .toList(); - final account = AccountRecord( - jid: _jidController.text.trim(), - password: _rememberPassword ? _passwordController.text : '', - host: _hostController.text.trim(), - port: port, - resource: _resourceController.text.trim().isEmpty - ? 'wimsy' - : _resourceController.text.trim(), - rememberPassword: _rememberPassword, - useWebSocket: useWebSocket, - directTls: useDirectTls, - wsEndpoint: _wsEndpointController.text.trim(), - wsProtocols: wsProtocols, - ); - widget.storage.storeAccount(account.toMap()); - SharedPreferences.getInstance().then((prefs) { - prefs.setString('wimsy_last_jid', account.jid); - }); - widget.service.connect( - jid: account.jid, - password: _passwordController.text, - resource: account.resource, - host: account.host, - port: port, - useWebSocket: useWebSocket, - directTls: useDirectTls, - wsEndpoint: account.wsEndpoint, - wsProtocols: wsProtocols, - ); - } - void _sendMessage(String? activeChat) { if (activeChat == null) { return; @@ -2083,9 +1829,7 @@ class _WimsyHomeState extends State { : null, onTap: () async { widget.service.selectAudioInput(inputs[i].deviceId); - final prefs = await SharedPreferences.getInstance(); - await prefs.setString( - 'wimsy_audio_input', + await widget.preferences.setAudioInputId( inputs[i].deviceId, ); if (context.mounted) { @@ -2128,9 +1872,7 @@ class _WimsyHomeState extends State { : null, onTap: () async { widget.service.selectVideoInput(inputs[i].deviceId); - final prefs = await SharedPreferences.getInstance(); - await prefs.setString( - 'wimsy_video_input', + await widget.preferences.setVideoInputId( inputs[i].deviceId, ); if (context.mounted) { @@ -2541,123 +2283,6 @@ class _WimsyHomeState extends State { ); } - Widget _buildAdvancedOptions(XmppService service, ThemeData theme) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton.icon( - onPressed: service.isConnecting - ? null - : () { - setState(() { - _advancedOptionsExpanded = !_advancedOptionsExpanded; - }); - }, - icon: Icon( - _advancedOptionsExpanded ? Icons.expand_less : Icons.expand_more, - ), - label: Text( - _advancedOptionsExpanded - ? 'Hide advanced options' - : 'Show advanced options', - ), - ), - if (_advancedOptionsExpanded) ...[ - const SizedBox(height: 4), - Row( - children: [ - Expanded( - flex: 2, - child: TextField( - controller: _hostController, - enabled: !service.isConnecting, - decoration: const InputDecoration( - labelText: 'Host (optional)', - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _portController, - enabled: !service.isConnecting, - keyboardType: TextInputType.number, - decoration: const InputDecoration(labelText: 'Port'), - ), - ), - ], - ), - const SizedBox(height: 12), - CheckboxListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: const Text('Direct TLS (XEP-0368)'), - subtitle: const Text( - 'Uses direct TLS when the server advertises it via SRV.', - ), - value: _useDirectTls, - onChanged: service.isConnecting || kIsWeb - ? null - : (value) { - if (value == null) { - return; - } - setState(() { - _useDirectTls = value; - }); - }, - ), - const SizedBox(height: 12), - TextField( - controller: _resourceController, - enabled: !service.isConnecting, - decoration: const InputDecoration(labelText: 'Resource'), - ), - const SizedBox(height: 12), - CheckboxListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: const Text('Use WebSocket transport'), - subtitle: kIsWeb - ? const Text('Required for web builds.') - : const Text('Useful for testing server WebSocket support.'), - value: kIsWeb ? true : _useWebSocket, - onChanged: service.isConnecting || kIsWeb - ? null - : (value) { - if (value == null) { - return; - } - setState(() { - _useWebSocket = value; - }); - }, - ), - if (_useWebSocket || kIsWeb) ...[ - const SizedBox(height: 12), - TextField( - controller: _wsEndpointController, - enabled: !service.isConnecting, - decoration: const InputDecoration( - labelText: 'WebSocket endpoint', - hintText: 'wss://host/xmpp-websocket', - ), - ), - const SizedBox(height: 12), - TextField( - controller: _wsProtocolsController, - enabled: !service.isConnecting, - decoration: const InputDecoration( - labelText: 'WebSocket subprotocols (optional)', - hintText: 'xmpp, stanza', - ), - ), - ], - ], - ], - ); - } - Widget _buildRoomSubjectHeader({ required RoomEntry? roomEntry, required String roomJid, @@ -3575,81 +3200,6 @@ class _WimsyHomeState extends State { widget.service.setMyChatState(bareJid, state); } - void _scheduleEndpointDiscovery(String jid, {bool immediate = false}) { - if (!kIsWeb && !_useWebSocket) { - return; - } - final trimmed = jid.trim(); - if (trimmed.isEmpty) { - if (_endpointDiscoveryMessage != null) { - setState(() { - _endpointDiscoveryMessage = null; - _endpointDiscoveryBusy = false; - }); - } - return; - } - _endpointDiscoveryDebounce?.cancel(); - if (immediate) { - unawaited(_discoverEndpoint(trimmed)); - return; - } - _endpointDiscoveryDebounce = Timer( - const Duration(milliseconds: 700), - () => _discoverEndpoint(trimmed), - ); - } - - Future _discoverEndpoint(String jid) async { - final parsed = Jid.fromFullJid(jid); - if (!parsed.isValid()) { - return; - } - final domain = _domainFromBareJid(parsed.userAtDomain); - if (domain.isEmpty) { - return; - } - if (_endpointDiscoveryBusy && _lastEndpointDiscoveryDomain == domain) { - return; - } - if (_lastEndpointDiscoveryDomain == domain && - _wsEndpointController.text.trim().isNotEmpty) { - return; - } - setState(() { - _endpointDiscoveryBusy = true; - _endpointDiscoveryMessage = 'Discovering WebSocket endpoint…'; - }); - _lastEndpointDiscoveryDomain = domain; - final discovered = await discoverWebSocketEndpoint(domain); - if (!mounted) { - return; - } - if (discovered != null) { - final parsedEndpoint = parseWsEndpoint(discovered.toString()); - if (parsedEndpoint != null) { - _wsEndpointController.text = parsedEndpoint.uri.toString(); - } - setState(() { - _endpointDiscoveryBusy = false; - _endpointDiscoveryMessage = - 'WebSocket endpoint discovered for $domain.'; - }); - return; - } - setState(() { - _endpointDiscoveryBusy = false; - _endpointDiscoveryMessage = - 'Could not discover a WebSocket endpoint for $domain. Enter one in advanced options.'; - _advancedOptionsExpanded = true; - }); - } - - String _domainFromBareJid(String bareJid) { - final parts = bareJid.split('@'); - return parts.length == 2 ? parts[1] : ''; - } - void _handleAutoScroll(int messageCount) { if (messageCount == _lastMessageCount) { return; @@ -5261,17 +4811,18 @@ Color _presenceDotColor(ThemeData theme, PresenceShowElement? show) { class _PresenceMenu extends StatelessWidget { const _PresenceMenu({ required this.service, + required this.preferences, required this.onClearCacheExit, required this.onExit, }); final XmppService service; + final PreferencesService preferences; final VoidCallback? onClearCacheExit; final VoidCallback onExit; Future _setSentryOptIn(BuildContext context, bool enabled) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_sentryOptInKey, enabled); + await preferences.setSentryOptIn(enabled); if (enabled) { await _enableSentryAndRestart(); return; @@ -5279,10 +4830,7 @@ class _PresenceMenu extends StatelessWidget { await _restartWithoutSentry(); } - Future _getSentryOptIn() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_sentryOptInKey) ?? false; - } + Future _getSentryOptIn() async => preferences.sentryOptIn; Future _editProfile(BuildContext context) async { final selfJid = service.currentUserBareJid; @@ -6064,9 +5612,13 @@ class _CropMaskPainter extends CustomPainter { } class _PinSetupScreen extends StatefulWidget { - const _PinSetupScreen({required this.onPinSet}); + const _PinSetupScreen({ + required this.onPinSet, + required this.preferences, + }); final Future Function(String pin, {required bool ignored}) onPinSet; + final PreferencesService preferences; @override State<_PinSetupScreen> createState() => _PinSetupScreenState(); @@ -6181,8 +5733,7 @@ class _PinSetupScreenState extends State<_PinSetupScreen> { _submitting = true; }); try { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_sentryOptInKey, _sentryOptIn); + await widget.preferences.setSentryOptIn(_sentryOptIn); await widget.onPinSet(pin, ignored: false); if (_sentryOptIn && mounted) { await _enableSentryAndRestart(); @@ -6202,8 +5753,7 @@ class _PinSetupScreenState extends State<_PinSetupScreen> { _submitting = true; }); try { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_sentryOptInKey, _sentryOptIn); + await widget.preferences.setSentryOptIn(_sentryOptIn); await widget.onPinSet('0000', ignored: true); if (_sentryOptIn && mounted) { await _enableSentryAndRestart(); @@ -6219,10 +5769,15 @@ class _PinSetupScreenState extends State<_PinSetupScreen> { } class _PinUnlockScreen extends StatefulWidget { - const _PinUnlockScreen({required this.onUnlocked, required this.pinIgnored}); + const _PinUnlockScreen({ + required this.onUnlocked, + required this.pinIgnored, + required this.preferences, + }); final Future Function(String pin) onUnlocked; final bool pinIgnored; + final PreferencesService preferences; @override State<_PinUnlockScreen> createState() => _PinUnlockScreenState(); @@ -6320,10 +5875,9 @@ class _PinUnlockScreenState extends State<_PinUnlockScreen> { _submitting = true; }); try { - final prefs = await SharedPreferences.getInstance(); - final existingOptIn = prefs.getBool(_sentryOptInKey) ?? false; + final existingOptIn = widget.preferences.sentryOptIn; if (_sentryOptIn && !existingOptIn) { - await prefs.setBool(_sentryOptInKey, true); + await widget.preferences.setSentryOptIn(true); } await widget.onUnlocked(pin); if (_sentryOptIn && !existingOptIn && mounted) { @@ -6345,11 +5899,10 @@ class _PinUnlockScreenState extends State<_PinUnlockScreen> { } Future _loadSentryOptIn() async { - final prefs = await SharedPreferences.getInstance(); + final existing = widget.preferences.sentryOptIn; if (!mounted) { return; } - final existing = prefs.getBool(_sentryOptInKey) ?? false; setState(() { _sentryOptIn = existing; _loadedSentryPref = true; @@ -6508,3 +6061,89 @@ class _SpeakingPill extends StatelessWidget { ); } } + +class _QuicStatsGraph extends StatelessWidget { + const _QuicStatsGraph({ + required this.label, + required this.data, + required this.color, + required this.unit, + }); + + final String label; + final List data; + final Color color; + final String unit; + + @override + Widget build(BuildContext context) { + if (data.isEmpty) return const SizedBox.shrink(); + final lastValue = data.last; + final theme = Theme.of(context); + return Tooltip( + message: '$label: $lastValue$unit', + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '$lastValue$unit', + style: theme.textTheme.labelSmall?.copyWith( + fontSize: 8, + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 1), + Container( + width: 36, + height: 12, + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outlineVariant, width: 0.5), + borderRadius: BorderRadius.circular(2), + ), + child: CustomPaint( + painter: _SparklinePainter(data: data, color: color), + ), + ), + ], + ), + ); + } +} + +class _SparklinePainter extends CustomPainter { + _SparklinePainter({required this.data, required this.color}); + + final List data; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + if (data.length < 2) return; + + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0; + + final maxVal = data.reduce((a, b) => a > b ? a : b).toDouble(); + final minVal = data.reduce((a, b) => a < b ? a : b).toDouble(); + final range = (maxVal - minVal).clamp(1.0, double.infinity); + + final path = Path(); + for (var i = 0; i < data.length; i++) { + final x = (i / (data.length - 1)) * size.width; + final y = size.height - ((data[i] - minVal) / range * size.height); + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _SparklinePainter oldDelegate) => + oldDelegate.data != data; +} diff --git a/lib/models/avatar_metadata.dart b/lib/models/avatar_metadata.dart index f9c7de4..167c3b1 100644 --- a/lib/models/avatar_metadata.dart +++ b/lib/models/avatar_metadata.dart @@ -6,11 +6,35 @@ class AvatarMetadata { required this.updatedAt, }); + /// Sentinel hash used to mark a JID as having no PEP user avatar + /// (typically: server replied with ``). + /// + /// Persisted via `StorageService` so we don't refetch on every restart. + /// See R3.3 in `doc/startup-fetch-review.md`. + static const String noPepAvatarHash = '__no_pep_avatar__'; + + /// Construct a sentinel marking [bareJid] as having no PEP avatar. + /// `bareJid` is unused inside the entry itself, but kept in the + /// signature for call-site readability. + factory AvatarMetadata.noPepAvatar({DateTime? updatedAt}) { + return AvatarMetadata( + hash: noPepAvatarHash, + mimeType: 'application/x-no-avatar', + bytes: -1, + updatedAt: updatedAt ?? DateTime.now(), + ); + } + final String hash; final String mimeType; final int bytes; final DateTime updatedAt; + /// True when this entry is the "no PEP avatar" sentinel produced by + /// [AvatarMetadata.noPepAvatar]. Callers should treat such entries as a + /// negative cache: skip both the metadata IQ and the data IQ. + bool get isNoPepAvatar => hash == noPepAvatarHash; + Map toMap() { return { 'hash': hash, @@ -28,9 +52,24 @@ class AvatarMetadata { final mimeType = map['mimeType']?.toString() ?? ''; final bytesRaw = map['bytes']; final updatedRaw = map['updatedAt']?.toString() ?? ''; - final bytes = bytesRaw is int ? bytesRaw : int.tryParse(bytesRaw?.toString() ?? '') ?? 0; + final bytes = bytesRaw is int + ? bytesRaw + : int.tryParse(bytesRaw?.toString() ?? '') ?? 0; final updatedAt = DateTime.tryParse(updatedRaw); - if (hash.isEmpty || mimeType.isEmpty || bytes == 0 || updatedAt == null) { + if (updatedAt == null) { + return null; + } + // Sentinel entry: round-trip even though bytes==-1 / mimeType is the + // sentinel value. See [AvatarMetadata.noPepAvatar]. + if (hash == noPepAvatarHash) { + return AvatarMetadata( + hash: hash, + mimeType: mimeType, + bytes: bytes, + updatedAt: updatedAt, + ); + } + if (hash.isEmpty || mimeType.isEmpty || bytes == 0) { return null; } return AvatarMetadata( diff --git a/lib/models/chat_message.dart b/lib/models/chat_message.dart index d758046..1d65599 100644 --- a/lib/models/chat_message.dart +++ b/lib/models/chat_message.dart @@ -61,6 +61,108 @@ class ChatMessage { final bool receiptReceived; final bool displayed; + // Sentinel object used to distinguish "caller passed null explicitly" from + // "caller did not pass this parameter" in copyWith. + static const Object _absent = Object(); + + ChatMessage copyWith({ + String? from, + String? to, + String? body, + DateTime? timestamp, + bool? outgoing, + Object? messageId = _absent, + Object? mamId = _absent, + Object? stanzaId = _absent, + Object? oobUrl = _absent, + Object? oobDescription = _absent, + Object? rawXml = _absent, + Object? inviteRoomJid = _absent, + Object? inviteReason = _absent, + Object? invitePassword = _absent, + Object? fileTransferId = _absent, + Object? fileName = _absent, + Object? fileSize = _absent, + Object? fileMime = _absent, + Object? fileBytes = _absent, + Object? fileState = _absent, + bool? edited, + Object? editedAt = _absent, + Object? reactions = _absent, + Object? replyToId = _absent, + Object? replyToJid = _absent, + Object? replyFallback = _absent, + bool? acked, + bool? receiptReceived, + bool? displayed, + }) { + return ChatMessage( + from: from ?? this.from, + to: to ?? this.to, + body: body ?? this.body, + timestamp: timestamp ?? this.timestamp, + outgoing: outgoing ?? this.outgoing, + messageId: identical(messageId, _absent) + ? this.messageId + : messageId as String?, + mamId: identical(mamId, _absent) ? this.mamId : mamId as String?, + stanzaId: identical(stanzaId, _absent) + ? this.stanzaId + : stanzaId as String?, + oobUrl: identical(oobUrl, _absent) ? this.oobUrl : oobUrl as String?, + oobDescription: identical(oobDescription, _absent) + ? this.oobDescription + : oobDescription as String?, + rawXml: identical(rawXml, _absent) ? this.rawXml : rawXml as String?, + inviteRoomJid: identical(inviteRoomJid, _absent) + ? this.inviteRoomJid + : inviteRoomJid as String?, + inviteReason: identical(inviteReason, _absent) + ? this.inviteReason + : inviteReason as String?, + invitePassword: identical(invitePassword, _absent) + ? this.invitePassword + : invitePassword as String?, + fileTransferId: identical(fileTransferId, _absent) + ? this.fileTransferId + : fileTransferId as String?, + fileName: identical(fileName, _absent) + ? this.fileName + : fileName as String?, + fileSize: identical(fileSize, _absent) + ? this.fileSize + : fileSize as int?, + fileMime: identical(fileMime, _absent) + ? this.fileMime + : fileMime as String?, + fileBytes: identical(fileBytes, _absent) + ? this.fileBytes + : fileBytes as int?, + fileState: identical(fileState, _absent) + ? this.fileState + : fileState as String?, + edited: edited ?? this.edited, + editedAt: identical(editedAt, _absent) + ? this.editedAt + : editedAt as DateTime?, + reactions: identical(reactions, _absent) + ? this.reactions + : reactions as Map>?, + replyToId: identical(replyToId, _absent) + ? this.replyToId + : replyToId as String?, + replyToJid: identical(replyToJid, _absent) + ? this.replyToJid + : replyToJid as String?, + replyFallback: identical(replyFallback, _absent) + ? this.replyFallback + : replyFallback as String?, + acked: acked ?? this.acked, + receiptReceived: receiptReceived ?? this.receiptReceived, + displayed: displayed ?? this.displayed, + ); + } + Map toMap() { return { 'from': from, diff --git a/lib/pep/pep_caps_manager.dart b/lib/pep/pep_caps_manager.dart index b7a3827..05506c1 100644 --- a/lib/pep/pep_caps_manager.dart +++ b/lib/pep/pep_caps_manager.dart @@ -1,15 +1,28 @@ import 'package:xmpp_stone/xmpp_stone.dart'; +import '../storage/storage_service.dart'; import 'pep_manager.dart'; class PepCapsManager { PepCapsManager({ required this.connection, required this.pepManager, - }); + StorageService? storage, + }) : _storage = storage { + // R5: seed the in-memory cache from disk so MUC presence storms at + // connect time don't trigger a `disco#info` fan-out for caps we + // already verified in a previous session. + final cached = storage?.loadEntityCaps(); + if (cached != null) { + for (final entry in cached.entries) { + _capsFeatures[entry.key] = Set.from(entry.value); + } + } + } final Connection connection; final PepManager pepManager; + final StorageService? _storage; final Map> _capsFeatures = {}; final Map> _capsKeyBareJids = {}; @@ -107,6 +120,9 @@ class PepCapsManager { } } _capsFeatures[pending.capsKey] = features; + // R5: persist the verified caps so we can avoid the disco#info IQ + // for this `node#ver` on the next session. + _storage?.storeEntityCaps(pending.capsKey, features); _recordFeaturesForBareJid(pending.bareJid, features); final linkedBareJids = _capsKeyBareJids[pending.capsKey]; if (linkedBareJids != null) { diff --git a/lib/pep/pep_manager.dart b/lib/pep/pep_manager.dart index 74859e7..506a3bc 100644 --- a/lib/pep/pep_manager.dart +++ b/lib/pep/pep_manager.dart @@ -17,6 +17,11 @@ class PepManager { }) : _onUpdate = onUpdate { _metadataByJid.addAll(storage.loadAvatarMetadata()); _avatarBlobs.addAll(storage.loadAvatarBlobs()); + // R3.1: drop any cached blobs whose hash isn't referenced by any + // current metadata entry. This keeps the on-disk avatar store from + // growing unbounded over the lifetime of the app as contacts rotate + // their avatars. + gcUnreferencedAvatarBlobs(); } final Connection connection; @@ -27,12 +32,19 @@ class PepManager { final Map _metadataByJid = {}; final Map _avatarBlobs = {}; final Map _pendingDataRequests = {}; + // R3.3: track outstanding `urn:xmpp:avatar:metadata` GET IQs so we can + // observe `` responses (e.g. `item-not-found`) and persist a + // negative-cache sentinel for that JID. + final Map _pendingMetadataRequests = {}; Uint8List? avatarBytesFor(String bareJid) { final meta = _metadataByJid[bareJid]; if (meta == null) { return null; } + if (meta.isNoPepAvatar) { + return null; + } final base64Data = _avatarBlobs[meta.hash]; if (base64Data == null) { return null; @@ -49,7 +61,16 @@ class PepManager { } String? avatarHashFor(String bareJid) { - return _metadataByJid[bareJid]?.hash; + final meta = _metadataByJid[bareJid]; + if (meta == null || meta.isNoPepAvatar) { + return null; + } + return meta.hash; + } + + /// True iff we have a persisted negative-cache marker for this JID. R3.3. + bool isKnownToHaveNoPepAvatar(String bareJid) { + return _metadataByJid[bareJid]?.isNoPepAvatar ?? false; } void subscribeToAvatarMetadata(String bareJid) { @@ -95,9 +116,42 @@ class PepManager { void clearCache() { _metadataByJid.clear(); _avatarBlobs.clear(); + _pendingDataRequests.clear(); + _pendingMetadataRequests.clear(); _onUpdate(); } + /// R3.1: drop any cached avatar blob whose hash is not referenced by a + /// current (non-sentinel) `_metadataByJid` entry. Keeps `_avatarBlobs` + /// and the persisted blob store from leaking when contacts rotate + /// avatars over the lifetime of the install. + /// + /// Returns the number of blobs evicted (helpful for tests and a future + /// telemetry hook). Calling this is safe at any time; in particular it + /// is invoked once from the constructor right after the on-disk seeds + /// are loaded. + int gcUnreferencedAvatarBlobs() { + if (_avatarBlobs.isEmpty) { + return 0; + } + final referenced = { + for (final meta in _metadataByJid.values) + if (!meta.isNoPepAvatar && meta.hash.isNotEmpty) meta.hash, + }; + final toRemove = [ + for (final hash in _avatarBlobs.keys) + if (!referenced.contains(hash)) hash, + ]; + if (toRemove.isEmpty) { + return 0; + } + for (final hash in toRemove) { + _avatarBlobs.remove(hash); + } + storage.replaceAvatarBlobs(_avatarBlobs); + return toRemove.length; + } + void _sendSubscribe(String bareJid) { final id = AbstractStanza.getRandomId(); final iqStanza = IqStanza(id, IqStanzaType.SET); @@ -123,6 +177,9 @@ class PepManager { items.addAttribute(XmppAttribute('max_items', '1')); pubsub.addChild(items); iqStanza.addChild(pubsub); + // R3.3: remember which JID this IQ asked about so we can persist a + // negative-cache sentinel if the response is an error. + _pendingMetadataRequests[id] = bareJid; connection.writeStanza(iqStanza); } @@ -181,6 +238,29 @@ class PepManager { } void _handleIqResult(IqStanza stanza) { + // R3.3: handle metadata-IQ responses first. A non-result (typically + // ``) means the JID has no PEP + // avatar; record a negative-cache sentinel that survives restarts. + final pendingMetadata = _pendingMetadataRequests.remove(stanza.id); + if (pendingMetadata != null) { + if (stanza.type != IqStanzaType.RESULT) { + // Don't overwrite a real metadata entry that may have arrived via a + // PubSub event in between. Only stamp the sentinel when we have no + // entry yet for this JID. + if (!_metadataByJid.containsKey(pendingMetadata)) { + final sentinel = AvatarMetadata.noPepAvatar(); + _metadataByJid[pendingMetadata] = sentinel; + storage.storeAvatarMetadata(pendingMetadata, sentinel); + _onUpdate(); + } + } + // The actual metadata payload (when it arrives via IQ result) is + // delivered as a PubSub items result and processed through the same + // path as event messages. We treat the IQ-result case as a no-op + // because xmpp_stone surfaces the items separately as a PubSub event + // when retrieving via ``. + return; + } final pending = _pendingDataRequests.remove(stanza.id); if (pending == null) { return; diff --git a/lib/storage/account_record.dart b/lib/storage/account_record.dart index b3d1992..ac265c3 100644 --- a/lib/storage/account_record.dart +++ b/lib/storage/account_record.dart @@ -10,6 +10,8 @@ class AccountRecord { required this.directTls, required this.wsEndpoint, required this.wsProtocols, + this.useQuic = true, + this.useTcp = true, }); final String jid; @@ -22,6 +24,13 @@ class AccountRecord { final bool directTls; final String wsEndpoint; final List wsProtocols; + /// Whether to attempt QUIC transport (XEP-0467) when available. + final bool useQuic; + + /// Whether to attempt plain TCP (`_xmpp-client._tcp` SRV records and the + /// non-TLS fallback). When false, plain-TCP SRV records are ignored and + /// no plain-TCP fallback connection will be attempted. + final bool useTcp; Map toMap() { return { @@ -35,6 +44,8 @@ class AccountRecord { 'directTls': directTls, 'wsEndpoint': wsEndpoint, 'wsProtocols': wsProtocols, + 'useQuic': useQuic, + 'useTcp': useTcp, }; } @@ -51,6 +62,8 @@ class AccountRecord { final useWebSocketRaw = map['useWebSocket']; final directTlsRaw = map['directTls']; final wsEndpoint = map['wsEndpoint']?.toString() ?? ''; + final useQuicRaw = map['useQuic']; + final useTcpRaw = map['useTcp']; final wsProtocolsRaw = map['wsProtocols']; final port = portRaw is int ? portRaw : int.tryParse(portRaw?.toString() ?? '') ?? 5222; if (jid.isEmpty) { @@ -63,6 +76,8 @@ class AccountRecord { ? useWebSocketRaw : wsEndpoint.isNotEmpty; final directTls = directTlsRaw is bool ? directTlsRaw : false; + final useQuic = useQuicRaw is bool ? useQuicRaw : true; + final useTcp = useTcpRaw is bool ? useTcpRaw : true; final wsProtocols = []; if (wsProtocolsRaw is List) { for (final entry in wsProtocolsRaw) { @@ -83,6 +98,8 @@ class AccountRecord { directTls: directTls, wsEndpoint: wsEndpoint, wsProtocols: wsProtocols, + useQuic: useQuic, + useTcp: useTcp, ); } } diff --git a/lib/storage/preferences_service.dart b/lib/storage/preferences_service.dart new file mode 100644 index 0000000..9e8404c --- /dev/null +++ b/lib/storage/preferences_service.dart @@ -0,0 +1,58 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Centralises all [SharedPreferences] access for Wimsy. +/// +/// Construct once at app startup via [PreferencesService.load] and pass the +/// instance down to widgets that need it. All preference key strings live +/// here so there is a single source of truth. +class PreferencesService { + PreferencesService._(this._prefs); + + /// Loads [SharedPreferences] and returns a ready-to-use [PreferencesService]. + static Future load() async { + final prefs = await SharedPreferences.getInstance(); + return PreferencesService._(prefs); + } + + final SharedPreferences _prefs; + + // ── Key constants ────────────────────────────────────────────────────────── + + static const _sentryOptInKey = 'sentry_opt_in'; + static const _pinIgnoredKey = 'wimsy_pin_ignored'; + static const _lastJidKey = 'wimsy_last_jid'; + static const _audioInputKey = 'wimsy_audio_input'; + static const _videoInputKey = 'wimsy_video_input'; + + // ── Sentry opt-in ────────────────────────────────────────────────────────── + + bool get sentryOptIn => _prefs.getBool(_sentryOptInKey) ?? false; + + Future setSentryOptIn(bool value) => + _prefs.setBool(_sentryOptInKey, value); + + // ── PIN screen ───────────────────────────────────────────────────────────── + + bool get pinIgnored => _prefs.getBool(_pinIgnoredKey) ?? false; + + Future setPinIgnored(bool value) => + _prefs.setBool(_pinIgnoredKey, value); + + // ── Last signed-in JID ───────────────────────────────────────────────────── + + String? get lastJid => _prefs.getString(_lastJidKey); + + Future setLastJid(String jid) => _prefs.setString(_lastJidKey, jid); + + // ── Media device preferences ─────────────────────────────────────────────── + + String? get audioInputId => _prefs.getString(_audioInputKey); + + Future setAudioInputId(String deviceId) => + _prefs.setString(_audioInputKey, deviceId); + + String? get videoInputId => _prefs.getString(_videoInputKey); + + Future setVideoInputId(String deviceId) => + _prefs.setString(_videoInputKey, deviceId); +} diff --git a/lib/storage/storage_service.dart b/lib/storage/storage_service.dart index 61ace26..17f0a6a 100644 --- a/lib/storage/storage_service.dart +++ b/lib/storage/storage_service.dart @@ -24,6 +24,20 @@ class StorageService { static const _vcardAvatarStateKey = 'vcard_avatar_state'; static const _bookmarksKey = 'bookmarks'; static const _displayedSyncKey = 'displayed_sync'; + // R1.3: pending (chatJid -> stanza-id) markers we received via MDS but + // could not yet match against any local message. These are resolved + // lazily as messages with the matching stanza-id arrive (live or via + // MAM) and are removed from disk once resolved. + static const _displayedSyncPendingKey = 'displayed_sync_pending'; + // R5: persisted Entity Capabilities (XEP-0115) cache, keyed by + // `node#ver`. Values are the disco#info feature lists. + static const _entityCapsKey = 'entity_caps'; + // R2.1: single global anchor for the newest MAM id we've ingested + // across all chats. The intent is to enable a unified MAM catch-up + // query (`...`) that + // replaces the per-chat fan-out at connect time. We persist the + // anchor today; the unified query is a follow-up. + static const _lastMamIdSeenKey = 'last_mam_id_seen'; static const int _maxCachedMessageBytes = 20 * 1024 * 1024; final SecureStore _secureStorage = createSecureStore(); @@ -346,6 +360,9 @@ class StorageService { return; } await box.put(_displayedSyncKey, const {}); + // R1.3: also wipe the pending-resolution map so disconnect/forget + // genuinely clears all MDS-related state. + await box.put(_displayedSyncPendingKey, const {}); } Future clearRoomMessages() async { @@ -405,6 +422,159 @@ class StorageService { await box.put(_displayedSyncKey, Map.from(sync)); } + /// R1.3: load pending displayed-sync markers — i.e. (chatJid -> stanzaId) + /// pairs we received from the MDS node but could not yet match against + /// any locally cached message. The XmppService resolves these as + /// matching messages arrive (live, via MAM, or via Carbons). + Map loadDisplayedSyncPending() { + final box = _box; + if (box == null) { + return const {}; + } + final data = box.get( + _displayedSyncPendingKey, + defaultValue: const {}, + ); + if (data is Map) { + final result = {}; + for (final entry in data.entries) { + final key = entry.key.toString(); + final value = entry.value?.toString() ?? ''; + if (key.isNotEmpty && value.isNotEmpty) { + result[key] = value; + } + } + return result; + } + return const {}; + } + + /// R1.3: persist the pending-resolution map. + Future storeDisplayedSyncPending(Map pending) async { + final box = _box; + if (box == null) { + return; + } + await box.put( + _displayedSyncPendingKey, + Map.from(pending), + ); + } + + /// R5: load the persisted Entity Capabilities (XEP-0115) cache. The + /// returned map is keyed by `node#ver` and the values are the verified + /// `disco#info` feature lists. + /// + /// We persist this cache to skip the per-`node#ver` `disco#info` IQ + /// fan-out at connect time when MUC presence broadcasts caps for many + /// occupants. If the cache returns nothing for a given `node#ver` the + /// existing online query path still runs. + Map> loadEntityCaps() { + final box = _box; + if (box == null) { + return const {}; + } + final data = box.get( + _entityCapsKey, + defaultValue: const {}, + ); + if (data is Map) { + final result = >{}; + for (final entry in data.entries) { + final key = entry.key.toString(); + if (key.isEmpty) { + continue; + } + final value = entry.value; + if (value is List) { + final features = { + for (final feature in value) + if (feature is String && feature.isNotEmpty) feature, + }; + if (features.isNotEmpty) { + result[key] = features; + } + } + } + return result; + } + return const {}; + } + + /// R5: persist a single `node#ver` -> features mapping. Existing entries + /// for other `node#ver` keys are preserved. We never overwrite an + /// existing entry — Entity Capabilities are content-addressed by hash so + /// a stable key always corresponds to the same feature set. + Future storeEntityCaps(String capsKey, Set features) async { + final box = _box; + if (box == null) { + return; + } + if (capsKey.isEmpty || features.isEmpty) { + return; + } + final existing = box.get(_entityCapsKey, defaultValue: {}); + final next = {}; + if (existing is Map) { + for (final entry in existing.entries) { + next[entry.key.toString()] = entry.value; + } + } + if (next.containsKey(capsKey)) { + return; + } + next[capsKey] = features.toList(growable: false); + await box.put(_entityCapsKey, next); + } + + /// R5: clear the entire caps cache (e.g. for "forget account" flows). + Future clearEntityCaps() async { + final box = _box; + if (box == null) { + return; + } + await box.put(_entityCapsKey, const {}); + } + + /// R2.1: load the persisted "newest MAM id we've seen" anchor across + /// all chats. Returns null when there is no recorded anchor. + String? loadLastMamIdSeen() { + final box = _box; + if (box == null) { + return null; + } + final value = box.get(_lastMamIdSeenKey)?.toString(); + if (value == null || value.isEmpty) { + return null; + } + return value; + } + + /// R2.1: persist the global anchor. Callers should only call this with + /// an id that is genuinely newer than the previous anchor; the helper + /// performs no ordering check (MAM ids are server-assigned and only + /// totally ordered within a single MAM archive, so the value passed + /// here must come from the caller's append path). + Future storeLastMamIdSeen(String mamId) async { + final box = _box; + if (box == null) { + return; + } + if (mamId.isEmpty) { + return; + } + await box.put(_lastMamIdSeenKey, mamId); + } + + /// R2.1: clear the anchor (forget-account flows). + Future clearLastMamIdSeen() async { + final box = _box; + if (box == null) { + return; + } + await box.delete(_lastMamIdSeenKey); + } + Future storeAvatarMetadata(String bareJid, AvatarMetadata metadata) async { final box = _box; if (box == null) { @@ -449,6 +619,17 @@ class StorageService { await box.put(_avatarBlobsKey, next); } + /// R3.1: replace the entire avatar-blobs map with [blobs]. Used by the + /// PEP avatar GC pass that keeps only blobs referenced by a current + /// metadata entry. + Future replaceAvatarBlobs(Map blobs) async { + final box = _box; + if (box == null) { + return; + } + await box.put(_avatarBlobsKey, Map.from(blobs)); + } + Future clearAvatars() async { final box = _box; if (box == null) { diff --git a/lib/xmpp/chat_message_mutations.dart b/lib/xmpp/chat_message_mutations.dart index 25ffd72..300bb5c 100644 --- a/lib/xmpp/chat_message_mutations.dart +++ b/lib/xmpp/chat_message_mutations.dart @@ -45,36 +45,13 @@ class ChatMessageMutations { existing.editedAt == nextEditedAt) { return true; } - list[i] = ChatMessage( - from: existing.from, - to: existing.to, + list[i] = existing.copyWith( body: newBody, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, - mamId: existing.mamId, - stanzaId: existing.stanzaId, oobUrl: nextOobUrl, oobDescription: nextOobDescription, rawXml: nextRawXml, - inviteRoomJid: existing.inviteRoomJid, - inviteReason: existing.inviteReason, - invitePassword: existing.invitePassword, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, edited: true, editedAt: nextEditedAt, - reactions: existing.reactions ?? const {}, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, - acked: existing.acked, - receiptReceived: existing.receiptReceived, - displayed: existing.displayed, ); return true; } @@ -103,33 +80,8 @@ class ChatMessageMutations { if (_reactionsEqual(existing.reactions ?? const {}, nextReactions)) { return true; } - list[i] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, - mamId: existing.mamId, - stanzaId: existing.stanzaId, - oobUrl: existing.oobUrl, - oobDescription: existing.oobDescription, - rawXml: existing.rawXml, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, + list[i] = existing.copyWith( reactions: nextReactions, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, - acked: existing.acked, - receiptReceived: existing.receiptReceived, - displayed: existing.displayed, ); return true; } diff --git a/lib/xmpp/mam_merge_engine.dart b/lib/xmpp/mam_merge_engine.dart index 1221f27..f47070e 100644 --- a/lib/xmpp/mam_merge_engine.dart +++ b/lib/xmpp/mam_merge_engine.dart @@ -28,35 +28,13 @@ bool mergeMamIdsIntoExisting( (oobDescription != null && oobDescription.isNotEmpty) ? oobDescription : existing.oobDescription; - list[i] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, + list[i] = existing.copyWith( mamId: (mamId != null && mamId.isNotEmpty) ? mamId : existing.mamId, stanzaId: (stanzaId != null && stanzaId.isNotEmpty) ? stanzaId : existing.stanzaId, - oobUrl: existing.oobUrl, oobDescription: nextOobDescription, rawXml: nextRawXml, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, - acked: existing.acked, - receiptReceived: existing.receiptReceived, - displayed: existing.displayed, ); return true; } @@ -75,34 +53,11 @@ bool mergeMamIdsIntoExisting( (existing.stanzaId ?? '').isNotEmpty) { continue; } - list[i] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, + list[i] = existing.copyWith( mamId: (mamId != null && mamId.isNotEmpty) ? mamId : existing.mamId, stanzaId: (stanzaId != null && stanzaId.isNotEmpty) ? stanzaId : existing.stanzaId, - oobUrl: existing.oobUrl, - oobDescription: existing.oobDescription, - rawXml: existing.rawXml, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, - acked: existing.acked, - receiptReceived: existing.receiptReceived, - displayed: existing.displayed, ); return true; } diff --git a/lib/xmpp/quic_endpoint_plan.dart b/lib/xmpp/quic_endpoint_plan.dart new file mode 100644 index 0000000..1991a35 --- /dev/null +++ b/lib/xmpp/quic_endpoint_plan.dart @@ -0,0 +1,21 @@ +import 'package:xmpp_stone/xmpp_stone.dart'; + +import 'srv_target.dart'; + +List buildQuicEndpointPlan({ + required String domain, + List srvCandidates = const [], +}) { + if (srvCandidates.isEmpty) { + return const []; + } + return srvCandidates + .map( + (candidate) => XmppQuicEndpoint( + host: candidate.host, + port: candidate.port, + tlsHost: domain, + ), + ) + .toList(); +} diff --git a/lib/xmpp/quic_xmpp_socket.dart b/lib/xmpp/quic_xmpp_socket.dart new file mode 100644 index 0000000..c51d19d --- /dev/null +++ b/lib/xmpp/quic_xmpp_socket.dart @@ -0,0 +1,1323 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +// ignore_for_file: implementation_imports + +import 'package:flutter_quic/flutter_quic.dart'; +import 'package:xmpp_stone/src/connection/XmppWebsocketIo.dart'; +import 'package:xmpp_stone/src/logger/Log.dart'; + +class QuicCapableXmppSocket extends XmppWebSocket { + QuicCapableXmppSocket({ + this.quicConnectTimeout = const Duration(seconds: 5), + this.happyEyeballsDelay = const Duration(milliseconds: 250), + this.quicConnectMaxAttempts = 3, + }); + + final Duration quicConnectTimeout; + final Duration happyEyeballsDelay; + + /// How many times to retry the full Happy Eyeballs round (across all + /// candidate addresses) before giving up. Each retry races every + /// candidate again with a fresh staggered start. + final int quicConnectMaxAttempts; + final XmppWebSocketIo _fallbackSocket = XmppWebSocketIo(); + final StreamController _quicStreamController = + StreamController.broadcast(); + + static Future? _rustInitFuture; + static bool _rustInitialized = false; + static const int _auxStreamSlots = 20; + static const int _maxControlBufferChars = 16 * 1024; + + bool _useQuic = false; + bool _closed = false; + bool _postBindReady = false; + // Set to true when connectionOpenBi times out (peer has not granted bidi + // stream credits). Cleared when we detect the server has sent a new + // MAX_STREAMS frame (frameRx.maxStreamsBidi increases in connectionStats). + bool _auxStreamsBlocked = false; + // The frameRx.maxStreamsBidi count the last time we checked stats. Used to + // detect when the server sends a new MAX_STREAMS(bidi) frame. + BigInt _lastMaxStreamsBidiFrameCount = BigInt.zero; + Timer? _maxStreamsWatchTimer; + String? _accountBareJid; + String _controlBuffer = ''; + late String Function(String event) _map; + // Factory that produces an independent mapper closure (with its own buffer) + // for each aux QUIC stream. Set by Connection after connect() via + // setAuxMapperFactory(). Each call returns a new closure with its own + // partial-XML buffer, so interleaved chunks from different QUIC streams + // do not corrupt each other's parse state. + String Function(String) Function()? _makeAuxMapper; + + /// Called by [Connection] after a successful QUIC connect to supply a + /// factory for per-aux-stream XML mappers. Each aux recv loop calls this + /// factory once to get its own independent buffer/mapper closure. + void setAuxMapperFactory(String Function(String) Function() factory) { + _makeAuxMapper = factory; + } + + Future _writeQueue = Future.value(); + + // Kept as members so the winning endpoint/connection remain alive while + // stream objects are in use. + // ignore: unused_field + QuicEndpoint? _endpoint; + // ignore: unused_field + QuicConnection? _connection; + QuicSendStream? _sendStream; + QuicRecvStream? _recvStream; + final Map _auxStreamsBySlot = {}; + // In-flight futures for aux stream opens, keyed by slot. Concurrent callers + // for the same slot await the same future rather than racing on _connection. + final Map> _auxStreamOpening = {}; + // Global serialisation lock for ALL FFI calls that consume _connection via + // Auto_Owned transfer (connectionOpenBi, connectionAcceptBi, connectionStats, + // connectionPeerTransportParams, connectionCloseReason). Only one such call + // may be in-flight at a time; concurrent callers would both read the same + // arc and the second would find it already disposed. + Future _auxOpenLock = Future.value(); + + // Per-slot queue of stanzas that arrived while the aux stream for that slot + // was still being opened. Flushed onto the aux stream once it is ready. + final Map> _auxStreamPendingQueue = {}; + + // Pool of server-initiated bidirectional streams that have been accepted but + // not yet assigned to a slot. When we need to open an aux stream for a new + // bare-JID slot, we pop from this pool first rather than calling + // connectionOpenBi, saving a round-trip and a stream-credit. + final List<_QuicStreamChannel> _serverStreamPool = []; + + /// Number of server-initiated streams currently waiting in the pool. + /// Exposed for testing only. + @visibleForTesting + int get serverStreamPoolSize => _serverStreamPool.length; + + /// Injects a fake server-initiated stream into the pool for testing. + /// The [sendStream] and [recvStream] are the raw QUIC stream objects. + /// In production these come from [connectionAcceptBi]; in tests a fake + /// pair can be injected to verify pool-preference logic without a live + /// QUIC connection. + @visibleForTesting + void injectServerStreamForTesting( + QuicSendStream sendStream, + QuicRecvStream recvStream, + ) { + _serverStreamPool.add( + _QuicStreamChannel(sendStream: sendStream, recvStream: recvStream), + ); + } + + @override + Future connect( + String host, + int port, { + String Function(String event)? map, + List? wsProtocols, + String? wsPath, + Uri? wsUri, + bool useWebSocket = false, + bool useQuic = false, + bool directTls = false, + String? tlsHost, + }) async { + _map = map ?? (element) => element; + // If the caller supplied a map factory (Connection.makeStreamResponseMapper), + // store it so aux recv loops can each get their own independent buffer. + _makeAuxMapper = null; // reset; Connection sets this via makeStreamResponseMapper + _closed = false; + if (!useQuic) { + _useQuic = false; + return _fallbackSocket.connect( + host, + port, + map: map, + wsProtocols: wsProtocols, + wsPath: wsPath, + wsUri: wsUri, + useWebSocket: useWebSocket, + useQuic: false, + directTls: directTls, + tlsHost: tlsHost, + ); + } + + _useQuic = true; + await _ensureRustInitialized(); + await _connectQuic(host, port, tlsHost ?? host); + _startControlRecvLoop(); + _startServerStreamAcceptLoop(); + return this; + } + + @override + bool get isQuic => _useQuic; + + @override + Future getQuicStats() async { + final conn = _connection; + if (conn == null || !_useQuic) return null; + try { + final (updatedConn, stats) = await connectionStats(connection: conn); + _connection = updatedConn; + return stats; + } catch (e) { + // debugPrint('Error getting QUIC stats: $e'); + return null; + } + } + + @override + void write(Object? message) { + if (!_useQuic) { + _fallbackSocket.write(message); + return; + } + final payload = message?.toString() ?? ''; + if (payload.isEmpty) { + return; + } + _writeQueue = _writeQueue.then((_) async { + if (_closed) { + return; + } + try { + final target = await _selectSendTarget(payload); + if (target == null) { + // Payload has been enqueued for the aux stream; nothing to send now. + return; + } + Log.xmppp_sending(payload, channel: target.label); + final updated = await sendStreamWriteAll( + stream: target.stream, + data: utf8.encode(payload), + ); + target.update(updated); + } catch (error, stackTrace) { + if (!_quicStreamController.isClosed) { + _quicStreamController.addError(error, stackTrace); + } + } + }); + } + + @override + void close() { + _closed = true; + if (!_useQuic) { + _fallbackSocket.close(); + return; + } + + final controlStream = _sendStream; + final auxSlots = _auxStreamsBySlot.keys.toList(growable: false); + final auxStreams = _auxStreamsBySlot.values + .map((channel) => channel.sendStream) + .toList(growable: false); + if (auxSlots.isNotEmpty) { + debugPrint('QUIC closing ${auxSlots.length} aux stream(s): slots=$auxSlots'); + } + + _sendStream = null; + _recvStream = null; + // Do NOT null _connection here: any in-flight connectionOpenBi call holds + // a local reference to the same RustArc. Nulling it here drops the last + // Dart-side strong reference and disposes the arc while the FFI call is + // still encoding its arguments, causing DroppableDisposedException. + // The connection will be released naturally once all local references drop. + _endpoint = null; + _postBindReady = false; + _auxStreamsBlocked = false; + _lastMaxStreamsBidiFrameCount = BigInt.zero; + _maxStreamsWatchTimer?.cancel(); + _maxStreamsWatchTimer = null; + _accountBareJid = null; + _controlBuffer = ''; + _auxStreamsBySlot.clear(); + _auxStreamOpening.clear(); + _auxStreamPendingQueue.clear(); + _serverStreamPool.clear(); + _auxOpenLock = Future.value(); + + if (controlStream != null) { + unawaited( + Future(() async { + try { + await sendStreamFinish(stream: controlStream); + } catch (_) { + // ignore close races + } + }), + ); + } + + for (final auxStream in auxStreams) { + unawaited( + Future(() async { + try { + await sendStreamFinish(stream: auxStream); + } catch (_) { + // ignore close races + } + }), + ); + } + + if (!_quicStreamController.isClosed) { + unawaited(_quicStreamController.close()); + } + } + + @override + Future secure({ + host, + SecurityContext? context, + bool Function(X509Certificate certificate)? onBadCertificate, + List? supportedProtocols, + }) { + if (_useQuic) { + return Future.value(null); + } + return _fallbackSocket.secure( + host: host, + context: context, + onBadCertificate: onBadCertificate, + supportedProtocols: supportedProtocols, + ); + } + + @override + String getStreamOpeningElement(String domain) { + return _fallbackSocket.getStreamOpeningElement(domain); + } + + @override + StreamSubscription listen( + void Function(String event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + if (!_useQuic) { + return _fallbackSocket.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + return _quicStreamController.stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + Future _connectQuic(String host, int port, String serverName) async { + final addresses = await InternetAddress.lookup(host); + if (addresses.isEmpty) { + throw SocketException('No addresses found for QUIC host $host'); + } + final candidates = buildQuicHappyEyeballsPlan(addresses); + + final maxAttempts = quicConnectMaxAttempts < 1 ? 1 : quicConnectMaxAttempts; + Object? lastError; + for (var attempt = 1; attempt <= maxAttempts; attempt++) { + debugPrint( + 'QUIC connect round $attempt/$maxAttempts: ' + 'racing ${candidates.length} candidate(s) ' + 'host=$host port=$port ' + 'stagger=${happyEyeballsDelay.inMilliseconds}ms ' + 'perAttemptTimeout=${quicConnectTimeout.inMilliseconds}ms', + ); + try { + final connected = await _raceQuicCandidates( + candidates, + port, + serverName, + ); + final winnerAddress = connected.address; + debugPrint( + 'QUIC connect winner: ' + '${winnerAddress?.address ?? '?'}:$port ' + 'type=${winnerAddress?.type ?? '?'} ' + '(round $attempt/$maxAttempts)', + ); + _endpoint = connected.endpoint; + _connection = connected.connection; + _sendStream = connected.sendStream; + _recvStream = connected.recvStream; + // Log initial connection stats so we can see the server's stream-credit + // situation immediately after the handshake. Must be awaited: the call + // consumes _connection via Auto_Owned and stores the replacement arc + // back into _connection; without await the arc would be stale for any + // subsequent FFI call (e.g. _startAuxStreamsOpen). + await _logConnectionStats('post-connect'); + // Log the peer's advertised transport parameters so we know up front + // how many bidi streams the server will allow us to open before it + // needs to send MAX_STREAMS. If the peer advertised <= 1 (just enough + // for the control stream) there is no point attempting aux opens: + // `open_bi()` would block indefinitely waiting for credit. + await _logPeerTransportParams('post-connect'); + return; + } catch (error) { + lastError = error; + debugPrint( + 'QUIC connect round $attempt/$maxAttempts failed: $error', + ); + } + } + throw Exception( + 'Failed QUIC connect host=$host port=$port ' + 'after $maxAttempts attempt(s) error=$lastError', + ); + } + + /// Races every candidate address in parallel using a Happy Eyeballs style + /// staggered start: candidate `i` is launched after `i * happyEyeballsDelay`. + /// Each individual attempt is bounded by [quicConnectTimeout]. The first + /// attempt to succeed wins; the rest are abandoned (their Futures are + /// drained in the background to avoid unhandled-error noise). + /// + /// Throws if every candidate fails or times out. + Future<_QuicConnectResult> _raceQuicCandidates( + List candidates, + int port, + String serverName, + ) async { + if (candidates.isEmpty) { + throw SocketException('No QUIC candidates to attempt'); + } + final completer = Completer<_QuicConnectResult>(); + final errors = []; + var pending = candidates.length; + final timers = []; + + void recordFailure(InternetAddress address, Object error) { + errors.add('${address.address}/${address.type.name}: $error'); + pending--; + if (pending == 0 && !completer.isCompleted) { + completer.completeError( + Exception( + 'All ${candidates.length} QUIC candidate(s) failed: ' + '${errors.join('; ')}', + ), + ); + } + } + + void launch(InternetAddress address) { + if (completer.isCompleted) { + // Another candidate already won; do not start more attempts. + pending--; + return; + } + debugPrint( + 'QUIC connect attempt: ${address.address}:$port ' + 'type=${address.type} timeout=${quicConnectTimeout.inMilliseconds}ms', + ); + unawaited( + _connectQuicAddress( + address, + port, + serverName, + timeout: quicConnectTimeout, + ).then((result) { + if (completer.isCompleted) { + // We lost the race: discard this connection. Best-effort; we + // don't have a clean cancel path through the FFI, so just let + // the Rust side drop it when the arc goes out of scope. + debugPrint( + 'QUIC connect discard (lost race): ' + '${address.address}:$port type=${address.type}', + ); + return; + } + completer.complete( + _QuicConnectResult( + address: address, + endpoint: result.endpoint, + connection: result.connection, + sendStream: result.sendStream, + recvStream: result.recvStream, + ), + ); + }).catchError((Object error) { + debugPrint( + 'QUIC connect failed: ${address.address}:$port ' + 'type=${address.type} error=$error', + ); + if (!completer.isCompleted) { + recordFailure(address, error); + } + }), + ); + } + + for (var i = 0; i < candidates.length; i++) { + final address = candidates[i]; + if (i == 0) { + launch(address); + } else { + final timer = Timer(happyEyeballsDelay * i, () => launch(address)); + timers.add(timer); + } + } + + try { + return await completer.future; + } finally { + for (final timer in timers) { + timer.cancel(); + } + } + } + + Future<_QuicConnectResult> _connectQuicAddress( + InternetAddress address, + int port, + String serverName, { + required Duration timeout, + }) async { + final endpoint = await createClientEndpoint(); + // Build a per-connection qlog path so each QUIC session gets its own + // trace file. The file is written by Quinn in qlog JSON-SEQ format and + // can be analysed with qvis (https://qvis.quictools.info/) or Wireshark. + final qlogPath = + '/tmp/wimsy_quic_${DateTime.now().millisecondsSinceEpoch}.qlog'; + debugPrint('QUIC qlog: writing trace to $qlogPath'); + final connect = endpointConnect( + endpoint: endpoint, + addr: _formatSocketAddress(address, port), + serverName: serverName, + qlogPath: qlogPath, + ); + final connected = await connect.timeout(timeout); + final (conn, sendStream, recvStream) = await connectionOpenBi(connection: connected.$2); + return _QuicConnectResult( + endpoint: connected.$1, + connection: conn, + sendStream: sendStream, + recvStream: recvStream, + ); + } + + /// Accepts server-initiated bidirectional QUIC streams and routes their + /// data into [_quicStreamController] exactly like client-initiated aux + /// streams. Per XEP-0467 §Multiple Streams the server may open streams to + /// push large responses (e.g. MAM pages) without waiting for the client to + /// open a stream first. Each accepted stream gets its own independent XML + /// mapper so partial-chunk buffering does not corrupt other streams. + /// + /// Each `connectionAcceptBi` call uses a shared reference to `_connection` + /// (not Auto_Owned), so it does NOT consume the arc and can run concurrently + /// with `connectionOpenBi` calls without holding [_auxOpenLock]. + void _startServerStreamAcceptLoop() { + final conn = _connection; + if (conn == null) return; + var streamIndex = 0; + unawaited( + Future(() async { + while (!_closed) { + final connection = _connection; + if (connection == null) break; + try { + // connectionAcceptBi takes &QuicConnection (shared ref), so it + // does not consume the arc and is safe to call concurrently with + // connectionOpenBi / connectionStats. + final result = await connectionAcceptBi(connection: connection); + final recvStream = result.$2; + if (recvStream == null) { + // Connection closed — exit the accept loop. + debugPrint('QUIC server-stream accept loop: connection closed'); + break; + } + final sendStream = result.$1; + final label = 'quic-server-${streamIndex++}'; + debugPrint( + 'QUIC accepted server-initiated stream: $label ' + '(recv loop started immediately; send stream pooled for reuse)', + ); + // Start the recv loop immediately so inbound stanzas pushed by + // the server on this stream are processed right away, regardless + // of whether the send side is ever assigned to a slot. + final mapper = _makeAuxMapper?.call() ?? _map; + _startRecvLoop( + recvStream, + isControl: false, + mapper: mapper, + label: label, + ); + if (sendStream != null) { + // Pool the send stream (with its already-running recv loop) so + // that _openAuxStream can reuse it for outbound traffic without + // calling connectionOpenBi. + _serverStreamPool.add( + _QuicStreamChannel( + sendStream: sendStream, + recvStream: recvStream, + ), + ); + } + } catch (error) { + if (!_closed) { + debugPrint('QUIC server-stream accept loop error: $error'); + } + break; + } + } + }), + ); + } + + void _startControlRecvLoop() { + final recvStream = _recvStream; + if (recvStream == null) { + return; + } + _startRecvLoop(recvStream, isControl: true, mapper: _map); + } + + void _startRecvLoop( + QuicRecvStream initial, { + required bool isControl, + int? slot, + String? label, + String Function(String)? mapper, + }) { + unawaited( + Future(() async { + var recvStream = initial; + try { + while (!_closed) { + final readResult = await recvStreamRead( + stream: recvStream, + maxLength: BigInt.from(16 * 1024), + ); + recvStream = readResult.$1; + if (isControl) { + _recvStream = recvStream; + } else if (slot != null) { + final channel = _auxStreamsBySlot[slot]; + if (channel != null) { + channel.recvStream = recvStream; + } + } + final bytes = readResult.$2; + if (bytes == null) { + break; + } + if (bytes.isEmpty) { + continue; + } + final chunk = utf8.decode(bytes, allowMalformed: true); + if (isControl) { + _captureBindResult(chunk); + } + final recvLabel = isControl + ? 'quic-control' + : (label ?? 'quic-aux-${slot ?? '?'}'); + Log.xmppp_receiving(chunk, channel: recvLabel); + // Use the per-stream mapper if provided (aux streams), otherwise + // fall back to the shared _map (control stream). This ensures each + // QUIC stream's partial XML fragments are buffered independently + // and do not corrupt each other's parse state. + final mapped = (mapper ?? _map)(chunk); + _quicStreamController.add(mapped); + } + } catch (error, stackTrace) { + if (!_quicStreamController.isClosed && + !_isQuicConnectionClosure(error)) { + try { + _quicStreamController.addError(error, stackTrace); + } catch (_) { + // If no listener is present for stream errors, treat as closed. + } + } + } finally { + if (isControl) { + await _logQuicCloseReason(); + if (!_quicStreamController.isClosed) { + await _quicStreamController.close(); + } + } else { + final endLabel = label ?? (slot != null ? 'quic-aux-$slot' : 'quic-aux-?'); + debugPrint('QUIC aux stream recv loop ended label=$endLabel'); + } + } + }), + ); + } + + Future<_QuicSendTarget?> _selectSendTarget(String payload) async { + final control = _sendStream; + if (control == null) { + throw StateError('QUIC control send stream is not connected'); + } + if (!_postBindReady) { + return _QuicSendTarget( + stream: control, + update: (updated) => _sendStream = updated, + label: 'quic-control', + ); + } + + // Per XEP-0467 §Multiple Streams: only stanzas (`message`/`presence`/`iq`) + // may be routed onto an aux stream. Top-level non-stanzas — stream errors, + // CSI (``/``), XEP-0198 ``/``, SASL frames, + // `` openers, etc. — MUST be sent on the initial (control) + // stream regardless of any `to=` they might carry. We check the element + // name first so a future extension that puts a `to=` on a non-stanza top + // level element does not accidentally route off the control stream. + if (!_isStanzaPayload(payload)) { + return _QuicSendTarget( + stream: control, + update: (updated) => _sendStream = updated, + label: 'quic-control', + ); + } + + final toBare = extractToBareJidForRouting(payload); + // Keep stanzas on the control stream when: + // * the stanza has no `to` (server-directed, typical for negotiation IQs) + // * the `to` is our own bare JID (self-directed) + // * the `to` is our own server's bare domain (e.g. disco#info to the + // account domain during negotiation) — this is negotiation traffic + // which MUST not be queued behind an aux-stream open. + final accountDomain = _accountBareJid == null + ? null + : _accountBareJid!.contains('@') + ? _accountBareJid!.split('@').last + : _accountBareJid; + if (toBare == null || + (_accountBareJid != null && toBare == _accountBareJid) || + (accountDomain != null && toBare == accountDomain)) { + return _QuicSendTarget( + stream: control, + update: (updated) => _sendStream = updated, + label: 'quic-control', + ); + } + + final slot = quicAuxSlotForBareJid(toBare, _auxStreamSlots); + final existing = _auxStreamsBySlot[slot]; + if (existing == null) { + // The aux stream for this slot is not yet open. Enqueue the payload so + // it is sent on the correct stream once it opens, rather than falling + // back to the control stream and mixing traffic. + _auxStreamPendingQueue.putIfAbsent(slot, () => []).add(payload); + // Kick off opening the aux stream (coalesced: concurrent calls for the + // same slot share one future). On success flush the pending queue. + _ensureAuxStream(slot, reason: 'on-demand routing for $toBare').then( + (channel) => _flushAuxPendingQueue(slot, channel), + onError: (Object error) { + // Aux stream open failed (disposed arc, timeout, closed connection). + // Drain the pending queue onto the control stream so stanzas are not + // silently dropped. + debugPrint( + 'QUIC aux stream on-demand open failed slot=$slot error=$error; ' + 'draining ${_auxStreamPendingQueue[slot]?.length ?? 0} queued ' + 'stanza(s) to control stream', + ); + final queued = _auxStreamPendingQueue.remove(slot) ?? []; + for (final qPayload in queued) { + Log.xmppp_sending(qPayload, channel: 'quic-control (fallback)'); + _writeQueue = _writeQueue.then((_) async { + if (_closed || _sendStream == null) return; + try { + final updated = await sendStreamWriteAll( + stream: _sendStream!, + data: utf8.encode(qPayload), + ); + _sendStream = updated; + } catch (_) {} + }); + } + }, + ); + // Return a null-target sentinel: the write() caller must not send the + // payload now — it has been enqueued above. + return null; + } + final channel = existing; + return _QuicSendTarget( + stream: channel.sendStream, + update: (updated) => channel.sendStream = updated, + label: 'quic-aux-$slot', + ); + } + + /// Flushes stanzas queued for [slot] onto [channel] now that the aux stream + /// is open. Each stanza is sent in order via the write queue. + void _flushAuxPendingQueue(int slot, _QuicStreamChannel channel) { + final queued = _auxStreamPendingQueue.remove(slot); + if (queued == null || queued.isEmpty) return; + debugPrint( + 'QUIC aux stream slot=$slot flushing ${queued.length} queued stanza(s)', + ); + for (final qPayload in queued) { + Log.xmppp_sending(qPayload, channel: 'quic-aux-$slot (flushed)'); + _writeQueue = _writeQueue.then((_) async { + if (_closed) return; + try { + final updated = await sendStreamWriteAll( + stream: channel.sendStream, + data: utf8.encode(qPayload), + ); + channel.sendStream = updated; + } catch (error, stackTrace) { + if (!_quicStreamController.isClosed) { + _quicStreamController.addError(error, stackTrace); + } + } + }); + } + } + + Future<_QuicStreamChannel> _ensureAuxStream(int slot, {String? reason}) { + final existing = _auxStreamsBySlot[slot]; + if (existing != null) { + return Future.value(existing); + } + // If the peer is known to be credit-starved (advertised + // initial_max_streams_bidi <= 1, or a previous open hit the timeout), + // skip even attempting connectionOpenBi: it will block until the server + // grants more credits via a MAX_STREAMS frame, which `_maxStreamsWatchTimer` + // is monitoring. Failing fast lets the caller fall back to the control + // stream rather than queueing on the FFI lock. + if (_auxStreamsBlocked) { + return Future.error( + StateError( + 'QUIC aux streams blocked: peer has not granted bidi stream credits', + ), + ); + } + // Coalesce concurrent opens for the same slot: if one is already in + // flight, return the same future so callers share the result rather than + // each trying to consume _connection via Auto_Owned FFI transfer. + return _auxStreamOpening.putIfAbsent(slot, () => _openAuxStream(slot, reason: reason ?? 'on-demand')); + } + + Future<_QuicStreamChannel> _openAuxStream(int slot, {String reason = 'pre-open'}) async { + // Prefer a server-initiated stream from the pool over opening a new + // client-initiated stream. This avoids a connectionOpenBi round-trip and + // conserves bidi-stream credits. + if (_serverStreamPool.isNotEmpty) { + final pooled = _serverStreamPool.removeAt(0); + _auxStreamsBySlot[slot] = pooled; + debugPrint( + 'QUIC aux stream slot=$slot assigned from server-stream pool ' + '(recv loop already running; ${_serverStreamPool.length} remaining in pool)', + ); + // The recv loop for this stream was started immediately when the server + // stream was accepted — do NOT start a second one here. + _auxStreamOpening.remove(slot); + return pooled; + } + + try { + // Serialise all connectionOpenBi calls globally: the FFI function uses + // Auto_Owned transfer, consuming _connection and returning a new arc. + // Two concurrent calls on different slots would both capture the same + // (soon-to-be-consumed) arc, causing DroppableDisposedException on the + // second call. + final completer = Completer(); + final previousLock = _auxOpenLock; + _auxOpenLock = completer.future; + try { + debugPrint('QUIC aux stream slot=$slot awaiting open lock (reason=$reason)'); + await previousLock; + if (_connection == null || _closed) { + throw StateError('QUIC connection is not established'); + } + debugPrint('QUIC aux stream opening slot=$slot reason=$reason (calling connectionOpenBi)'); + await _logConnectionStats('pre-open slot=$slot'); + // Re-read _connection after the stats call: _logConnectionStats + // consumes the arc via Auto_Owned and stores the returned arc back + // into _connection, so the local variable captured above is stale. + final freshConnection = _connection; + if (freshConnection == null || _closed) { + throw StateError('QUIC connection lost during pre-open stats'); + } + // connectionOpenBi (Quinn open_bi) will BLOCK until the peer has + // granted enough bidirectional-stream credits for a new stream to be + // opened. If the server advertises a low initial_max_streams_bidi and + // does not proactively send MAX_STREAMS frames, this future can hang + // indefinitely — which previously held _auxOpenLock forever and + // prevented every subsequent aux slot from even logging that it was + // trying to open. Add progress logging and a hard timeout so this + // failure mode is visible and recoverable. + final openStart = DateTime.now(); + Timer? progressTimer; + progressTimer = Timer.periodic(const Duration(seconds: 2), (_) { + final elapsed = DateTime.now().difference(openStart); + debugPrint( + 'QUIC aux stream slot=$slot connectionOpenBi still pending after ' + '${elapsed.inMilliseconds}ms — peer has likely not granted bidi ' + 'stream credits yet', + ); + }); + try { + final opened = await connectionOpenBi(connection: freshConnection) + .timeout(const Duration(seconds: 10), onTimeout: () { + // Mark that we are credit-starved and log stats so we can see + // the server's MAX_STREAMS frame count at the moment of timeout. + _auxStreamsBlocked = true; + _startMaxStreamsWatcher(); + throw TimeoutException( + 'connectionOpenBi timed out for aux slot $slot after 10s ' + '(peer likely did not grant additional bidi stream credits)', + ); + }); + // Re-check after the async gap: close() may have fired while we awaited. + if (_closed) { + // Discard the newly opened streams; the connection is being torn down. + throw StateError('QUIC socket closed during aux stream open'); + } + _connection = opened.$1; + final channel = _QuicStreamChannel( + sendStream: opened.$2, + recvStream: opened.$3, + ); + _auxStreamsBySlot[slot] = channel; + final elapsed = DateTime.now().difference(openStart); + debugPrint( + 'QUIC aux stream opened (outbound) slot=$slot ' + 'in ${elapsed.inMilliseconds}ms', + ); + // Give each aux recv loop its own independent XML mapper/buffer so + // that partial fragments from different QUIC streams do not + // interleave in the shared restOfResponse buffer of Connection's + // prepareStreamResponse. Fall back to _map if no factory was set + // (e.g. in tests that don't call setAuxMapperFactory). + final auxMapper = _makeAuxMapper?.call() ?? _map; + _startRecvLoop(opened.$3, isControl: false, slot: slot, mapper: auxMapper); + return channel; + } catch (error, stack) { + final elapsed = DateTime.now().difference(openStart); + debugPrint( + 'QUIC aux stream open error slot=$slot after ' + '${elapsed.inMilliseconds}ms error=$error', + ); + debugPrint('QUIC aux stream open stack slot=$slot: $stack'); + // Log stats after a timeout/error so we can see the connection + // state at the moment of failure. Use unawaited here since we are + // in a catch block and the arc may already be consumed; errors are + // swallowed by _logConnectionStats itself. + unawaited(_logConnectionStats('post-open-error slot=$slot')); + rethrow; + } finally { + progressTimer.cancel(); + } + } finally { + completer.complete(); + } + } finally { + _auxStreamOpening.remove(slot); + } + } + + void _captureBindResult(String chunk) { + if (_postBindReady) { + return; + } + _controlBuffer += chunk; + if (_controlBuffer.length > _maxControlBufferChars) { + _controlBuffer = _controlBuffer.substring( + _controlBuffer.length - _maxControlBufferChars, + ); + } + + final bindResult = RegExp( + ']*xmlns=(["\\\'])urn:ietf:params:xml:ns:xmpp-bind\\1[^>]*>.*?([^<]+)', + dotAll: true, + caseSensitive: false, + ).firstMatch(_controlBuffer); + if (bindResult == null) { + return; + } + final fullJid = bindResult.group(2); + final bare = bareJidForRouting(fullJid); + if (bare == null || bare.isEmpty) { + return; + } + _accountBareJid = bare; + _postBindReady = true; + debugPrint( + 'QUIC post-bind multi-stream enabled; accountBareJid=$bare; ' + 'aux streams will be opened lazily on demand', + ); + // Per XEP-0467 §Multiple Streams, aux streams "MAY be opened ... after + // resource binding" — they are not required to be eagerly pre-opened. + // We previously opened all _auxStreamSlots immediately post-bind, which + // was permissible but suboptimal: + // * On a credit-starved server it burned the first slot on a 10s + // timeout and then chained DroppableDisposedException across the + // remaining 19 (see WIMSY-1B/1C/1D Sentry chain). + // * It opened streams the client did not actually need yet. + // Aux streams are now opened lazily by `_selectSendTarget` the first + // time a stanza is routed to a destination bare JID that hashes to a + // slot we have not yet opened. This matches the spec's intent ("opened + // when needed for a destination bare-JID pair") and naturally limits + // how many credits we consume to the number of distinct bare JIDs we + // are actually communicating with. + } + + /// Logs key connection stats (MAX_STREAMS frame counts) for diagnostics. + /// Returns a Future that completes after the stats have been fetched and + /// _connection updated with the returned arc. Callers inside async contexts + /// should await this so that _connection is valid for the next FFI call. + Future _logConnectionStats(String context) async { + final connection = _connection; + if (connection == null) { + return; + } + try { + final result = await connectionStats(connection: connection); + _connection = result.$1; + final stats = result.$2; + final rxMaxBidi = stats.frameRx.maxStreamsBidi; + final rxMaxUni = stats.frameRx.maxStreamsUni; + final rxBlockedBidi = stats.frameRx.streamsBlockedBidi; + final txBlockedBidi = stats.frameTx.streamsBlockedBidi; + final txStream = stats.frameTx.stream; + final rxStream = stats.frameRx.stream; + debugPrint( + 'QUIC connection stats [$context]: ' + 'server MAX_STREAMS(bidi) frames received=$rxMaxBidi ' + 'MAX_STREAMS(uni) frames received=$rxMaxUni ' + 'STREAMS_BLOCKED(bidi) sent by us=$txBlockedBidi ' + 'STREAMS_BLOCKED(bidi) received from server=$rxBlockedBidi ' + 'STREAM frames sent=$txStream received=$rxStream', + ); + _lastMaxStreamsBidiFrameCount = rxMaxBidi; + } catch (error) { + debugPrint('QUIC connection stats error [$context]: $error'); + } + } + + /// Logs the peer's advertised QUIC transport parameters (initial stream + /// credits and connection flow-control window) for diagnostics. + /// + /// Exposed via our patched vendored quinn/quinn-proto (upstream quinn 0.11 + /// does not surface peer transport parameters). If the peer advertised only + /// enough bidi streams for the control stream (<= 1), pre-flags aux stream + /// opens as blocked so `_startAuxStreamsOpen` can decline immediately + /// instead of burning a 10s timeout on `connectionOpenBi`. + Future _logPeerTransportParams(String context) async { + final connection = _connection; + if (connection == null) { + return; + } + try { + final result = + await connectionPeerTransportParams(connection: connection); + _connection = result.$1; + final params = result.$2; + final bidi = params.initialMaxStreamsBidi; + final uni = params.initialMaxStreamsUni; + final data = params.initialMaxData; + debugPrint( + 'QUIC peer transport params [$context]: ' + 'initial_max_streams_bidi=$bidi ' + 'initial_max_streams_uni=$uni ' + 'initial_max_data=$data', + ); + // The control stream consumes client-initiated bidi stream id 0, so we + // need the peer to have advertised at least 2 to open aux slot 0. + if (bidi <= BigInt.one) { + _auxStreamsBlocked = true; + debugPrint( + 'QUIC peer transport params [$context]: ' + 'peer advertised initial_max_streams_bidi=$bidi (<= 1); ' + 'aux stream opens are pre-flagged as blocked, ' + 'waiting for server MAX_STREAMS(bidi) frame before attempting', + ); + _startMaxStreamsWatcher(); + } + } catch (error) { + debugPrint('QUIC peer transport params error [$context]: $error'); + } + } + + /// Starts a periodic watcher that detects when the server sends a new + /// MAX_STREAMS(bidi) frame, clears the blocked flag, and resumes aux opens. + void _startMaxStreamsWatcher() { + if (_maxStreamsWatchTimer != null) { + return; // Already watching. + } + debugPrint( + 'QUIC aux stream watcher started: ' + 'waiting for server MAX_STREAMS(bidi) frame', + ); + _maxStreamsWatchTimer = Timer.periodic(const Duration(seconds: 5), (_) { + if (_closed || _connection == null) { + _maxStreamsWatchTimer?.cancel(); + _maxStreamsWatchTimer = null; + return; + } + final prevCount = _lastMaxStreamsBidiFrameCount; + unawaited(_logConnectionStats('max-streams-watcher').then((_) { + final rxMaxBidi = _lastMaxStreamsBidiFrameCount; + debugPrint( + 'QUIC aux stream watcher: ' + 'server MAX_STREAMS(bidi) frames received=$rxMaxBidi ' + '(was $prevCount)', + ); + if (rxMaxBidi > prevCount) { + _auxStreamsBlocked = false; + _maxStreamsWatchTimer?.cancel(); + _maxStreamsWatchTimer = null; + debugPrint( + 'QUIC aux stream watcher: server granted more bidi stream credits ' + '(MAX_STREAMS frame count increased to $rxMaxBidi); ' + 'lazy aux stream opens via _selectSendTarget will now succeed', + ); + } + })); + }); + } + + String _formatSocketAddress(InternetAddress address, int port) { + if (address.type == InternetAddressType.IPv6) { + return '[${address.address}]:$port'; + } + return '${address.address}:$port'; + } + + Future _ensureRustInitialized() { + if (_rustInitialized) { + return Future.value(); + } + _rustInitFuture ??= RustLib.init().then((_) { + _rustInitialized = true; + }); + return _rustInitFuture!; + } + + /// Logs a human-readable message explaining who closed the QUIC connection + /// and why, using Quinn's [ConnectionError] variant embedded in the Debug + /// string returned by [connectionCloseReason]. + /// + /// Variant meanings: + /// - `LocallyClosed` — **we** called `connection.close()` (client-side) + /// - `ApplicationClosed` — the **remote peer** sent a QUIC APPLICATION_CLOSE + /// - `TimedOut` — idle timeout expired (no keepalive from either side) + /// - `Reset` — stateless reset from the remote peer + /// - `TransportError` — QUIC transport-level protocol error + /// - `VersionMismatch` — QUIC version negotiation failed + /// - null / unknown — stream ended cleanly or reason unavailable + Future _logQuicCloseReason() async { + final conn = _connection; + if (conn == null) { + debugPrint('QUIC connection closed (no connection arc available for close-reason query)'); + return; + } + try { + final (updatedConn, reason) = await connectionCloseReason(connection: conn); + _connection = updatedConn; + if (reason == null) { + debugPrint('QUIC connection closed cleanly (no error reported)'); + return; + } + final String who; + final String detail; + if (reason.contains('LocallyClosed')) { + who = 'LOCAL (we closed it)'; + detail = reason; + } else if (reason.contains('ApplicationClosed')) { + who = 'REMOTE (peer sent APPLICATION_CLOSE)'; + detail = reason; + } else if (reason.contains('TimedOut')) { + who = 'TIMEOUT (idle timeout — neither side sent keepalive in time)'; + detail = reason; + } else if (reason.contains('Reset')) { + who = 'REMOTE (stateless reset from peer)'; + detail = reason; + } else if (reason.contains('TransportError')) { + who = 'TRANSPORT ERROR (QUIC protocol error)'; + detail = reason; + } else if (reason.contains('VersionMismatch')) { + who = 'VERSION MISMATCH'; + detail = reason; + } else { + who = 'UNKNOWN'; + detail = reason; + } + debugPrint('QUIC connection closed: $who — $detail'); + } catch (e) { + debugPrint('QUIC connection closed (could not query close reason: $e)'); + } + } + + bool _isQuicConnectionClosure(Object error) { + final message = error.toString(); + return message.contains('QuicReadException.connectionLost') || + message.contains('ConnectionLost') || + message.contains('TimedOut') || + message.contains('ApplicationClosed') || + message.contains('Reset'); + } +} + +List buildQuicHappyEyeballsPlan( + List addresses, +) { + final ipv6 = []; + final ipv4 = []; + for (final address in addresses) { + if (address.type == InternetAddressType.IPv6) { + ipv6.add(address); + } else if (address.type == InternetAddressType.IPv4) { + ipv4.add(address); + } + } + + final preferIpv6 = + addresses.isNotEmpty && addresses.first.type == InternetAddressType.IPv6; + final plan = []; + while (ipv6.isNotEmpty || ipv4.isNotEmpty) { + if (preferIpv6) { + if (ipv6.isNotEmpty) { + plan.add(ipv6.removeAt(0)); + } + if (ipv4.isNotEmpty) { + plan.add(ipv4.removeAt(0)); + } + } else { + if (ipv4.isNotEmpty) { + plan.add(ipv4.removeAt(0)); + } + if (ipv6.isNotEmpty) { + plan.add(ipv6.removeAt(0)); + } + } + } + return plan; +} + +/// Returns true when [payload] begins with a top-level XMPP stanza element +/// (`message`, `presence`, or `iq`). +/// +/// Per XEP-0467 §Multiple Streams, only stanzas may be sent on aux streams; +/// every other top level element (stream errors, CSI, ``/``, SASL +/// frames, the `` opener, etc.) MUST be sent on the initial +/// stream. This helper looks at the first XML element name in the payload, +/// skipping any leading `` prolog and whitespace, and matching the +/// XMPP-defined stanza local-names case-sensitively as required by the +/// `jabber:client` namespace. +bool isStanzaPayload(String payload) { + var i = 0; + final length = payload.length; + while (i < length) { + final ch = payload.codeUnitAt(i); + // Skip whitespace. + if (ch == 0x20 || ch == 0x09 || ch == 0x0a || ch == 0x0d) { + i++; + continue; + } + if (ch != 0x3c /* '<' */) { + return false; + } + // Skip XML prolog ``. + if (i + 1 < length && payload.codeUnitAt(i + 1) == 0x3f /* '?' */) { + final end = payload.indexOf('?>', i + 2); + if (end < 0) { + return false; + } + i = end + 2; + continue; + } + // First real element starts at i+1. + final nameStart = i + 1; + var j = nameStart; + while (j < length) { + final c = payload.codeUnitAt(j); + if (c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d || + c == 0x2f /* '/' */ || c == 0x3e /* '>' */) { + break; + } + j++; + } + final name = payload.substring(nameStart, j); + return name == 'message' || name == 'presence' || name == 'iq'; + } + return false; +} + +bool _isStanzaPayload(String payload) => isStanzaPayload(payload); + +String? extractToBareJidForRouting(String payload) { + final toMatch = RegExp('\\bto=(["\\\'])([^"\\\']+)\\1').firstMatch(payload); + if (toMatch == null) { + return null; + } + return bareJidForRouting(toMatch.group(2)); +} + +String? bareJidForRouting(String? jid) { + if (jid == null) { + return null; + } + final trimmed = jid.trim(); + if (trimmed.isEmpty) { + return null; + } + final slash = trimmed.indexOf('/'); + if (slash == -1) { + return trimmed; + } + return trimmed.substring(0, slash); +} + +int quicAuxSlotForBareJid(String bareJid, int slotCount) { + if (slotCount <= 0) { + throw ArgumentError.value(slotCount, 'slotCount', 'must be > 0'); + } + var hash = 0x811c9dc5; + for (final unit in bareJid.codeUnits) { + hash ^= unit; + hash = (hash * 0x01000193) & 0xffffffff; + } + return hash % slotCount; +} + +class _QuicConnectResult { + const _QuicConnectResult({ + this.address, + required this.endpoint, + required this.connection, + required this.sendStream, + required this.recvStream, + }); + + /// Remote address that won the race. May be null for intermediate results + /// produced by [_connectQuicAddress] before the racer wraps them. + final InternetAddress? address; + final QuicEndpoint endpoint; + final QuicConnection connection; + final QuicSendStream sendStream; + final QuicRecvStream recvStream; +} + +class _QuicStreamChannel { + _QuicStreamChannel({required this.sendStream, required this.recvStream}); + + QuicSendStream sendStream; + QuicRecvStream recvStream; +} + +class _QuicSendTarget { + const _QuicSendTarget({ + required this.stream, + required this.update, + required this.label, + }); + + final QuicSendStream stream; + final void Function(QuicSendStream updated) update; + final String label; +} diff --git a/lib/xmpp/srv_lookup_native.dart b/lib/xmpp/srv_lookup_native.dart index 5a416f7..2015e6e 100644 --- a/lib/xmpp/srv_lookup_native.dart +++ b/lib/xmpp/srv_lookup_native.dart @@ -48,6 +48,27 @@ Future> resolveXmppSrvCandidates(String domain) async { return ordered; } +Future> resolveXmppQuicSrvCandidates(String domain) async { + debugPrint('QUIC SRV lookup: domain=$domain'); + final records = await _lookupSrv( + '_xmpp-client._quic.$domain', + directTls: false, + ); + if (records.isEmpty) { + debugPrint('QUIC SRV lookup: no records found'); + return const []; + } + final ordered = orderXmppSrvTargets(records); + final orderSummary = ordered + .map( + (record) => + '${record.host}:${record.port}/p${record.priority}/w${record.weight}', + ) + .join(', '); + debugPrint('QUIC SRV lookup: ordered candidates=[$orderSummary]'); + return ordered; +} + Future> _lookupSrv( String name, { required bool directTls, diff --git a/lib/xmpp/srv_lookup_stub.dart b/lib/xmpp/srv_lookup_stub.dart index 163e3e9..15099df 100644 --- a/lib/xmpp/srv_lookup_stub.dart +++ b/lib/xmpp/srv_lookup_stub.dart @@ -11,3 +11,7 @@ Future resolveXmppSrv(String domain) async { Future> resolveXmppSrvCandidates(String domain) async { return const []; } + +Future> resolveXmppQuicSrvCandidates(String domain) async { + return const []; +} diff --git a/lib/xmpp/srv_target.dart b/lib/xmpp/srv_target.dart index 3d46e3e..9f4a490 100644 --- a/lib/xmpp/srv_target.dart +++ b/lib/xmpp/srv_target.dart @@ -13,3 +13,20 @@ class XmppSrvTarget { final int weight; final bool directTls; } + +/// Returns the subset of [candidates] permitted by the user's transport +/// allow-flags. +/// +/// `_xmpps-client._tcp` (Direct TLS) records are kept only when +/// [allowDirectTls] is true; `_xmpp-client._tcp` (plain TCP) records are kept +/// only when [allowPlainTcp] is true. The flags are independent allow-lists, +/// so disabling both yields an empty list. +List filterTcpSrvCandidatesByTransport( + List candidates, { + required bool allowDirectTls, + required bool allowPlainTcp, +}) { + return candidates + .where((c) => c.directTls ? allowDirectTls : allowPlainTcp) + .toList(growable: false); +} diff --git a/lib/xmpp/startup_fetch_helpers.dart b/lib/xmpp/startup_fetch_helpers.dart new file mode 100644 index 0000000..e6c4328 --- /dev/null +++ b/lib/xmpp/startup_fetch_helpers.dart @@ -0,0 +1,71 @@ +/// Pure helpers extracted from `xmpp_service.dart` so the connect-time +/// fetch-elision decisions tracked in `doc/startup-fetch-review.md` can be +/// unit-tested without spinning up a live Connection / StorageService. +/// +/// Each function is intentionally tiny, side-effect free, and named after +/// the recommendation it implements (R1.1, R2.2, etc.). The corresponding +/// production code calls into them from `xmpp_service.dart`. +library; + +/// R1.1 — skip the bootstrap `urn:xmpp:mds:displayed:0` PubSub items GET +/// when the in-memory map of displayed stanza ids was successfully restored +/// from disk on startup. +/// +/// PEP +notify pushes (already handled by the existing event handler) keep +/// the cache live thereafter, so the IQ is only required on a true cold +/// start (no persisted MDS state for this account). +/// +/// [hasCachedDisplayedSync] should be true when the in-memory +/// `_displayedStanzaIdByChat` map is non-empty after disk seeding. +/// +/// [force] is provided for completeness — callers that genuinely want to +/// re-pull MDS regardless of cache (e.g. a manual "resync" command in the +/// UI) can pass true. +bool shouldFetchDisplayedSyncBootstrap({ + required bool hasCachedDisplayedSync, + bool force = false, +}) { + if (force) { + return true; + } + return !hasCachedDisplayedSync; +} + +/// R2.2 — skip the per-chat MAM catch-up `` when the displayed +/// marker we already have on disk points at a stanza whose MAM id matches +/// the latest local MAM id for the same chat. Under those conditions we are +/// demonstrably caught up to the displayed marker and there is nothing the +/// catch-up query could give us beyond what +notify / live messages already +/// will. +/// +/// All three pieces of context come from `xmpp_service.dart`'s in-memory +/// state: +/// +/// * [displayedStanzaId] — the stanza-id MDS reports as displayed in this +/// chat. Empty / null when MDS has nothing for the chat. +/// * [latestLocalMamId] — the MAM id of the newest message in our local +/// cache. Null when the chat has no cached messages. +/// * [stanzaIdAtLatestMamId] — the stanza-id of that newest cached +/// message. Null when missing. +/// +/// Returns true when the catch-up query should still go out. +bool shouldFetchMamCatchUpForChat({ + required String? displayedStanzaId, + required String? latestLocalMamId, + required String? stanzaIdAtLatestMamId, +}) { + // No MDS marker — we don't know whether we're caught up, so fetch. + if (displayedStanzaId == null || displayedStanzaId.isEmpty) { + return true; + } + // No local MAM cursor — nothing to compare against, so fetch. + if (latestLocalMamId == null || latestLocalMamId.isEmpty) { + return true; + } + // Latest local message has no recorded stanza-id — be safe and fetch. + if (stanzaIdAtLatestMamId == null || stanzaIdAtLatestMamId.isEmpty) { + return true; + } + // Marker matches the latest local message: caught up. + return displayedStanzaId != stanzaIdAtLatestMamId; +} diff --git a/lib/xmpp/vcard_utils.dart b/lib/xmpp/vcard_utils.dart index a3a8ec9..54ea202 100644 --- a/lib/xmpp/vcard_utils.dart +++ b/lib/xmpp/vcard_utils.dart @@ -57,6 +57,68 @@ Future vcardPhotoHash(Uint8List bytes) async { return _toHex(hash.bytes); } +/// Sentinel value stored in the vCard avatar state map to mark a JID as +/// having no avatar. Persisted to disk via `StorageService` so we don't keep +/// re-fetching across restarts. +const String vcardNoAvatarSentinel = 'none'; + +/// Decide whether we should send an `` to [bareJid] given +/// what we already have cached. +/// +/// The vCard avatar bytes for [bareJid] live in [cachedAvatarBytes] (keyed by +/// bare JID) and the SHA-1 photo hash in [cachedAvatarState]. The state value +/// may be: +/// +/// * empty / missing — we have never received any state for this JID, +/// * [vcardNoAvatarSentinel] — we know the JID has no avatar, +/// * any other string — the SHA-1 hash advertised in the most recent +/// `vcard-temp:x:update` presence we saw. +/// +/// When [preferName] is true the caller specifically wants the FN/NICKNAME +/// fields (not just the avatar), so we always fetch. +/// +/// Otherwise we only fetch when the cache cannot satisfy a presence-driven +/// avatar refresh, i.e. when we are missing bytes for the advertised hash, or +/// when we have no recorded state at all. +bool shouldFetchVcardForCache({ + required String bareJid, + required bool preferName, + required Map cachedAvatarBytes, + required Map cachedAvatarState, + String? advertisedHash, +}) { + if (preferName) { + return true; + } + final state = cachedAvatarState[bareJid] ?? ''; + // No state at all — never seen this JID. Need to fetch. + if (state.isEmpty) { + return true; + } + // We've previously confirmed there is no avatar for this JID. The sentinel + // is sticky until presence advertises a non-empty hash. + if (state == vcardNoAvatarSentinel) { + if (advertisedHash != null && advertisedHash.isNotEmpty) { + return true; + } + return false; + } + // We have a recorded hash. If presence advertises a different hash we need + // to refetch the bytes. + if (advertisedHash != null && + advertisedHash.isNotEmpty && + advertisedHash != state) { + return true; + } + // Same hash (or no presence-advertised hash to compare against): only skip + // when we still have the bytes cached. Missing bytes implies a previous + // partial fetch we should retry. + if (!cachedAvatarBytes.containsKey(bareJid)) { + return true; + } + return false; +} + String normalizeVcardPhotoHash(String hash) { var normalized = hash.trim(); while (normalized.length.isEven && normalized.isNotEmpty) { diff --git a/lib/xmpp/xmpp_service.dart b/lib/xmpp/xmpp_service.dart index 26c25db..f183463 100644 --- a/lib/xmpp/xmpp_service.dart +++ b/lib/xmpp/xmpp_service.dart @@ -37,11 +37,14 @@ import 'jid_discovery.dart'; import 'chat_message_mutations.dart'; import 'message_intent_builder.dart'; import 'message_stanza_parser.dart'; +import 'startup_fetch_helpers.dart'; import 'vcard_utils.dart'; import 'ws_endpoint.dart'; import 'srv_lookup.dart'; import 'srv_target.dart'; import 'alt_connection.dart'; +import 'quic_endpoint_plan.dart'; +import 'quic_xmpp_socket.dart'; import 'tcp_endpoint_plan.dart'; class ReplyReference { @@ -144,6 +147,15 @@ class XmppService extends ChangeNotifier { final Map _chatStates = {}; final Map _lastDisplayedMarkerIdByChat = {}; String? _activeChatBareJid; + final List _quicRttHistory = []; + final List _quicLossHistory = []; + BigInt _lastQuicLostPackets = BigInt.zero; + Timer? _quicStatsTimer; + static const int _maxQuicHistory = 60; + + List get quicRttHistory => _quicRttHistory; + List get quicLossHistory => _quicLossHistory; + void Function(String bareJid, ChatMessage message)? _incomingMessageHandler; void Function(String roomJid, ChatMessage message)? _incomingRoomMessageHandler; @@ -182,6 +194,18 @@ class XmppService extends ChangeNotifier { String? _rosterVersion; final Map _displayedStanzaIdByChat = {}; final Map _displayedAtByChat = {}; + // R1.3: pending displayed-sync markers we received from MDS but could + // not yet match to any locally cached message. Resolved as messages + // with the matching stanza-id are appended (live or via MAM). Persisted + // through `StorageService.storeDisplayedSyncPending` so the resolution + // survives restarts. + final Map _displayedSyncPending = {}; + // R2.1: globally newest MAM id we have ingested across all chats. + // Persisted via `StorageService.storeLastMamIdSeen` so future sessions + // can issue a single unified catch-up query (`afterId=` this anchor) + // instead of fanning out per-chat. The unified-query wiring is a + // follow-up; this commit lays the persistence foundation. + String? _lastMamIdSeen; final Map _roomLastTrafficAt = {}; final Map _roomLastPingAt = {}; final Map _roomHistoryCutoffAt = {}; @@ -205,7 +229,7 @@ class XmppService extends ChangeNotifier { final Map _vcardDisplayNames = {}; final Set _vcardRequests = {}; final Set _vcardUnavailable = {}; - static const _vcardNoAvatar = 'none'; + static const _vcardNoAvatar = vcardNoAvatarSentinel; String _selfVcardPhotoHash = ''; bool _selfVcardPhotoKnown = false; final Map _fileTransfers = {}; @@ -432,6 +456,34 @@ class XmppService extends ChangeNotifier { _displayedStanzaIdByChat ..clear() ..addAll(storage.loadDisplayedSync()); + // R1.3: seed pending displayed-sync markers from disk so we can keep + // trying to resolve them as messages arrive in this session. + _displayedSyncPending + ..clear() + ..addAll(storage.loadDisplayedSyncPending()); + // R2.1: seed the globally-newest MAM id anchor. + _lastMamIdSeen = storage.loadLastMamIdSeen(); + } + + /// R2.1: returns the globally newest MAM id we have ingested across all + /// chats, persisted across restarts. May be null on a fresh install. + String? get lastMamIdSeen => _lastMamIdSeen; + + /// R2.1: update the global MAM-id anchor. We compare lexicographically + /// because XEP-0359 stanza ids are unique, server-assigned strings; the + /// MAM ids we get from a single archive are typically allocated in + /// monotonic order, so a string-greater-than comparison is good enough + /// to discard out-of-order updates without spurious disk writes. + void _bumpLastMamIdSeen(String? mamId) { + if (mamId == null || mamId.isEmpty) { + return; + } + final current = _lastMamIdSeen; + if (current != null && current.compareTo(mamId) >= 0) { + return; + } + _lastMamIdSeen = mamId; + _storage?.storeLastMamIdSeen(mamId); } List messagesFor(String bareJid) { @@ -750,7 +802,16 @@ class XmppService extends ChangeNotifier { bool directTls = false, String? wsEndpoint, List? wsProtocols, + bool useQuic = true, + bool useTcp = true, }) async { + final quicTransportAvailable = + !kIsWeb && + (Platform.isAndroid || + Platform.isIOS || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows); final shouldUseWebSocket = kIsWeb || useWebSocket; WsEndpointConfig? wsConfig; if (shouldUseWebSocket) { @@ -770,7 +831,8 @@ class XmppService extends ChangeNotifier { var resolvedHost = host?.trim().isNotEmpty == true ? host!.trim() : ''; var resolvedPort = port; var resolvedDirectTls = directTls; - List srvCandidates = const []; + List quicSrvCandidates = const []; + List tcpSrvCandidates = const []; _finishSpan(_connectTransaction); _connectTransaction = _startTransaction( @@ -790,16 +852,56 @@ class XmppService extends ChangeNotifier { 'xmpp.srv_lookup', description: domain, ); - srvCandidates = await resolveXmppSrvCandidates(domain); + if (quicTransportAvailable && useQuic) { + quicSrvCandidates = await resolveXmppQuicSrvCandidates(domain); + } + tcpSrvCandidates = await resolveXmppSrvCandidates(domain); + // Filter SRV candidates by the user's transport allow-flags. + // - When Direct TLS is off, drop _xmpps-client._tcp records. + // - When Plain TCP is off, drop _xmpp-client._tcp records. + // The two flags act as independent allow-lists; the user is only + // allowed to actually connect via a transport they've enabled. + final filteredTcpSrv = filterTcpSrvCandidatesByTransport( + tcpSrvCandidates, + allowDirectTls: directTls, + allowPlainTcp: useTcp, + ); + if (filteredTcpSrv.length != tcpSrvCandidates.length) { + debugPrint( + 'XMPP SRV: filtered TCP candidates by user flags ' + '(directTls=$directTls useTcp=$useTcp): ' + '${tcpSrvCandidates.length} -> ${filteredTcpSrv.length}', + ); + } + tcpSrvCandidates = filteredTcpSrv; _finishSpan(srvSpan); - if (srvCandidates.isNotEmpty) { - final first = srvCandidates.first; + if (quicTransportAvailable && quicSrvCandidates.isNotEmpty) { + final first = quicSrvCandidates.first; + resolvedHost = first.host; + resolvedPort = first.port; + resolvedDirectTls = false; + } else if (tcpSrvCandidates.isNotEmpty) { + final first = tcpSrvCandidates.first; resolvedHost = first.host; resolvedPort = first.port; resolvedDirectTls = first.directTls; } else if (resolvedPort == 0 || resolvedPort == 5222) { resolvedPort = directTls ? 5223 : 5222; } + // If after filtering we have no usable transport at all (no QUIC, + // no surviving TCP SRV records, and the user has disabled the + // transport that the fallback port would use), bail out with a + // clear error rather than silently falling through. + final hasQuic = quicTransportAvailable && quicSrvCandidates.isNotEmpty; + final hasTcp = tcpSrvCandidates.isNotEmpty; + final fallbackAllowed = directTls || useTcp; + if (!hasQuic && !hasTcp && !fallbackAllowed) { + _setError( + 'No transport enabled: both Direct TLS and Plain TCP are ' + 'disabled and no QUIC/WebSocket endpoint is available.', + ); + return; + } } if (shouldUseWebSocket && wsConfig == null) { @@ -845,12 +947,18 @@ class XmppService extends ChangeNotifier { account.sasl2Software = 'Wimsy'; account.sasl2Device = resource; if (!shouldUseWebSocket) { + account.quicEndpoints = (quicTransportAvailable && useQuic) + ? buildQuicEndpointPlan( + domain: account.domain, + srvCandidates: quicSrvCandidates, + ) + : const []; account.tcpEndpoints = buildTcpEndpointPlan( domain: account.domain, resolvedHost: resolvedHost, resolvedPort: resolvedPort, directTls: resolvedDirectTls, - srvCandidates: srvCandidates, + srvCandidates: tcpSrvCandidates, ); } if (wsConfig != null) { @@ -862,7 +970,13 @@ class XmppService extends ChangeNotifier { account.wsProtocols = protocols.isEmpty ? null : protocols; } - final connection = Connection.getInstance(account); + final connection = + quicTransportAvailable && (account.quicEndpoints?.isNotEmpty ?? false) + ? Connection.getInstance( + account, + socketFactory: () => QuicCapableXmppSocket(), + ) + : Connection.getInstance(account); _connection = connection; connection.setReconnectPolicy( const ReconnectionPolicy( @@ -920,6 +1034,7 @@ class XmppService extends ChangeNotifier { _errorMessage = null; notifyListeners(); _setupKeepalive(); + _setupQuicStats(); _setupDeliveryTracking(); _setupJingle(); _setupIbb(); @@ -943,6 +1058,16 @@ class XmppService extends ChangeNotifier { _blockingSupported = connection.getSupportedFeatures().any( (feature) => feature.xmppVar == blockingNamespace, ); + // R6: Re-seed the vCard avatar caches from disk on every Ready + // event, not just at startup. The disconnect handler clears these + // maps so that stale in-memory state doesn't survive a resource + // rebind, but without re-seeding the R4.1 cache-guard sees an + // empty cache and re-fetches every vCard on reconnect. + final storage = _storage; + if (storage != null) { + _seedVcardAvatars(storage.loadVcardAvatars()); + _seedVcardAvatarState(storage.loadVcardAvatarState()); + } _setupRoster(); _setupChatManager(); _setupMuc(); @@ -951,6 +1076,7 @@ class XmppService extends ChangeNotifier { _setupIbb(); _setupPresence(); _setupKeepalive(); + _setupQuicStats(); _setupDeliveryTracking(); _setupPep(); _setupBookmarks(); @@ -1062,6 +1188,7 @@ class XmppService extends ChangeNotifier { _bookmarkPersistor?.call(const []); _storage?.storeRosterVersion(null); _displayedStanzaIdByChat.clear(); + _displayedSyncPending.clear(); _displayedAtByChat.clear(); _storage?.clearDisplayedSync(); notifyListeners(); @@ -2204,7 +2331,7 @@ class XmppService extends ChangeNotifier { return false; } return RegExp( - r'[\u{00A9}\u{00AE}\u{203C}-\u{3299}\u{1F000}-\u{1FAFF}]', + r'[\u00A9\u00AE\u203C-\u3299\u{1F000}-\u{1FAFF}]', unicode: true, ).hasMatch(value); } @@ -5060,6 +5187,10 @@ class XmppService extends ChangeNotifier { _pepCapsManager = PepCapsManager( connection: connection, pepManager: _pepManager!, + // R5: persist the XEP-0115 caps cache across restarts so MUC + // presence storms don't trigger a `disco#info` fan-out for caps + // we already verified in a previous session. + storage: storage, ); _requestRecentReactionEmojis(); _pepManager?.requestMetadataIfMissing(_currentUserBareJid!); @@ -5108,11 +5239,28 @@ class XmppService extends ChangeNotifier { _bookmarksManager?.requestBookmarks(); } - void _setupDisplayedSync() { + /// Send the bootstrap `urn:xmpp:mds:displayed:0` PubSub items GET. + /// + /// R1.1: Skip the IQ entirely when `_displayedStanzaIdByChat` was + /// successfully restored from disk on startup. PEP +notify pushes (which + /// we already handle in `_handleDisplayedSyncEvent`) keep the cache live + /// after that, so the bootstrap fetch is only needed on a true cold + /// start — i.e. when the in-memory map is still empty. + /// + /// We also expose [force] for tests and for hypothetical callers that + /// want to refresh the entire MDS state regardless of the local cache. + void _setupDisplayedSync({bool force = false}) { final connection = _connection; if (connection == null || _currentUserBareJid == null) { return; } + if (!shouldFetchDisplayedSyncBootstrap( + hasCachedDisplayedSync: _displayedStanzaIdByChat.isNotEmpty, + force: force, + )) { + // Cache was seeded from disk; rely on +notify for live updates. + return; + } final id = AbstractStanza.getRandomId(); final iqStanza = IqStanza(id, IqStanzaType.GET); iqStanza.toJid = Jid.fromFullJid(_currentUserBareJid!); @@ -5392,6 +5540,8 @@ class XmppService extends ChangeNotifier { ? _roomMessages[normalized] : _messages[normalized]; if (list == null || list.isEmpty) { + // R1.3: persist the pending marker so we can resolve it later. + _markDisplayedSyncPending(normalized, stanzaId); return false; } ChatMessage? matched; @@ -5412,19 +5562,77 @@ class XmppService extends ChangeNotifier { 'Displayed sync miss for chat=$normalized stanzaId=$stanzaId ' 'messages=${list.length} knownStanzaIds=$knownStanzaIds', ); + // R1.3: persist the (chat, stanzaId) pair. We will retry resolution + // every time a new message is appended (live or via MAM) for this + // chat — see `_resolveDisplayedSyncPending`. + _markDisplayedSyncPending(normalized, stanzaId); return false; } final existing = _displayedAtByChat[normalized]; if (existing != null && !matched.timestamp.isAfter(existing)) { + // Even if we don't update the timestamp, we *did* successfully match + // the marker — so clear the pending entry. + _clearDisplayedSyncPending(normalized); return false; } _displayedAtByChat[normalized] = matched.timestamp; final isRoom = _roomMessages.containsKey(normalized) || isBookmark(normalized); _markMamCatchUpCompleted(normalized, isRoom: isRoom); + _clearDisplayedSyncPending(normalized); return true; } + /// R1.3: record an unresolved (chat, stanza-id) pair and persist it so + /// the next session (or the next MAM page in this session) can resolve + /// it as messages arrive. + void _markDisplayedSyncPending(String bareJid, String stanzaId) { + if (bareJid.isEmpty || stanzaId.isEmpty) { + return; + } + if (_displayedSyncPending[bareJid] == stanzaId) { + return; + } + _displayedSyncPending[bareJid] = stanzaId; + _storage?.storeDisplayedSyncPending( + Map.from(_displayedSyncPending), + ); + } + + /// R1.3: clear a resolved pending marker and persist the smaller map. + void _clearDisplayedSyncPending(String bareJid) { + if (!_displayedSyncPending.containsKey(bareJid)) { + return; + } + _displayedSyncPending.remove(bareJid); + _storage?.storeDisplayedSyncPending( + Map.from(_displayedSyncPending), + ); + } + + /// R1.3: called from the message-append code paths whenever we add a + /// message with a known [stanzaId] to [bareJid]'s list. If the stanza-id + /// matches the chat's pending displayed-sync marker, kick the marker + /// resolution path. Cheap O(1) lookup; safe to call unconditionally. + void _resolveDisplayedSyncPending(String bareJid, String? stanzaId) { + if (stanzaId == null || stanzaId.isEmpty) { + return; + } + final normalized = _bareJid(bareJid); + final pending = _displayedSyncPending[normalized]; + if (pending == null || pending != stanzaId) { + return; + } + // Re-run the matcher; on success it clears the pending entry, sets the + // displayed timestamp, and marks MAM catch-up complete for this chat. + if (_applyDisplayedStateForChat(normalized)) { + _storage?.storeDisplayedSync( + Map.from(_displayedStanzaIdByChat), + ); + notifyListeners(); + } + } + void _setupPrivacyLists() { final connection = _connection; if (connection == null) { @@ -5617,6 +5825,36 @@ class XmppService extends ChangeNotifier { } } + void _setupQuicStats() { + _quicStatsTimer?.cancel(); + _quicRttHistory.clear(); + _quicLossHistory.clear(); + _lastQuicLostPackets = BigInt.zero; + + final socket = _connection?.socket; + if (socket == null || !socket.isQuic) return; + + _quicStatsTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + final stats = await socket.getQuicStats(); + if (stats == null) return; + + _quicRttHistory.add(stats.path.rttMillis.toInt()); + if (_quicRttHistory.length > _maxQuicHistory) { + _quicRttHistory.removeAt(0); + } + + final currentLoss = stats.path.lostPackets; + final deltaLoss = currentLoss - _lastQuicLostPackets; + _lastQuicLostPackets = currentLoss; + _quicLossHistory.add(deltaLoss.toInt()); + if (_quicLossHistory.length > _maxQuicHistory) { + _quicLossHistory.removeAt(0); + } + + notifyListeners(); + }); + } + void _setupKeepalive() { final connection = _connection; if (connection == null) { @@ -5921,13 +6159,7 @@ class XmppService extends ChangeNotifier { nextReplyToId != existing.replyToId || nextReplyToJid != existing.replyToJid || nextReplyFallback != existing.replyFallback) { - list[existingIndex] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, + list[existingIndex] = existing.copyWith( mamId: nextMamId, stanzaId: nextStanzaId, oobUrl: nextOobUrl, @@ -5936,21 +6168,9 @@ class XmppService extends ChangeNotifier { inviteRoomJid: nextInviteRoomJid, inviteReason: nextInviteReason, invitePassword: nextInvitePassword, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, replyToId: nextReplyToId, replyToJid: nextReplyToJid, replyFallback: nextReplyFallback, - acked: existing.acked, - receiptReceived: existing.receiptReceived, - displayed: existing.displayed, ); notifyListeners(); _messagePersistor?.call(normalized, List.unmodifiable(list)); @@ -6050,6 +6270,12 @@ class XmppService extends ChangeNotifier { } notifyListeners(); _messagePersistor?.call(normalized, List.unmodifiable(list)); + // R1.3: this newly-appended DM message may resolve a pending displayed + // marker (typical case after a restart for a chat where the marker + // pointed beyond our 25-message tail). + _resolveDisplayedSyncPending(normalized, stanzaId); + // R2.1: bump the global MAM-id anchor. + _bumpLastMamIdSeen(mamId); if (!outgoing && (mamId == null || mamId.isEmpty) && _isMamCatchUpComplete(normalized, isRoom: false)) { @@ -6123,33 +6349,17 @@ class XmppService extends ChangeNotifier { nextReplyToId != existing.replyToId || nextReplyToJid != existing.replyToJid || nextReplyFallback != existing.replyFallback) { - final updated = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, + final updated = existing.copyWith( timestamp: nextTimestamp, - messageId: existing.messageId, mamId: nextMamId, stanzaId: nextStanzaId, oobUrl: nextOobUrl, oobDescription: nextOobDescription, rawXml: nextRawXml, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, replyToId: nextReplyToId, replyToJid: nextReplyToJid, replyFallback: nextReplyFallback, - acked: existing.acked, receiptReceived: nextReceiptReceived, - displayed: existing.displayed, ); list.removeAt(existingIndex); _insertMessageOrdered(list, updated); @@ -6195,6 +6405,11 @@ class XmppService extends ChangeNotifier { _mamCursorStore.incrementPrependOffset(normalized); notifyListeners(); _roomMessagePersistor?.call(normalized, List.unmodifiable(list)); + // R1.3: a MAM-page-prepended MUC message can resolve a pending + // displayed marker for this room. + _resolveDisplayedSyncPending(normalized, stanzaId); + // R2.1: bump the global MAM-id anchor for prepended MAM messages. + _bumpLastMamIdSeen(mamId); if (!outgoing) { _incomingRoomMessageHandler?.call(normalized, list[insertIndex]); } @@ -6220,6 +6435,11 @@ class XmppService extends ChangeNotifier { _insertMessageOrdered(list, newMessage); notifyListeners(); _roomMessagePersistor?.call(normalized, List.unmodifiable(list)); + // R1.3: a freshly-appended MUC message may resolve a pending displayed + // marker for this room. + _resolveDisplayedSyncPending(normalized, stanzaId); + // R2.1: bump the global MAM-id anchor. + _bumpLastMamIdSeen(mamId); if (!outgoing && (mamId == null || mamId.isEmpty) && _isMamCatchUpComplete(normalized, isRoom: true) && @@ -6335,36 +6555,9 @@ class XmppService extends ChangeNotifier { } final nextState = state ?? existing.fileState; final nextBytes = fileBytes ?? existing.fileBytes; - list[i] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, - mamId: existing.mamId, - stanzaId: existing.stanzaId, - oobUrl: existing.oobUrl, - oobDescription: existing.oobDescription, - rawXml: existing.rawXml, - inviteRoomJid: existing.inviteRoomJid, - inviteReason: existing.inviteReason, - invitePassword: existing.invitePassword, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, + list[i] = existing.copyWith( fileBytes: nextBytes, fileState: nextState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, - acked: existing.acked, - receiptReceived: existing.receiptReceived, - displayed: existing.displayed, ); notifyListeners(); _messagePersistor?.call(normalized, List.unmodifiable(list)); @@ -6533,30 +6726,7 @@ class XmppService extends ChangeNotifier { nextDisplayed == existing.displayed) { return true; } - list[i] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, - mamId: existing.mamId, - stanzaId: existing.stanzaId, - oobUrl: existing.oobUrl, - oobDescription: existing.oobDescription, - rawXml: existing.rawXml, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, + list[i] = existing.copyWith( acked: nextAcked, receiptReceived: nextReceipt, displayed: nextDisplayed, @@ -6593,30 +6763,7 @@ class XmppService extends ChangeNotifier { nextDisplayed == existing.displayed) { return true; } - list[i] = ChatMessage( - from: existing.from, - to: existing.to, - body: existing.body, - outgoing: existing.outgoing, - timestamp: existing.timestamp, - messageId: existing.messageId, - mamId: existing.mamId, - stanzaId: existing.stanzaId, - oobUrl: existing.oobUrl, - oobDescription: existing.oobDescription, - rawXml: existing.rawXml, - fileTransferId: existing.fileTransferId, - fileName: existing.fileName, - fileSize: existing.fileSize, - fileMime: existing.fileMime, - fileBytes: existing.fileBytes, - fileState: existing.fileState, - edited: existing.edited, - editedAt: existing.editedAt, - reactions: existing.reactions ?? const {}, - replyToId: existing.replyToId, - replyToJid: existing.replyToJid, - replyFallback: existing.replyFallback, + list[i] = existing.copyWith( acked: nextAcked, receiptReceived: nextReceipt, displayed: nextDisplayed, @@ -6982,6 +7129,11 @@ class XmppService extends ChangeNotifier { } Future _safeClose({required bool preserveCache}) async { + _quicStatsTimer?.cancel(); + _quicStatsTimer = null; + _quicRttHistory.clear(); + _quicLossHistory.clear(); + _lastQuicLostPackets = BigInt.zero; _csiIdleTimer?.cancel(); _csiIdleTimer = null; _mucSelfPingTimer?.cancel(); @@ -7072,6 +7224,7 @@ class XmppService extends ChangeNotifier { _roomHistoryCutoffAt.clear(); _lastDisplayedMarkerIdByChat.clear(); _displayedStanzaIdByChat.clear(); + _displayedSyncPending.clear(); _displayedAtByChat.clear(); if (!preserveCache) { _recentReactionEmojis.clear(); @@ -7556,15 +7709,40 @@ class XmppService extends ChangeNotifier { } _lastGlobalMamSyncAt = now; - for (final entry in _messages.entries) { - final bareJid = _bareJid(entry.key); - if (isBookmark(bareJid)) { - continue; - } - if (entry.value.isEmpty) { - continue; + // R2.1: When we have a global MAM anchor (_lastMamIdSeen), issue a single + // unified catch-up query against the user's own server archive + // (`afterId=_lastMamIdSeen`, no `to=` JID). The server returns all missed + // DM messages across every contact in one paginated stream, which the + // existing _addMessage routing dispatches to the correct chat by JID. + // This replaces the O(N) per-chat fan-out with a single O(1) IQ. + // + // When _lastMamIdSeen is null (fresh install / first session) we fall back + // to the per-chat fan-out so each chat still gets its initial tail. + final anchor = _lastMamIdSeen; + if (anchor != null && anchor.isNotEmpty) { + _startUnifiedDmCatchUp(anchor); + } else { + // Fallback: no anchor yet — fan out per-chat as before. + for (final entry in _messages.entries) { + final bareJid = _bareJid(entry.key); + if (isBookmark(bareJid)) { + continue; + } + if (entry.value.isEmpty) { + continue; + } + // R2.2: skip the catch-up query when MDS already proves we are + // caught up to the displayed marker for this chat. + if (!shouldFetchMamCatchUpForChat( + displayedStanzaId: _displayedStanzaIdByChat[bareJid], + latestLocalMamId: latestMamIdFor(bareJid), + stanzaIdAtLatestMamId: + _stanzaIdAtLatestMamId(bareJid, isRoom: false), + )) { + continue; + } + _startMamCatchUp(bareJid, isRoom: false); } - _startMamCatchUp(bareJid, isRoom: false); } for (final bookmark in _bookmarks) { @@ -7572,13 +7750,156 @@ class XmppService extends ChangeNotifier { final roomMessages = _roomMessages[roomJid]; if (roomMessages == null || roomMessages.isEmpty) { _requestRoomMam(roomJid, max: 25, before: ''); - } else { - _startMamCatchUp(roomJid, isRoom: true); + continue; } + // R2.2: same short-circuit for MUCs. + if (!shouldFetchMamCatchUpForChat( + displayedStanzaId: _displayedStanzaIdByChat[roomJid], + latestLocalMamId: _latestRoomMamIdFor(roomJid), + stanzaIdAtLatestMamId: _stanzaIdAtLatestMamId(roomJid, isRoom: true), + )) { + continue; + } + _startMamCatchUp(roomJid, isRoom: true); } _finishMamSyncIfIdle(); } + /// R2.1: Issue a single unified MAM catch-up query against the user's own + /// server archive, starting after [anchor] (the globally newest MAM id we + /// have seen). On completion the RSM `` id is used to advance + /// [_lastMamIdSeen] so the next session's anchor is up to date. + /// + /// The query is built manually so we can capture the IQ id and register an + /// [IqRouter] response handler — [MessageArchiveManager.queryById] writes + /// the stanza internally and does not expose the id. + void _startUnifiedDmCatchUp(String anchor) { + final connection = _connection; + if (connection == null) { + return; + } + + // Build the MAM IQ: + // + // 50 + final id = AbstractStanza.getRandomId(); + final iq = IqStanza(id, IqStanzaType.SET); + // No toJid — queries the user's own server archive. + + final query = XmppElement()..name = 'query'; + query.addAttribute(XmppAttribute('xmlns', 'urn:xmpp:mam:2')); + query.addAttribute(XmppAttribute('queryid', AbstractStanza.getRandomId())); + + final x = XmppElement()..name = 'x'; + x.addAttribute( + XmppAttribute('xmlns', 'jabber:x:data'), + ); + x.addAttribute(XmppAttribute('type', 'submit')); + + final formType = XmppElement()..name = 'field'; + formType.addAttribute(XmppAttribute('var', 'FORM_TYPE')); + formType.addAttribute(XmppAttribute('type', 'hidden')); + final formTypeValue = XmppElement() + ..name = 'value' + ..textValue = 'urn:xmpp:mam:2'; + formType.addChild(formTypeValue); + x.addChild(formType); + + final afterIdField = XmppElement()..name = 'field'; + afterIdField.addAttribute(XmppAttribute('var', 'after-id')); + final afterIdValue = XmppElement() + ..name = 'value' + ..textValue = anchor; + afterIdField.addChild(afterIdValue); + x.addChild(afterIdField); + + query.addChild(x); + + // RSM: request up to 50 messages per page. + final set = XmppElement()..name = 'set'; + set.addAttribute( + XmppAttribute('xmlns', 'http://jabber.org/protocol/rsm'), + ); + final max = XmppElement() + ..name = 'max' + ..textValue = '50'; + set.addChild(max); + query.addChild(set); + + iq.addChild(query); + + // Register a response handler so we can advance _lastMamIdSeen once the + // server returns the result. + final router = IqRouter.getInstance(connection); + router.registerResponseHandler(id, (response) { + if (response == null) { + return; + } + if (response.type != IqStanzaType.RESULT) { + return; + } + // Parse + final fin = response.children.firstWhere( + (child) => + child.name == 'fin' && + child.getAttribute('xmlns')?.value == 'urn:xmpp:mam:2', + orElse: () => XmppElement(), + ); + if (fin.name != 'fin') { + return; + } + final rsmSet = fin.children.firstWhere( + (child) => + child.name == 'set' && + child.getAttribute('xmlns')?.value == + 'http://jabber.org/protocol/rsm', + orElse: () => XmppElement(), + ); + if (rsmSet.name != 'set') { + return; + } + final lastEl = rsmSet.children.firstWhere( + (child) => child.name == 'last', + orElse: () => XmppElement(), + ); + final lastId = lastEl.name == 'last' ? lastEl.textValue?.trim() : null; + if (lastId != null && lastId.isNotEmpty) { + _bumpLastMamIdSeen(lastId); + } + }); + + connection.writeStanza(iq); + } + + /// Returns the stanza-id of the message in [bareJid]'s message list whose + /// MAM id equals the chat's latest MAM id. Used by R2.2 to compare the + /// displayed marker against the newest local message we have. Returns + /// null when no local message has both a matching MAM id and a non-empty + /// stanza-id. + String? _stanzaIdAtLatestMamId(String bareJid, {required bool isRoom}) { + final normalized = _bareJid(bareJid); + final list = isRoom ? _roomMessages[normalized] : _messages[normalized]; + if (list == null || list.isEmpty) { + return null; + } + final latestMamId = isRoom + ? _latestRoomMamIdFor(normalized) + : latestMamIdFor(normalized); + if (latestMamId == null || latestMamId.isEmpty) { + return null; + } + for (final message in list.reversed) { + if (message.mamId == latestMamId) { + final sid = message.stanzaId; + if (sid != null && sid.isNotEmpty) { + return sid; + } + return null; + } + } + return null; + } + void _seedVcardAvatars(Map base64ByJid) { for (final entry in base64ByJid.entries) { if (entry.value.trim().isEmpty) { @@ -7602,7 +7923,11 @@ class XmppService extends ChangeNotifier { _requestVcardDetails(bareJid, preferName: false); } - void _requestVcardDetails(String bareJid, {required bool preferName}) { + void _requestVcardDetails( + String bareJid, { + required bool preferName, + String? advertisedHash, + }) { final connection = _connection; final storage = _storage; if (connection == null || storage == null) { @@ -7614,6 +7939,19 @@ class XmppService extends ChangeNotifier { if (_vcardRequests.contains(bareJid)) { return; } + // R4.1: Skip the IQ entirely when we already have the bytes cached for + // the advertised photo hash. `preferName == true` callers (e.g. self + // vCard fetch on Ready, or contact list "show details") bypass the cache + // because they want the FN/NICKNAME fields, not just the avatar. + if (!shouldFetchVcardForCache( + bareJid: bareJid, + preferName: preferName, + cachedAvatarBytes: _vcardAvatarBytes, + cachedAvatarState: _vcardAvatarState, + advertisedHash: advertisedHash, + )) { + return; + } _vcardRequests.add(bareJid); final manager = VCardManager.getInstance(connection); manager @@ -7718,13 +8056,12 @@ class XmppService extends ChangeNotifier { } return; } - if (existing == hash && _vcardAvatarBytes.containsKey(bareJid)) { - return; - } - _vcardAvatarState[bareJid] = hash; - storage.storeVcardAvatarState(bareJid, hash); + // The centralised guard inside `_requestVcardDetails` (R4.1) handles the + // "same hash & bytes already cached" case by short-circuiting. Pass the + // advertised hash *before* mutating cached state so the guard can compare + // it against the previously recorded hash and refetch when they differ. _vcardRequests.remove(bareJid); - _requestVcardAvatar(bareJid); + _requestVcardDetails(bareJid, preferName: false, advertisedHash: hash); } Future updateSelfVcard({ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index ace2d09..6b84e8a 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_quic jni ) diff --git a/pubspec.lock b/pubspec.lock index f116235..f92eb81 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,14 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" characters: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" cli_util: dependency: transitive description: @@ -302,6 +310,21 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" + flutter_quic: + dependency: "direct main" + description: + path: "vendor/flutter_quic" + relative: true + source: path + version: "1.0.0" + flutter_rust_bridge: + dependency: "direct overridden" + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" flutter_secure_storage: dependency: "direct main" description: @@ -368,6 +391,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" get_it: dependency: transitive description: @@ -536,6 +567,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -588,18 +627,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -985,10 +1024,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc27083..88790c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: flutter_webrtc: ^1.4.0 permission_handler: ^12.0.1 http: ^1.6.0 + flutter_quic: + path: vendor/flutter_quic dev_dependencies: sentry_dart_plugin: ^3.2.1 @@ -66,6 +68,9 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^6.0.0 +dependency_overrides: + flutter_rust_bridge: 2.11.1 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/shape-cfg.sh b/shape-cfg.sh new file mode 100644 index 0000000..8becdda --- /dev/null +++ b/shape-cfg.sh @@ -0,0 +1,7 @@ +# Remote QUIC endpoint +IPV6="2a02:8010:300a::3" +IPV4="88.98.37.179" +PORT="5224" +IFACE=$(ip route get "$IPV4" | awk '/dev/ {for (i=1;i<=NF;i++) if ($i=="dev") print $(i+1)}') + +echo "[*] Using $IFACE" diff --git a/test/account_record_test.dart b/test/account_record_test.dart new file mode 100644 index 0000000..40836fd --- /dev/null +++ b/test/account_record_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/storage/account_record.dart'; + +void main() { + group('AccountRecord useTcp', () { + test('defaults to true when not specified', () { + final record = AccountRecord( + jid: 'user@example.org', + password: '', + host: '', + port: 5222, + resource: 'wimsy', + rememberPassword: false, + useWebSocket: false, + directTls: false, + wsEndpoint: '', + wsProtocols: const [], + ); + expect(record.useTcp, isTrue); + // Direct TLS default unchanged. + expect(record.directTls, isFalse); + // QUIC default unchanged. + expect(record.useQuic, isTrue); + }); + + test('round-trips through toMap/fromMap', () { + final record = AccountRecord( + jid: 'user@example.org', + password: '', + host: '', + port: 5222, + resource: 'wimsy', + rememberPassword: false, + useWebSocket: false, + directTls: false, + wsEndpoint: '', + wsProtocols: const [], + useTcp: false, + ); + final restored = AccountRecord.fromMap(record.toMap()); + expect(restored, isNotNull); + expect(restored!.useTcp, isFalse); + }); + + test('fromMap defaults useTcp to true for legacy records', () { + // Older persisted records have no `useTcp` key. They must default to + // true so existing accounts continue to allow plain-TCP connections + // exactly as they did before this option was introduced. + final restored = AccountRecord.fromMap({ + 'jid': 'user@example.org', + 'password': '', + 'host': '', + 'port': 5222, + 'resource': 'wimsy', + 'rememberPassword': false, + 'useWebSocket': false, + 'directTls': false, + 'wsEndpoint': '', + 'wsProtocols': const [], + }); + expect(restored, isNotNull); + expect(restored!.useTcp, isTrue); + }); + + test('fromMap honours explicit useTcp=false', () { + final restored = AccountRecord.fromMap({ + 'jid': 'user@example.org', + 'password': '', + 'host': '', + 'port': 5222, + 'resource': 'wimsy', + 'rememberPassword': false, + 'useWebSocket': false, + 'directTls': true, + 'wsEndpoint': '', + 'wsProtocols': const [], + 'useTcp': false, + }); + expect(restored, isNotNull); + expect(restored!.useTcp, isFalse); + expect(restored.directTls, isTrue); + }); + }); +} diff --git a/test/chat_message_mutations_test.dart b/test/chat_message_mutations_test.dart index 265351b..8928195 100644 --- a/test/chat_message_mutations_test.dart +++ b/test/chat_message_mutations_test.dart @@ -120,6 +120,186 @@ void main() { }); }); + test('applyCorrectionInList returns false when replaceId not found', () { + final list = [ + _message( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'original', + id: 'm-existing', + ), + ]; + + final applied = ChatMessageMutations.applyCorrectionInList( + list, + sender: 'alice@example.com', + replaceId: 'no-such-id', + newBody: 'updated', + rawXml: '', + timestamp: DateTime.utc(2026, 1, 2), + matchSenderBare: false, + bareJid: (jid) => jid.split('/').first, + ); + + expect(applied, isFalse); + expect(list.single.body, 'original'); + }); + + test( + 'applyCorrectionInList returns false when sender bare JID does not match', + () { + final list = [ + _message( + from: 'alice@example.com/phone', + to: 'bob@example.com', + body: 'original', + id: 'm-sender-mismatch', + ), + ]; + + final applied = ChatMessageMutations.applyCorrectionInList( + list, + sender: 'eve@example.com/laptop', + replaceId: 'm-sender-mismatch', + newBody: 'spoofed', + rawXml: '', + timestamp: DateTime.utc(2026, 1, 2), + matchSenderBare: true, + bareJid: (jid) => jid.split('/').first, + ); + + expect(applied, isFalse); + expect(list.single.body, 'original'); + }, + ); + + test( + 'applyCorrectionInList returns false when exact sender does not match ' + 'and matchSenderBare is false', + () { + final list = [ + _message( + from: 'alice@example.com/phone', + to: 'bob@example.com', + body: 'original', + id: 'm-exact-mismatch', + ), + ]; + + // Same bare JID but different resource — must not match when + // matchSenderBare is false. + final applied = ChatMessageMutations.applyCorrectionInList( + list, + sender: 'alice@example.com/laptop', + replaceId: 'm-exact-mismatch', + newBody: 'updated', + rawXml: '', + timestamp: DateTime.utc(2026, 1, 2), + matchSenderBare: false, + bareJid: (jid) => jid.split('/').first, + ); + + expect(applied, isFalse); + expect(list.single.body, 'original'); + }, + ); + + test('applyCorrectionInList applies correction to most-recent matching message', + () { + // Two messages with the same ID (shouldn't happen in practice, but the + // implementation iterates from the end — verify it picks the last one). + final list = [ + _message( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'first', + id: 'm-dup', + ), + _message( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'second', + id: 'm-dup', + ), + ]; + + ChatMessageMutations.applyCorrectionInList( + list, + sender: 'alice@example.com', + replaceId: 'm-dup', + newBody: 'corrected', + rawXml: '', + timestamp: DateTime.utc(2026, 1, 2), + matchSenderBare: false, + bareJid: (jid) => jid.split('/').first, + ); + + expect(list[0].body, 'first'); + expect(list[1].body, 'corrected'); + }); + + test('updateReactionsInList returns false when stanza ID not found', () { + final list = [ + _message( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'hello', + id: 'm5', + stanzaId: 's5', + ), + ]; + + final changed = ChatMessageMutations.updateReactionsInList( + list, + 'alice@example.com', + ReactionUpdate('no-such-stanza', ['👍']), + ); + + expect(changed, isFalse); + expect(list.single.reactions, isNull); + }); + + test('updateReactionsInList returns false for empty sender', () { + final list = [ + _message( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'hello', + id: 'm6', + stanzaId: 's6', + ), + ]; + + final changed = ChatMessageMutations.updateReactionsInList( + list, + '', + ReactionUpdate('s6', ['👍']), + ); + + expect(changed, isFalse); + }); + + test('updateReactionsInList matches by messageId when stanzaId absent', () { + final list = [ + _message( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'hello', + id: 'm7', + // no stanzaId + ), + ]; + + final changed = ChatMessageMutations.updateReactionsInList( + list, + 'bob@example.com', + ReactionUpdate('m7', ['❤️']), + ); + + expect(changed, isTrue); + expect(list.single.reactions, {'❤️': ['bob@example.com']}); + }); + test( 'updateReactionsInList removes sender reactions when reaction list is empty', () { diff --git a/test/chat_message_test.dart b/test/chat_message_test.dart index 24faa66..eb106fb 100644 --- a/test/chat_message_test.dart +++ b/test/chat_message_test.dart @@ -63,6 +63,261 @@ void main() { expect(roundtrip, isNull); }); + // ── copyWith ──────────────────────────────────────────────────────────────── + + group('copyWith', () { + // A fully-populated message used as the baseline for copyWith tests. + final base = ChatMessage( + from: 'alice@example.com', + to: 'bob@example.com', + body: 'hello', + timestamp: DateTime.utc(2024, 1, 1), + outgoing: false, + messageId: 'msg-base', + mamId: 'mam-1', + stanzaId: 'stanza-1', + oobUrl: 'https://example.com/img.png', + oobDescription: 'An image', + rawXml: '', + inviteRoomJid: 'room@example.com', + inviteReason: 'Join us', + invitePassword: 'secret', + fileTransferId: 'ft-1', + fileName: 'photo.png', + fileSize: 1024, + fileMime: 'image/png', + fileBytes: 512, + fileState: 'in_progress', + edited: false, + editedAt: null, + reactions: const {'👍': ['alice@example.com']}, + replyToId: 'orig-1', + replyToJid: 'alice@example.com', + replyFallback: '> hello', + acked: false, + receiptReceived: false, + displayed: false, + ); + + test('returns identical field values when called with no arguments', () { + final copy = base.copyWith(); + expect(copy.from, base.from); + expect(copy.to, base.to); + expect(copy.body, base.body); + expect(copy.timestamp, base.timestamp); + expect(copy.outgoing, base.outgoing); + expect(copy.messageId, base.messageId); + expect(copy.mamId, base.mamId); + expect(copy.stanzaId, base.stanzaId); + expect(copy.oobUrl, base.oobUrl); + expect(copy.oobDescription, base.oobDescription); + expect(copy.rawXml, base.rawXml); + expect(copy.inviteRoomJid, base.inviteRoomJid); + expect(copy.inviteReason, base.inviteReason); + expect(copy.invitePassword, base.invitePassword); + expect(copy.fileTransferId, base.fileTransferId); + expect(copy.fileName, base.fileName); + expect(copy.fileSize, base.fileSize); + expect(copy.fileMime, base.fileMime); + expect(copy.fileBytes, base.fileBytes); + expect(copy.fileState, base.fileState); + expect(copy.edited, base.edited); + expect(copy.editedAt, base.editedAt); + expect(copy.reactions, base.reactions); + expect(copy.replyToId, base.replyToId); + expect(copy.replyToJid, base.replyToJid); + expect(copy.replyFallback, base.replyFallback); + expect(copy.acked, base.acked); + expect(copy.receiptReceived, base.receiptReceived); + expect(copy.displayed, base.displayed); + }); + + test('overrides scalar non-nullable fields', () { + final copy = base.copyWith( + from: 'carol@example.com', + to: 'dave@example.com', + body: 'updated body', + outgoing: true, + edited: true, + acked: true, + receiptReceived: true, + displayed: true, + ); + expect(copy.from, 'carol@example.com'); + expect(copy.to, 'dave@example.com'); + expect(copy.body, 'updated body'); + expect(copy.outgoing, isTrue); + expect(copy.edited, isTrue); + expect(copy.acked, isTrue); + expect(copy.receiptReceived, isTrue); + expect(copy.displayed, isTrue); + // Unrelated fields must be unchanged. + expect(copy.messageId, base.messageId); + expect(copy.mamId, base.mamId); + }); + + test('overrides nullable String fields via sentinel', () { + final copy = base.copyWith( + messageId: 'new-id', + mamId: 'new-mam', + stanzaId: 'new-stanza', + oobUrl: 'https://example.com/new.png', + oobDescription: 'New description', + rawXml: '', + fileTransferId: 'ft-2', + fileName: 'video.mp4', + fileMime: 'video/mp4', + fileState: 'complete', + replyToId: 'orig-2', + replyToJid: 'carol@example.com', + replyFallback: '> updated', + ); + expect(copy.messageId, 'new-id'); + expect(copy.mamId, 'new-mam'); + expect(copy.stanzaId, 'new-stanza'); + expect(copy.oobUrl, 'https://example.com/new.png'); + expect(copy.oobDescription, 'New description'); + expect(copy.rawXml, ''); + expect(copy.fileTransferId, 'ft-2'); + expect(copy.fileName, 'video.mp4'); + expect(copy.fileMime, 'video/mp4'); + expect(copy.fileState, 'complete'); + expect(copy.replyToId, 'orig-2'); + expect(copy.replyToJid, 'carol@example.com'); + expect(copy.replyFallback, '> updated'); + }); + + test('overrides nullable int fields via sentinel', () { + final copy = base.copyWith(fileSize: 2048, fileBytes: 1024); + expect(copy.fileSize, 2048); + expect(copy.fileBytes, 1024); + }); + + test('clears nullable fields to null via explicit null (sentinel pattern)', + () { + final copy = base.copyWith( + messageId: null, + mamId: null, + stanzaId: null, + oobUrl: null, + oobDescription: null, + rawXml: null, + inviteRoomJid: null, + inviteReason: null, + invitePassword: null, + fileTransferId: null, + fileName: null, + fileSize: null, + fileMime: null, + fileBytes: null, + fileState: null, + editedAt: null, + reactions: null, + replyToId: null, + replyToJid: null, + replyFallback: null, + ); + expect(copy.messageId, isNull); + expect(copy.mamId, isNull); + expect(copy.stanzaId, isNull); + expect(copy.oobUrl, isNull); + expect(copy.oobDescription, isNull); + expect(copy.rawXml, isNull); + expect(copy.inviteRoomJid, isNull); + expect(copy.inviteReason, isNull); + expect(copy.invitePassword, isNull); + expect(copy.fileTransferId, isNull); + expect(copy.fileName, isNull); + expect(copy.fileSize, isNull); + expect(copy.fileMime, isNull); + expect(copy.fileBytes, isNull); + expect(copy.fileState, isNull); + expect(copy.editedAt, isNull); + expect(copy.reactions, isNull); + expect(copy.replyToId, isNull); + expect(copy.replyToJid, isNull); + expect(copy.replyFallback, isNull); + // Non-nullable fields must be unchanged. + expect(copy.from, base.from); + expect(copy.body, base.body); + }); + + test('updates reactions map independently', () { + final newReactions = { + '❤️': ['bob@example.com'], + '👍': ['alice@example.com', 'carol@example.com'], + }; + final copy = base.copyWith(reactions: newReactions); + expect(copy.reactions, newReactions); + // Original must be unaffected. + expect(base.reactions, const {'👍': ['alice@example.com']}); + }); + + test('sets editedAt when marking a message as edited', () { + final editTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + final copy = base.copyWith(edited: true, editedAt: editTime); + expect(copy.edited, isTrue); + expect(copy.editedAt, editTime); + }); + + test('does not mutate the original message', () { + base.copyWith(body: 'mutated', acked: true, mamId: null); + expect(base.body, 'hello'); + expect(base.acked, isFalse); + expect(base.mamId, 'mam-1'); + }); + }); + + // ── Notification ID stability ──────────────────────────────────────────────── + + group('notification ID stability', () { + // The notification ID formula used in main.dart: + // bareJid.hashCode.abs() % (1 << 31) + // It must be deterministic (same JID → same ID) and within Android's + // valid notification ID range [0, 2^31 - 1]. + + int notifId(String bareJid) => bareJid.hashCode.abs() % (1 << 31); + + test('same JID always produces the same notification ID', () { + const jid = 'alice@example.com'; + expect(notifId(jid), notifId(jid)); + }); + + test('notification ID is non-negative', () { + for (final jid in [ + 'alice@example.com', + 'bob@xmpp.org', + 'room@conference.example.com', + 'a@b', + ]) { + expect(notifId(jid), isNonNegative, + reason: 'ID for $jid must be non-negative'); + } + }); + + test('notification ID is within 32-bit signed range', () { + for (final jid in [ + 'alice@example.com', + 'bob@xmpp.org', + 'room@conference.example.com', + ]) { + expect(notifId(jid), lessThan(1 << 31), + reason: 'ID for $jid must be < 2^31'); + } + }); + + test('different JIDs produce different notification IDs', () { + final ids = { + notifId('alice@example.com'), + notifId('bob@example.com'), + notifId('carol@xmpp.org'), + }; + // All three should be distinct (hash collisions are astronomically + // unlikely for these short, well-separated strings). + expect(ids.length, 3); + }); + }); + test('ChatMessage accepts invite without body when raw XML present', () { final roundtrip = ChatMessage.fromMap({ 'from': 'alice@example.com', diff --git a/test/displayed_sync_pending_test.dart b/test/displayed_sync_pending_test.dart new file mode 100644 index 0000000..c2056a9 --- /dev/null +++ b/test/displayed_sync_pending_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/storage/storage_service.dart'; + +/// In-memory fake. We override only the methods touched by R1.3 so we can +/// exercise the load/store semantics without spinning up Hive. +class FakeStorageService extends StorageService { + final Map _pending = {}; + final Map _sync = {}; + + @override + Map loadDisplayedSync() => Map.from(_sync); + + @override + Future storeDisplayedSync(Map sync) async { + _sync + ..clear() + ..addAll(sync); + } + + @override + Map loadDisplayedSyncPending() => + Map.from(_pending); + + @override + Future storeDisplayedSyncPending(Map pending) async { + _pending + ..clear() + ..addAll(pending); + } + + @override + Future clearDisplayedSync() async { + _sync.clear(); + _pending.clear(); + } +} + +void main() { + group('R1.3: StorageService displayed_sync_pending round-trip', () { + test('empty by default', () { + final s = FakeStorageService(); + expect(s.loadDisplayedSyncPending(), isEmpty); + }); + + test('store then load returns the same map', () async { + final s = FakeStorageService(); + await s.storeDisplayedSyncPending({ + 'xsf@muc.xmpp.org': 'sid-1', + 'alice@example.com': 'sid-2', + }); + expect(s.loadDisplayedSyncPending(), { + 'xsf@muc.xmpp.org': 'sid-1', + 'alice@example.com': 'sid-2', + }); + }); + + test('overwrite replaces the whole map', () async { + final s = FakeStorageService(); + await s.storeDisplayedSyncPending({'a': '1', 'b': '2'}); + await s.storeDisplayedSyncPending({'b': '3'}); + expect(s.loadDisplayedSyncPending(), {'b': '3'}); + }); + + test('clearDisplayedSync wipes both displayed_sync and pending', () async { + final s = FakeStorageService(); + await s.storeDisplayedSync({'a': 'sid-a'}); + await s.storeDisplayedSyncPending({'a': 'sid-pending'}); + await s.clearDisplayedSync(); + expect(s.loadDisplayedSync(), isEmpty); + expect(s.loadDisplayedSyncPending(), isEmpty); + }); + }); +} diff --git a/test/last_mam_id_seen_test.dart b/test/last_mam_id_seen_test.dart new file mode 100644 index 0000000..820c114 --- /dev/null +++ b/test/last_mam_id_seen_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/storage/storage_service.dart'; + +/// In-memory fake. Storage methods touched by R2.1 are overridden to +/// avoid spinning up Hive in unit tests. +class FakeStorageService extends StorageService { + String? _anchor; + + @override + String? loadLastMamIdSeen() => _anchor; + + @override + Future storeLastMamIdSeen(String mamId) async { + if (mamId.isEmpty) { + return; + } + _anchor = mamId; + } + + @override + Future clearLastMamIdSeen() async { + _anchor = null; + } +} + +void main() { + group('R2.1: StorageService last_mam_id_seen anchor', () { + test('null when never written', () { + final s = FakeStorageService(); + expect(s.loadLastMamIdSeen(), isNull); + }); + + test('store then load round-trips', () async { + final s = FakeStorageService(); + await s.storeLastMamIdSeen('mam-id-42'); + expect(s.loadLastMamIdSeen(), 'mam-id-42'); + }); + + test('empty store is a no-op', () async { + final s = FakeStorageService(); + await s.storeLastMamIdSeen('mam-id-1'); + await s.storeLastMamIdSeen(''); + expect(s.loadLastMamIdSeen(), 'mam-id-1'); + }); + + test('clear removes the anchor', () async { + final s = FakeStorageService(); + await s.storeLastMamIdSeen('mam-id-1'); + await s.clearLastMamIdSeen(); + expect(s.loadLastMamIdSeen(), isNull); + }); + }); +} diff --git a/test/mam_merge_engine_test.dart b/test/mam_merge_engine_test.dart index 4baf676..2d78725 100644 --- a/test/mam_merge_engine_test.dart +++ b/test/mam_merge_engine_test.dart @@ -101,6 +101,108 @@ void main() { expect(list.single.mamId, 'existing'); }); + test('does not merge by heuristic when body differs', () { + final list = [ + _message(timestamp: DateTime.utc(2026, 1, 1, 10), body: 'hello'), + ]; + + final merged = mergeMamIdsIntoExisting( + list, + from: 'alice@example.com', + to: 'bob@example.com', + body: 'different body', + outgoing: false, + timestamp: DateTime.utc(2026, 1, 1, 10, 0, 30), + mamId: 'mam-x', + stanzaId: 'sid-x', + ); + + expect(merged, isFalse); + expect(list.single.mamId, isNull); + }); + + test('does not merge by heuristic when sender differs', () { + final list = [ + _message( + timestamp: DateTime.utc(2026, 1, 1, 10), + from: 'alice@example.com', + ), + ]; + + final merged = mergeMamIdsIntoExisting( + list, + from: 'eve@example.com', + to: 'bob@example.com', + body: 'hello', + outgoing: false, + timestamp: DateTime.utc(2026, 1, 1, 10, 0, 30), + mamId: 'mam-y', + stanzaId: 'sid-y', + ); + + expect(merged, isFalse); + expect(list.single.mamId, isNull); + }); + + test('merges oobUrl and oobDescription via message-id match', () { + final list = [ + _message( + timestamp: DateTime.utc(2026, 1, 1, 10), + messageId: 'msg-oob', + ), + ]; + + final merged = mergeMamIdsIntoExisting( + list, + from: 'alice@example.com', + to: 'bob@example.com', + body: 'hello', + outgoing: false, + timestamp: DateTime.utc(2026, 1, 1, 10, 0, 5), + messageId: 'msg-oob', + mamId: 'mam-oob', + stanzaId: 'sid-oob', + oobDescription: 'A photo', + rawXml: '', + ); + + expect(merged, isTrue); + expect(list.single.mamId, 'mam-oob'); + expect(list.single.oobDescription, 'A photo'); + expect(list.single.rawXml, ''); + }); + + test( + 'does not merge by message-id when both mamId and stanzaId already set', + () { + final list = [ + _message( + timestamp: DateTime.utc(2026, 1, 1, 10), + messageId: 'msg-full', + mamId: 'existing-mam', + stanzaId: 'existing-sid', + ), + ]; + + final merged = mergeMamIdsIntoExisting( + list, + from: 'alice@example.com', + to: 'bob@example.com', + body: 'hello', + outgoing: false, + timestamp: DateTime.utc(2026, 1, 1, 10, 0, 5), + messageId: 'msg-full', + mamId: 'new-mam', + stanzaId: 'new-sid', + ); + + // Message-id matched but both IDs already present — no overwrite. + expect(merged, isFalse); + expect(list.single.mamId, 'existing-mam'); + expect(list.single.stanzaId, 'existing-sid'); + }, + ); + test('does not merge when message is outside merge window', () { final list = [ _message(timestamp: DateTime.utc(2026, 1, 1, 10), messageId: 'local-1'), diff --git a/test/mam_reliability_regression_test.dart b/test/mam_reliability_regression_test.dart index 95ff9b6..3a469c7 100644 --- a/test/mam_reliability_regression_test.dart +++ b/test/mam_reliability_regression_test.dart @@ -44,11 +44,12 @@ void main() { stanzaId: 'sid-middle', ); - // Current behavior: the incoming archived message is folded into - // the existing local row, so list growth is suppressed. + // The incoming archived message is folded into the existing local row, + // so list growth is suppressed. The original messageId is preserved + // (copyWith keeps all fields not explicitly overridden). expect(merged, isTrue); expect(list, hasLength(1)); - expect(list.single.messageId, isNull); + expect(list.single.messageId, 'local-1'); expect(list.single.mamId, 'mam-middle'); expect(list.single.stanzaId, 'sid-middle'); }, diff --git a/test/pep_caps_manager_test.dart b/test/pep_caps_manager_test.dart index a73063b..43afcf2 100644 --- a/test/pep_caps_manager_test.dart +++ b/test/pep_caps_manager_test.dart @@ -40,11 +40,27 @@ class FakePepManager extends PepManager { } class FakeStorageService extends StorageService { + final Map> entityCaps = {}; + @override Map loadAvatarMetadata() => {}; @override Map loadAvatarBlobs() => {}; + + @override + Map> loadEntityCaps() => + Map>.from(entityCaps); + + @override + Future storeEntityCaps(String capsKey, Set features) async { + entityCaps.putIfAbsent(capsKey, () => Set.from(features)); + } + + @override + Future clearEntityCaps() async { + entityCaps.clear(); + } } PresenceStanza _presenceWithCaps({required String fromFullJid, required String node, required String ver}) { @@ -169,4 +185,137 @@ void main() { expect(features, isNotNull); expect(features!.contains('urn:xmpp:jingle:1'), isTrue); }); + + group('R5: persisted entity-caps cache', () { + test('disco#info result is persisted to storage', () async { + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final storage = FakeStorageService(); + final pep = FakePepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + final caps = PepCapsManager( + connection: connection, + pepManager: pep, + storage: storage, + ); + + caps.handleStanza( + _presenceWithCaps( + fromFullJid: 'alice@example.com/r', + node: 'https://example.com/caps', + ver: 'PERSIST1', + ), + ); + final iq = connection.written.last as IqStanza; + caps.handleStanza( + _discoInfoResult( + id: iq.id!, + capsKey: 'https://example.com/caps#PERSIST1', + feature: 'urn:xmpp:jingle:1', + ), + ); + + // Allow the unawaited storeEntityCaps future to complete. + await Future.delayed(Duration.zero); + + expect( + storage.entityCaps['https://example.com/caps#PERSIST1'], + contains('urn:xmpp:jingle:1'), + ); + }); + + test( + 'caps seeded from storage suppress the disco#info IQ for known node#ver', + () { + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final storage = FakeStorageService() + ..entityCaps['https://example.com/caps#KNOWN'] = { + 'urn:xmpp:jingle:1', + }; + final pep = FakePepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + final caps = PepCapsManager( + connection: connection, + pepManager: pep, + storage: storage, + ); + + caps.handleStanza( + _presenceWithCaps( + fromFullJid: 'alice@example.com/r', + node: 'https://example.com/caps', + ver: 'KNOWN', + ), + ); + + // The cache hit short-circuits before any disco#info IQ is sent. + expect(connection.written, isEmpty); + // Features are immediately available for the bare JID. + expect( + caps.featuresForBareJid('alice@example.com'), + contains('urn:xmpp:jingle:1'), + ); + }, + ); + + test( + 'unknown node#ver still triggers disco#info even with seeded cache', + () { + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final storage = FakeStorageService() + ..entityCaps['https://example.com/caps#KNOWN'] = { + 'urn:xmpp:jingle:1', + }; + final pep = FakePepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + final caps = PepCapsManager( + connection: connection, + pepManager: pep, + storage: storage, + ); + + caps.handleStanza( + _presenceWithCaps( + fromFullJid: 'bob@example.com/r', + node: 'https://example.com/caps', + ver: 'UNSEEN', + ), + ); + + expect(connection.written, isNotEmpty); + }, + ); + }); } diff --git a/test/pep_manager_test.dart b/test/pep_manager_test.dart index 6b0d504..c755d6e 100644 --- a/test/pep_manager_test.dart +++ b/test/pep_manager_test.dart @@ -26,6 +26,13 @@ class FakeStorageService extends StorageService { Future storeAvatarBlob(String hash, String base64Data) async { blobs[hash] = base64Data; } + + @override + Future replaceAvatarBlobs(Map next) async { + blobs + ..clear() + ..addAll(next); + } } class TestConnection extends Connection { @@ -150,4 +157,231 @@ void main() { expect(pep.avatarHashFor('alice@example.com'), 'hash123'); }); + + group('R3.3 negative cache for "no PEP avatar"', () { + test('error response to metadata IQ persists noPepAvatar sentinel', () { + final storage = FakeStorageService(); + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + var updates = 0; + final pep = PepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () => updates++, + ); + + pep.requestMetadataIfMissing('test1@example.com'); + // The IQ went out; capture its id from the wire so we can build a + // matching error response. + final outgoing = connection.lastWrittenStanza; + expect(outgoing, isA()); + final outgoingId = (outgoing as IqStanza).id!; + + final errorReply = IqStanza(outgoingId, IqStanzaType.ERROR); + pep.handleStanza(errorReply); + + // We now know this JID has no PEP avatar. + expect(pep.isKnownToHaveNoPepAvatar('test1@example.com'), isTrue); + // Persisted to disk so the next session won't retry. + expect( + storage.metadata['test1@example.com']?.isNoPepAvatar ?? false, + isTrue, + ); + // No-op for callers who only care about avatar bytes / hash. + expect(pep.avatarBytesFor('test1@example.com'), isNull); + expect(pep.avatarHashFor('test1@example.com'), isNull); + // Listener was notified once for the negative cache write. + expect(updates, 1); + }); + + test( + 'requestMetadataIfMissing skips IQ when sentinel is already cached', + () { + final storage = FakeStorageService(); + // Pre-seed the sentinel (e.g. from a previous session that cached it + // to disk). + storage.metadata['test1@example.com'] = AvatarMetadata.noPepAvatar(); + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final pep = PepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + + pep.requestMetadataIfMissing('test1@example.com'); + // No IQ should have been written: the in-memory cache already + // contains the sentinel, so requestMetadataIfMissing short-circuits. + expect(connection.lastWrittenStanza, isNull); + }, + ); + + test( + 'real metadata event clears the sentinel on next pubsub update', + () { + final storage = FakeStorageService(); + // Pre-seed the sentinel. + storage.metadata['test1@example.com'] = AvatarMetadata.noPepAvatar(); + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final pep = PepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + + // Now an event arrives advertising a real avatar. + final event = _buildMetadataEvent( + fromJid: 'test1@example.com', + hash: 'real-hash', + ); + pep.handleStanza(event); + + expect(pep.isKnownToHaveNoPepAvatar('test1@example.com'), isFalse); + expect(pep.avatarHashFor('test1@example.com'), 'real-hash'); + }, + ); + + test('AvatarMetadata.noPepAvatar round-trips through fromMap', () { + final original = AvatarMetadata.noPepAvatar( + updatedAt: DateTime.utc(2026, 5, 1, 12, 30), + ); + final restored = AvatarMetadata.fromMap(original.toMap()); + expect(restored, isNotNull); + expect(restored!.isNoPepAvatar, isTrue); + expect(restored.hash, AvatarMetadata.noPepAvatarHash); + expect(restored.updatedAt, DateTime.utc(2026, 5, 1, 12, 30)); + }); + + test( + 'gcUnreferencedAvatarBlobs (R3.1) drops blobs not referenced by metadata', + () async { + final storage = FakeStorageService(); + // Two blobs cached; only one is referenced by current metadata. + storage.metadata['alice@example.com'] = AvatarMetadata( + hash: 'hash-alive', + mimeType: 'image/png', + bytes: 4, + updatedAt: DateTime.utc(2024, 1, 1), + ); + storage.blobs['hash-alive'] = 'AAAA'; + storage.blobs['hash-stale'] = 'BBBB'; + // A noPepAvatar sentinel must NOT keep its (sentinel) hash alive. + storage.metadata['bob@example.com'] = AvatarMetadata.noPepAvatar(); + storage.blobs[AvatarMetadata.noPepAvatarHash] = 'CCCC'; + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final pep = PepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + // Constructor invokes the GC; allow any unawaited persist future. + await Future.delayed(Duration.zero); + + expect(storage.blobs.keys, ['hash-alive']); + expect(pep.avatarBytesFor('alice@example.com'), [0, 0, 0]); + // Sentinel-only contacts still report no avatar. + expect(pep.avatarBytesFor('bob@example.com'), isNull); + }, + ); + + test( + 'gcUnreferencedAvatarBlobs (R3.1) is idempotent and a no-op on a clean cache', + () async { + final storage = FakeStorageService(); + storage.metadata['alice@example.com'] = AvatarMetadata( + hash: 'hash-alive', + mimeType: 'image/png', + bytes: 4, + updatedAt: DateTime.utc(2024, 1, 1), + ); + storage.blobs['hash-alive'] = 'AAAA'; + final account = XmppAccountSettings( + 'test', + 'user', + 'example.com', + 'pass', + 5222, + ); + final connection = TestConnection(account); + final pep = PepManager( + connection: connection, + storage: storage, + selfBareJid: 'user@example.com', + onUpdate: () {}, + ); + await Future.delayed(Duration.zero); + + // Second pass should not change anything. + final removed = pep.gcUnreferencedAvatarBlobs(); + expect(removed, 0); + expect(storage.blobs.keys, ['hash-alive']); + }, + ); + + test( + 'AvatarMetadata.fromMap still rejects malformed legitimate entries', + () { + // empty hash + expect( + AvatarMetadata.fromMap({ + 'hash': '', + 'mimeType': 'image/png', + 'bytes': 4, + 'updatedAt': DateTime.utc(2026).toIso8601String(), + }), + isNull, + ); + // empty mime + expect( + AvatarMetadata.fromMap({ + 'hash': 'abc', + 'mimeType': '', + 'bytes': 4, + 'updatedAt': DateTime.utc(2026).toIso8601String(), + }), + isNull, + ); + // zero bytes + expect( + AvatarMetadata.fromMap({ + 'hash': 'abc', + 'mimeType': 'image/png', + 'bytes': 0, + 'updatedAt': DateTime.utc(2026).toIso8601String(), + }), + isNull, + ); + }, + ); + }); } diff --git a/test/preferences_service_test.dart b/test/preferences_service_test.dart new file mode 100644 index 0000000..1849efc --- /dev/null +++ b/test/preferences_service_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:wimsy/storage/preferences_service.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + Future load() => PreferencesService.load(); + + group('sentryOptIn', () { + test('defaults to false', () async { + final prefs = await load(); + expect(prefs.sentryOptIn, isFalse); + }); + + test('round-trips true', () async { + final prefs = await load(); + await prefs.setSentryOptIn(true); + expect(prefs.sentryOptIn, isTrue); + }); + + test('round-trips false after true', () async { + final prefs = await load(); + await prefs.setSentryOptIn(true); + await prefs.setSentryOptIn(false); + expect(prefs.sentryOptIn, isFalse); + }); + }); + + group('pinIgnored', () { + test('defaults to false', () async { + final prefs = await load(); + expect(prefs.pinIgnored, isFalse); + }); + + test('round-trips true', () async { + final prefs = await load(); + await prefs.setPinIgnored(true); + expect(prefs.pinIgnored, isTrue); + }); + }); + + group('lastJid', () { + test('defaults to null', () async { + final prefs = await load(); + expect(prefs.lastJid, isNull); + }); + + test('round-trips a JID string', () async { + final prefs = await load(); + await prefs.setLastJid('alice@example.com'); + expect(prefs.lastJid, 'alice@example.com'); + }); + }); + + group('audioInputId', () { + test('defaults to null', () async { + final prefs = await load(); + expect(prefs.audioInputId, isNull); + }); + + test('round-trips a device ID', () async { + final prefs = await load(); + await prefs.setAudioInputId('mic-device-1'); + expect(prefs.audioInputId, 'mic-device-1'); + }); + }); + + group('videoInputId', () { + test('defaults to null', () async { + final prefs = await load(); + expect(prefs.videoInputId, isNull); + }); + + test('round-trips a device ID', () async { + final prefs = await load(); + await prefs.setVideoInputId('cam-device-2'); + expect(prefs.videoInputId, 'cam-device-2'); + }); + }); +} diff --git a/test/quic_endpoint_plan_test.dart b/test/quic_endpoint_plan_test.dart new file mode 100644 index 0000000..7b13e5a --- /dev/null +++ b/test/quic_endpoint_plan_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/xmpp/quic_endpoint_plan.dart'; +import 'package:wimsy/xmpp/srv_target.dart'; + +void main() { + test('builds QUIC endpoints from SRV candidates in provided order', () { + final endpoints = buildQuicEndpointPlan( + domain: 'example.com', + srvCandidates: [ + XmppSrvTarget( + host: 'quic1.example.com', + port: 443, + priority: 10, + weight: 20, + directTls: false, + ), + XmppSrvTarget( + host: 'quic2.example.com', + port: 4433, + priority: 20, + weight: 5, + directTls: false, + ), + ], + ); + + expect(endpoints, hasLength(2)); + expect(endpoints[0].host, 'quic1.example.com'); + expect(endpoints[0].port, 443); + expect(endpoints[0].tlsHost, 'example.com'); + expect(endpoints[1].host, 'quic2.example.com'); + expect(endpoints[1].port, 4433); + }); + + test('returns empty list when no QUIC SRV candidates are available', () { + final endpoints = buildQuicEndpointPlan( + domain: 'example.com', + srvCandidates: const [], + ); + expect(endpoints, isEmpty); + }); +} diff --git a/test/quic_happy_eyeballs_test.dart b/test/quic_happy_eyeballs_test.dart new file mode 100644 index 0000000..d89d541 --- /dev/null +++ b/test/quic_happy_eyeballs_test.dart @@ -0,0 +1,69 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/xmpp/quic_xmpp_socket.dart'; + +void main() { + group('buildQuicHappyEyeballsPlan', () { + test('interleaves with IPv6 preference when first record is IPv6', () { + final addresses = [ + InternetAddress('2001:db8::1'), + InternetAddress('2001:db8::2'), + InternetAddress('192.0.2.1'), + InternetAddress('192.0.2.2'), + ]; + + final plan = buildQuicHappyEyeballsPlan(addresses); + + expect( + plan.map((address) => address.address).toList(), + equals(['2001:db8::1', '192.0.2.1', '2001:db8::2', '192.0.2.2']), + ); + }); + + test('interleaves with IPv4 preference when first record is IPv4', () { + final addresses = [ + InternetAddress('192.0.2.1'), + InternetAddress('2001:db8::1'), + InternetAddress('192.0.2.2'), + InternetAddress('2001:db8::2'), + ]; + + final plan = buildQuicHappyEyeballsPlan(addresses); + + expect( + plan.map((address) => address.address).toList(), + equals(['192.0.2.1', '2001:db8::1', '192.0.2.2', '2001:db8::2']), + ); + }); + }); + + group('QuicCapableXmppSocket connect tuning defaults', () { + test('defaults expose increased timeout and 3 retry attempts', () { + final socket = QuicCapableXmppSocket(); + + // Per-attempt timeout was bumped from 3s to 5s so QUIC handshakes + // (which can include packet loss + RTT * 2 for the TLS round trip) + // get a realistic budget on both IPv4 and IPv6. + expect(socket.quicConnectTimeout, const Duration(seconds: 5)); + + // Happy Eyeballs stagger between candidate launches. + expect(socket.happyEyeballsDelay, const Duration(milliseconds: 250)); + + // The whole Happy Eyeballs round is retried up to 3 times before + // we give up and fall back to TCP. + expect(socket.quicConnectMaxAttempts, 3); + }); + + test('explicit overrides are honoured', () { + final socket = QuicCapableXmppSocket( + quicConnectTimeout: const Duration(seconds: 10), + happyEyeballsDelay: const Duration(milliseconds: 100), + quicConnectMaxAttempts: 7, + ); + expect(socket.quicConnectTimeout, const Duration(seconds: 10)); + expect(socket.happyEyeballsDelay, const Duration(milliseconds: 100)); + expect(socket.quicConnectMaxAttempts, 7); + }); + }); +} diff --git a/test/quic_stream_routing_test.dart b/test/quic_stream_routing_test.dart new file mode 100644 index 0000000..db6af05 --- /dev/null +++ b/test/quic_stream_routing_test.dart @@ -0,0 +1,132 @@ +import 'package:flutter_quic/flutter_quic.dart' show connectionAcceptBi; +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/xmpp/quic_xmpp_socket.dart'; + +void main() { + group('extractToBareJidForRouting', () { + test('extracts bare jid from full jid in to attribute', () { + const stanza = ''; + expect(extractToBareJidForRouting(stanza), 'room@example.com'); + }); + + test('returns null when no to attribute exists', () { + const stanza = ''; + expect(extractToBareJidForRouting(stanza), isNull); + }); + + test('supports single-quoted to attribute', () { + const stanza = ""; + expect(extractToBareJidForRouting(stanza), 'romeo@example.net'); + }); + }); + + group('bareJidForRouting', () { + test('strips resource', () { + expect( + bareJidForRouting('juliet@example.com/balcony'), + 'juliet@example.com', + ); + }); + + test('keeps bare jid unchanged', () { + expect( + bareJidForRouting('room@conference.example.org'), + 'room@conference.example.org', + ); + }); + }); + + group('isStanzaPayload', () { + test('recognises message stanza', () { + expect(isStanzaPayload(''), isTrue); + }); + + test('recognises presence stanza', () { + expect(isStanzaPayload(''), isTrue); + }); + + test('recognises iq stanza', () { + expect(isStanzaPayload(''), isTrue); + }); + + test('skips XML prolog before stanza', () { + expect( + isStanzaPayload(""), + isTrue, + ); + }); + + test('rejects stream:stream opener', () { + expect( + isStanzaPayload( + "", + ), + isFalse, + ); + }); + + test('rejects XEP-0198 r/a even if they ever carry a to attribute', () { + expect(isStanzaPayload(''), isFalse); + expect(isStanzaPayload(''), isFalse); + }); + + test('rejects CSI elements', () { + expect(isStanzaPayload(''), isFalse); + expect(isStanzaPayload(''), isFalse); + }); + + test('rejects SASL frames', () { + expect( + isStanzaPayload(''), + isFalse, + ); + }); + + test('rejects empty payload', () { + expect(isStanzaPayload(''), isFalse); + expect(isStanzaPayload(' '), isFalse); + }); + }); + + group('quicAuxSlotForBareJid', () { + test('is deterministic for the same jid', () { + final first = quicAuxSlotForBareJid('room@example.com', 20); + final second = quicAuxSlotForBareJid('room@example.com', 20); + expect(first, second); + }); + + test('stays within bounds', () { + final slot = quicAuxSlotForBareJid('contact@example.com', 20); + expect(slot, inInclusiveRange(0, 19)); + }); + }); + + group('QuicCapableXmppSocket server stream pool', () { + test('serverStreamPoolSize starts at zero', () { + final socket = QuicCapableXmppSocket(); + expect(socket.serverStreamPoolSize, 0); + }); + + test('serverStreamPoolSize is zero after close()', () { + // close() on a non-QUIC socket is a no-op for the pool but the field + // must still be accessible and report 0 (pool was never populated). + final socket = QuicCapableXmppSocket(); + socket.close(); + expect(socket.serverStreamPoolSize, 0); + }); + }); + + group('connectionAcceptBi bridge export', () { + // connectionAcceptBi requires a live QUIC connection and cannot be called + // in a unit test. These tests verify that the symbol is exported from the + // flutter_quic bridge with the expected type so that compile-time regressions + // are caught immediately. + // + // The function now takes a shared reference (&QuicConnection) so it does + // NOT consume the arc and can run concurrently with connectionOpenBi. + test('connectionAcceptBi is a Function', () { + // If the codegen did not produce the symbol this line would not compile. + expect(connectionAcceptBi, isA()); + }); + }); +} diff --git a/test/service_discovery_features_test.dart b/test/service_discovery_features_test.dart new file mode 100644 index 0000000..76d8f63 --- /dev/null +++ b/test/service_discovery_features_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:xmpp_stone/src/features/servicediscovery/ServiceDiscoverySupport.dart'; + +/// Regression tests for the disco#info features Wimsy advertises. +/// +/// These exist primarily to catch accidental removals: the +notify entries +/// determine whether the server forwards PubSub events to us without an +/// explicit ``, so missing one of them silently regresses +/// startup traffic (we'd fall back to polling via IQ each connect). +void main() { + group('SERVICE_DISCOVERY_SUPPORT_LIST', () { + test('R1.2: advertises urn:xmpp:mds:displayed:0+notify', () { + expect( + SERVICE_DISCOVERY_SUPPORT_LIST, + contains('urn:xmpp:mds:displayed:0+notify'), + ); + }); + + test('advertises urn:xmpp:avatar:metadata+notify', () { + expect( + SERVICE_DISCOVERY_SUPPORT_LIST, + contains('urn:xmpp:avatar:metadata+notify'), + ); + }); + + test('contains the core Jingle/RTP/file-transfer namespaces', () { + const expected = [ + 'urn:xmpp:jingle:1', + 'urn:xmpp:jingle-message:0', + 'urn:xmpp:jingle:apps:rtp:1', + 'urn:xmpp:jingle:apps:rtp:audio', + 'urn:xmpp:jingle:apps:rtp:video', + 'urn:xmpp:jingle:transports:ice-udp:1', + 'urn:xmpp:jingle:apps:dtls:0', + 'urn:xmpp:jingle:apps:file-transfer:5', + 'urn:xmpp:jingle:transports:ibb:1', + ]; + for (final feature in expected) { + expect( + SERVICE_DISCOVERY_SUPPORT_LIST, + contains(feature), + reason: 'missing required disco feature: $feature', + ); + } + }); + + test('contains chatstates, blocking, IBB, reply, fallback', () { + const expected = [ + 'http://jabber.org/protocol/chatstates', + 'http://jabber.org/protocol/ibb', + 'urn:xmpp:blocking', + 'urn:xmpp:reply:0', + 'urn:xmpp:feature-fallback:0', + ]; + for (final feature in expected) { + expect( + SERVICE_DISCOVERY_SUPPORT_LIST, + contains(feature), + reason: 'missing required disco feature: $feature', + ); + } + }); + }); +} diff --git a/test/srv_transport_filter_test.dart b/test/srv_transport_filter_test.dart new file mode 100644 index 0000000..90683f2 --- /dev/null +++ b/test/srv_transport_filter_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/xmpp/srv_target.dart'; + +XmppSrvTarget _target({required bool directTls, String host = 'h', int port = 0}) { + return XmppSrvTarget( + host: host, + port: port, + priority: 0, + weight: 1, + directTls: directTls, + ); +} + +void main() { + group('filterTcpSrvCandidatesByTransport', () { + final directTls = _target(directTls: true, host: 'tls.example.org', port: 5223); + final plainTcp = _target(directTls: false, host: 'tcp.example.org', port: 5222); + final candidates = [directTls, plainTcp]; + + test('keeps both when both flags are on', () { + final result = filterTcpSrvCandidatesByTransport( + candidates, + allowDirectTls: true, + allowPlainTcp: true, + ); + expect(result, equals(candidates)); + }); + + test('drops plain-TCP records when plain TCP is off', () { + final result = filterTcpSrvCandidatesByTransport( + candidates, + allowDirectTls: true, + allowPlainTcp: false, + ); + expect(result.map((c) => c.host).toList(), equals(['tls.example.org'])); + }); + + test('drops Direct TLS records when Direct TLS is off', () { + final result = filterTcpSrvCandidatesByTransport( + candidates, + allowDirectTls: false, + allowPlainTcp: true, + ); + expect(result.map((c) => c.host).toList(), equals(['tcp.example.org'])); + }); + + test('returns empty when both flags are off', () { + final result = filterTcpSrvCandidatesByTransport( + candidates, + allowDirectTls: false, + allowPlainTcp: false, + ); + expect(result, isEmpty); + }); + + test('preserves original order within each transport bucket', () { + // Two plain-TCP records ordered by priority should keep that order. + final first = _target(directTls: false, host: 'one', port: 5222); + final second = _target(directTls: false, host: 'two', port: 5222); + final result = filterTcpSrvCandidatesByTransport( + [first, second], + allowDirectTls: false, + allowPlainTcp: true, + ); + expect(result.map((c) => c.host).toList(), equals(['one', 'two'])); + }); + }); +} diff --git a/test/startup_fetch_helpers_test.dart b/test/startup_fetch_helpers_test.dart new file mode 100644 index 0000000..b93f3db --- /dev/null +++ b/test/startup_fetch_helpers_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:wimsy/xmpp/startup_fetch_helpers.dart'; + +void main() { + group('shouldFetchDisplayedSyncBootstrap (R1.1)', () { + test('empty cache → fetch', () { + expect( + shouldFetchDisplayedSyncBootstrap(hasCachedDisplayedSync: false), + isTrue, + ); + }); + + test('cache populated from disk → skip the bootstrap IQ', () { + expect( + shouldFetchDisplayedSyncBootstrap(hasCachedDisplayedSync: true), + isFalse, + ); + }); + + test('force=true overrides the cache check', () { + expect( + shouldFetchDisplayedSyncBootstrap( + hasCachedDisplayedSync: true, + force: true, + ), + isTrue, + ); + }); + + test('force=true on empty cache also fetches (idempotent)', () { + expect( + shouldFetchDisplayedSyncBootstrap( + hasCachedDisplayedSync: false, + force: true, + ), + isTrue, + ); + }); + }); + + group('shouldFetchMamCatchUpForChat (R2.2)', () { + test('no displayed marker → fetch', () { + expect( + shouldFetchMamCatchUpForChat( + displayedStanzaId: null, + latestLocalMamId: 'mam-1', + stanzaIdAtLatestMamId: 'sid-1', + ), + isTrue, + ); + expect( + shouldFetchMamCatchUpForChat( + displayedStanzaId: '', + latestLocalMamId: 'mam-1', + stanzaIdAtLatestMamId: 'sid-1', + ), + isTrue, + ); + }); + + test('no local MAM cursor → fetch', () { + expect( + shouldFetchMamCatchUpForChat( + displayedStanzaId: 'sid-1', + latestLocalMamId: null, + stanzaIdAtLatestMamId: 'sid-1', + ), + isTrue, + ); + }); + + test('no stanza-id at latest MAM id → fetch', () { + expect( + shouldFetchMamCatchUpForChat( + displayedStanzaId: 'sid-1', + latestLocalMamId: 'mam-1', + stanzaIdAtLatestMamId: null, + ), + isTrue, + ); + }); + + test('marker matches latest local stanza-id → skip', () { + expect( + shouldFetchMamCatchUpForChat( + displayedStanzaId: 'sid-1', + latestLocalMamId: 'mam-1', + stanzaIdAtLatestMamId: 'sid-1', + ), + isFalse, + ); + }); + + test('marker is newer than what we have → fetch', () { + expect( + shouldFetchMamCatchUpForChat( + displayedStanzaId: 'sid-99', + latestLocalMamId: 'mam-1', + stanzaIdAtLatestMamId: 'sid-1', + ), + isTrue, + ); + }); + }); +} diff --git a/test/unified_mam_catchup_test.dart b/test/unified_mam_catchup_test.dart new file mode 100644 index 0000000..54e8bda --- /dev/null +++ b/test/unified_mam_catchup_test.dart @@ -0,0 +1,103 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:xmpp_stone/xmpp_stone.dart'; + +/// Tests for R2.1: unified server-archive MAM catch-up. +/// +/// The `_startUnifiedDmCatchUp` method in `xmpp_service.dart` builds a MAM +/// IQ manually and parses the `` IQ result to advance `_lastMamIdSeen`. +/// These tests verify the parsing logic using xmpp_stone's `XmppElement` API +/// directly, mirroring what the production code does. +void main() { + group('R2.1: RSM parsing', () { + /// Build a fake MAM IQ result with the given RSM value. + IqStanza _buildFinResult({String? lastId, bool includeRsm = true}) { + final iq = IqStanza(AbstractStanza.getRandomId(), IqStanzaType.RESULT); + + final fin = XmppElement()..name = 'fin'; + fin.addAttribute(XmppAttribute('xmlns', 'urn:xmpp:mam:2')); + + if (includeRsm) { + final set = XmppElement()..name = 'set'; + set.addAttribute( + XmppAttribute('xmlns', 'http://jabber.org/protocol/rsm'), + ); + if (lastId != null) { + final last = XmppElement() + ..name = 'last' + ..textValue = lastId; + set.addChild(last); + } + fin.addChild(set); + } + + iq.addChild(fin); + return iq; + } + + /// Replicate the parsing logic from `_startUnifiedDmCatchUp`. + String? _parseFinLastId(IqStanza response) { + if (response.type != IqStanzaType.RESULT) { + return null; + } + final fin = response.children.firstWhere( + (child) => + child.name == 'fin' && + child.getAttribute('xmlns')?.value == 'urn:xmpp:mam:2', + orElse: () => XmppElement(), + ); + if (fin.name != 'fin') { + return null; + } + final rsmSet = fin.children.firstWhere( + (child) => + child.name == 'set' && + child.getAttribute('xmlns')?.value == + 'http://jabber.org/protocol/rsm', + orElse: () => XmppElement(), + ); + if (rsmSet.name != 'set') { + return null; + } + final lastEl = rsmSet.children.firstWhere( + (child) => child.name == 'last', + orElse: () => XmppElement(), + ); + final lastId = + lastEl.name == 'last' ? lastEl.textValue?.trim() : null; + return (lastId != null && lastId.isNotEmpty) ? lastId : null; + } + + test('extracts id from a well-formed result', () { + final iq = _buildFinResult(lastId: 'mam-id-42'); + expect(_parseFinLastId(iq), equals('mam-id-42')); + }); + + test('returns null when IQ type is not RESULT', () { + final iq = IqStanza(AbstractStanza.getRandomId(), IqStanzaType.ERROR); + final fin = XmppElement()..name = 'fin'; + fin.addAttribute(XmppAttribute('xmlns', 'urn:xmpp:mam:2')); + iq.addChild(fin); + expect(_parseFinLastId(iq), isNull); + }); + + test('returns null when element is absent', () { + final iq = IqStanza(AbstractStanza.getRandomId(), IqStanzaType.RESULT); + expect(_parseFinLastId(iq), isNull); + }); + + test('returns null when RSM is absent from ', () { + final iq = _buildFinResult(lastId: 'mam-id-1', includeRsm: false); + expect(_parseFinLastId(iq), isNull); + }); + + test('returns null when is absent from RSM ', () { + final iq = _buildFinResult(lastId: null); + expect(_parseFinLastId(iq), isNull); + }); + + test('trims whitespace from text value', () { + final iq = _buildFinResult(lastId: ' mam-id-trimmed '); + expect(_parseFinLastId(iq), equals('mam-id-trimmed')); + }); + }); +} diff --git a/test/vcard_utils_test.dart b/test/vcard_utils_test.dart index f4254c7..dd5749c 100644 --- a/test/vcard_utils_test.dart +++ b/test/vcard_utils_test.dart @@ -43,4 +43,174 @@ void main() { 'a6626f8138cf2b82d2e8c3f3e28090a03d54aee2a6626f8138cf2b82d2e8c3f3e28090a03d54aee2'; expect(normalizeVcardPhotoHash(doubled), single); }); + + group('shouldFetchVcardForCache (R4.1)', () { + const jid = 'alice@example.com'; + const hash = 'a6626f8138cf2b82d2e8c3f3e28090a03d54aee2'; + + test('preferName=true always fetches', () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: true, + cachedAvatarBytes: { + jid: Uint8List.fromList([1, 2]), + }, + cachedAvatarState: {jid: hash}, + advertisedHash: hash, + ), + isTrue, + ); + }); + + test('no state recorded → fetch', () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: const {}, + cachedAvatarState: const {}, + ), + isTrue, + ); + }); + + test('no-avatar sentinel and no advertised hash → skip', () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: const {}, + cachedAvatarState: const { + jid: vcardNoAvatarSentinel, + }, + ), + isFalse, + ); + }); + + test( + 'no-avatar sentinel but presence advertises a hash → fetch', + () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: const {}, + cachedAvatarState: const { + jid: vcardNoAvatarSentinel, + }, + advertisedHash: hash, + ), + isTrue, + ); + }, + ); + + test('cached bytes & matching hash, no presence hash → skip', () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: { + jid: Uint8List.fromList([1, 2]), + }, + cachedAvatarState: const {jid: hash}, + ), + isFalse, + ); + }); + + test( + 'cached bytes & matching hash & matching presence hash → skip', + () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: { + jid: Uint8List.fromList([1, 2]), + }, + cachedAvatarState: const {jid: hash}, + advertisedHash: hash, + ), + isFalse, + ); + }, + ); + + test('cached bytes but presence advertises a different hash → fetch', () { + const newHash = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: { + jid: Uint8List.fromList([1, 2]), + }, + cachedAvatarState: const {jid: hash}, + advertisedHash: newHash, + ), + isTrue, + ); + }); + + test('hash recorded but no bytes cached → fetch', () { + expect( + shouldFetchVcardForCache( + bareJid: jid, + preferName: false, + cachedAvatarBytes: const {}, + cachedAvatarState: const {jid: hash}, + ), + isTrue, + ); + }); + + // R6: verify that after a re-seed (simulating the Ready-handler re-seed + // introduced by R6) the guard correctly suppresses the fetch. + test('R6: guard suppresses fetch after cache is re-seeded from disk', () { + final hash = 'abc123'; + final bytes = Uint8List.fromList([1, 2, 3]); + // Simulate what _seedVcardAvatars + _seedVcardAvatarState do on reconnect. + final cachedAvatarBytes = {'alice@example.com': bytes}; + final cachedAvatarState = {'alice@example.com': hash}; + + // The presence update advertises the same hash we already have cached. + expect( + shouldFetchVcardForCache( + bareJid: 'alice@example.com', + preferName: false, + cachedAvatarBytes: cachedAvatarBytes, + cachedAvatarState: cachedAvatarState, + advertisedHash: hash, + ), + isFalse, + reason: 'should skip fetch when re-seeded cache matches advertised hash', + ); + }); + + test('R6: guard allows fetch when re-seeded cache has different hash', () { + final oldHash = 'oldhash'; + final newHash = 'newhash'; + final bytes = Uint8List.fromList([1, 2, 3]); + final cachedAvatarBytes = {'alice@example.com': bytes}; + final cachedAvatarState = { + 'alice@example.com': oldHash, + }; + + // The presence update advertises a new hash — we must fetch. + expect( + shouldFetchVcardForCache( + bareJid: 'alice@example.com', + preferName: false, + cachedAvatarBytes: cachedAvatarBytes, + cachedAvatarState: cachedAvatarState, + advertisedHash: newHash, + ), + isTrue, + reason: 'should fetch when advertised hash differs from cached hash', + ); + }); + }); } diff --git a/test/widget_test.dart b/test/widget_test.dart index 3989d89..b3aeaeb 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -6,12 +6,16 @@ // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:wimsy/main.dart'; +import 'package:wimsy/storage/preferences_service.dart'; void main() { testWidgets('App launches smoke test', (WidgetTester tester) async { - await tester.pumpWidget(const WimsyApp()); + SharedPreferences.setMockInitialValues({}); + final prefs = await PreferencesService.load(); + await tester.pumpWidget(WimsyApp(preferences: prefs)); expect(find.byType(WimsyApp), findsOneWidget); }); diff --git a/vendor/flutter_quic/AUTHORS b/vendor/flutter_quic/AUTHORS new file mode 100644 index 0000000..c983495 --- /dev/null +++ b/vendor/flutter_quic/AUTHORS @@ -0,0 +1,26 @@ +# Authors + +This file lists the contributors to the Flutter QUIC project. + +## Maintainers + +- Shankar Kakumani + +## Contributors + +Contributors are listed in alphabetical order by last name. + + + +## Acknowledgments + +Special thanks to: + +- Quinn team for the excellent QUIC implementation in Rust +- flutter_rust_bridge team for making Rust-Flutter integration possible +- Flutter team for the amazing framework +- IETF QUIC Working Group for the protocol specification + +--- + +To add yourself to this list, please include your name and email in your first pull request. \ No newline at end of file diff --git a/vendor/flutter_quic/CHANGELOG.md b/vendor/flutter_quic/CHANGELOG.md new file mode 100644 index 0000000..8121caf --- /dev/null +++ b/vendor/flutter_quic/CHANGELOG.md @@ -0,0 +1,117 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-07-16 + +### Changed +- Version bump from 0.1.3-beta to 1.0.0 + +## [0.1.3-beta] - 2025-07-16 + +### Changed +- **Release automation**: Simplified GitHub Actions workflow for tag-based publishing +- **Version management**: Enhanced release script with smart version suggestions +- **Workflow optimization**: Removed unnecessary binding generation from automated releases + +## [0.1.0-beta.4] - 2025-07-16 + +### Changed +- **Release automation**: Added automated release scripts and GitHub Actions workflow +- **Testing**: Removed test requirements from release process for faster deployment +- **Documentation**: Updated release process documentation + +## [0.1.0-beta.3] - 2025-07-16 + +### Changed +- **Git repository**: Cleaned up repository history and removed confidential documentation +- **Security**: Improved gitignore to prevent accidental commit of sensitive files + +## [0.1.0-beta.2] - 2025-01-27 + +### Fixed +- **Certificate format compatibility**: Fixed private key format issues with rustls - now requires PKCS#8 DER format +- **Certificate generation**: Updated `generate_certs.sh` to generate proper DER format certificates for Flutter assets +- **Documentation improvements**: Completely restructured README to lead with substance, removed marketing fluff, and provided clear certificate format guidance + +### Changed +- **Certificate loading**: Example app now loads `.der` format certificates instead of raw PEM files +- **Asset configuration**: Updated Flutter assets to include DER format certificate files +- **Documentation structure**: Streamlined README with beta warnings upfront and consolidated examples + +## [0.1.0-beta.1] - 2025-07-15 + +### Added + +#### 🚀 Core QUIC Support +- **Client endpoints**: Create QUIC client connections with `createClientEndpoint()` +- **Server endpoints**: Create QUIC server connections with `createServerEndpoint()` +- **TLS configuration**: Full support for TLS 1.3 with `serverConfigWithSingleCert()` +- **Stream operations**: Bidirectional and unidirectional streams for data transfer +- **Connection management**: Connection stats, RTT monitoring, and remote address info +- **Datagram support**: Unreliable message delivery for real-time applications + +#### 🎯 Convenience API +- **HTTP-style interface**: Familiar GET/POST methods with QUIC performance +- **Connection pooling**: Automatic connection reuse and management +- **Retry logic**: Built-in exponential backoff for failed requests +- **Timeout protection**: Configurable timeouts for all operations +- **Configuration management**: Fine-grained control over connection parameters + +#### 🔧 Developer Experience +- **Async/await support**: Full Flutter integration with Future-based APIs +- **Memory safety**: Rust ownership prevents memory leaks and corruption +- **Type safety**: Generated bindings with compile-time guarantees +- **Error handling**: Specific error types for different failure scenarios +- **Cross-platform**: Support for Android, iOS, Windows, macOS, and Linux + +#### 📊 Production Features +- **Certificate loading**: Support for PEM/DER certificates and private keys +- **Connection migration**: Seamless WiFi ↔ cellular network switching +- **0-RTT connections**: Instant reconnection for repeat connections +- **Stream multiplexing**: Multiple independent streams over single connection +- **Flow control**: Built-in congestion control and bandwidth management + +### Technical Details + +#### API Coverage +- ✅ **Core API**: Complete 1:1 Quinn library mapping +- ✅ **Convenience API**: HTTP-style wrapper for common use cases +- ✅ **Client operations**: Full client-side QUIC functionality +- ✅ **Server operations**: Complete server-side QUIC implementation +- ✅ **Certificate management**: TLS certificate loading and validation + +#### Performance Features +- **Head-of-line blocking elimination**: Independent stream processing +- **Reduced connection overhead**: Single connection replaces multiple TCP connections +- **Built-in security**: TLS 1.3 integration without additional handshake overhead +- **Network adaptation**: Dynamic adjustment to changing network conditions + +#### Platform Support +- **Android**: API level 21+ with full feature support +- **iOS**: iOS 11+ with complete functionality +- **Desktop**: Windows 10+, macOS 10.14+, Linux (modern distributions) +- **Mobile-optimized**: Battery-aware optimizations for mobile platforms + +### Known Limitations +- **Web platform**: Not supported (WebTransport may be added in future) +- **HTTP/3 semantics**: Raw QUIC transport only (HTTP/3 helpers planned) +- **Advanced TLS**: Custom certificate validation not yet implemented + +### Dependencies +- `quinn = "0.11"` - Core QUIC implementation +- `flutter_rust_bridge = "2.11.1"` - Rust-Flutter integration +- `tokio` - Async runtime for Rust operations +- `rustls` - TLS 1.3 implementation + +### Breaking Changes +- None (initial release) + +--- + +**Note**: This is a beta release intended for testing and development. While the API is stable and feature-complete, it is not recommended for production use until the stable 1.0.0 release. + +**Ready to test QUIC?** Check out the examples in the `example/` directory and let us know your feedback! diff --git a/vendor/flutter_quic/CONTRIBUTING.md b/vendor/flutter_quic/CONTRIBUTING.md new file mode 100644 index 0000000..4002371 --- /dev/null +++ b/vendor/flutter_quic/CONTRIBUTING.md @@ -0,0 +1,233 @@ +# Contributing to Flutter QUIC + +Thank you for your interest in contributing to Flutter QUIC! This document provides guidelines and information for contributors. + +## Code of Conduct + +This project follows the [Flutter Code of Conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. + +## Development Setup + +### Prerequisites + +- Flutter SDK (latest stable version) +- Rust toolchain (1.70 or later) +- Android SDK (for Android development) +- Xcode (for iOS/macOS development) +- Visual Studio or Build Tools (for Windows development) + +### Setup Steps + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/flutter_quic.git +cd flutter_quic +``` + +2. Install Flutter dependencies: +```bash +flutter pub get +``` + +3. Install Rust dependencies: +```bash +cd rust +cargo build +cd .. +``` + +4. Generate Flutter-Rust bridge bindings: +```bash +flutter_rust_bridge_codegen generate +``` + +5. Run tests to verify setup: +```bash +flutter test +cd rust && cargo test +``` + +## Development Principles + +### 🚫 NO PLACEHOLDERS Policy + +This project has a strict **NO PLACEHOLDERS** policy. All code must be production-ready: + +- ❌ No `todo!()` macros in Rust +- ❌ No `// TODO:` comments +- ❌ No `unimplemented!()` calls +- ❌ No dummy return values like `Ok(())` without real logic +- ❌ No mock data instead of actual API calls +- ❌ No generic error messages like "Something went wrong" + +### ✅ Required Patterns + +- Complete function implementations +- Specific error handling with proper error types +- Real API calls to Quinn library +- Actual network operations +- Proper resource management and cleanup + +## Architecture Guidelines + +### Two-Tier API Design + +1. **Core API** (`lib/src/core/`): 1:1 mapping to Quinn Rust library + - Every Quinn public method must be exposed + - Direct async bridging: `Future` in Rust → `Future` in Dart + - Use `#[frb(opaque)]` for complex Rust types + - Implement proper `Drop` traits for cleanup + +2. **Convenience API** (`lib/src/convenience/`): High-level abstractions + - Built on top of Core API + - HTTP-style interface for common use cases + - Connection pooling and retry logic + - Simple configuration options + +### File Organization + +``` +lib/src/ +├── core/ # Core API implementations +├── convenience/ # Convenience API implementations +├── errors/ # Error type definitions +└── models/ # Data models and types + +rust/src/ +├── core/ # Core Quinn wrappers +├── convenience/ # High-level client implementations +├── errors/ # Error handling +└── models/ # Rust data structures +``` + +## Testing Standards + +### Required Tests + +1. **Unit Tests**: All public methods and error paths +2. **Integration Tests**: Real network communication with certificates +3. **Performance Tests**: Within 5% of native Quinn performance +4. **Platform Tests**: All supported platforms (Android, iOS, Windows, macOS, Linux) + +### Test Organization + +``` +test/ +├── unit/ # Unit tests for individual components +├── integration/ # Integration tests with real networking +└── performance/ # Performance benchmarks +``` + +### Running Tests + +```bash +# Dart tests +flutter test + +# Rust tests +cd rust && cargo test + +# Integration tests (requires test server) +flutter test test/integration/ + +# Performance tests +flutter test test/performance/ +``` + +## Code Style + +### Dart Code Style + +- Follow [Effective Dart](https://dart.dev/guides/language/effective-dart) guidelines +- Use `dart format` for formatting +- Use `dart analyze` for linting +- Document all public APIs with dartdoc comments + +### Rust Code Style + +- Follow standard Rust formatting with `cargo fmt` +- Use `cargo clippy` for linting +- Document all public APIs with `///` comments +- Use meaningful error types, not `Box` + +## Submitting Changes + +### Pull Request Process + +1. **Fork** the repository and create a feature branch +2. **Write tests** for your changes before implementing +3. **Implement** your changes following the project principles +4. **Test** thoroughly on multiple platforms +5. **Document** all public APIs and behavior changes +6. **Submit** a pull request with a clear description + +### Pull Request Requirements + +- [ ] All tests pass on all platforms +- [ ] Code follows project style guidelines +- [ ] Public APIs are documented +- [ ] No placeholders or TODO items +- [ ] Performance impact is acceptable +- [ ] Breaking changes are clearly marked + +### Commit Message Format + +Use conventional commit format: + +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +Examples: +- `feat(core): add bidirectional stream support` +- `fix(convenience): handle connection timeout correctly` +- `docs(readme): update installation instructions` + +## Issue Reporting + +### Bug Reports + +Please include: +- Flutter version and platform +- Rust version +- Steps to reproduce +- Expected vs actual behavior +- Relevant logs or stack traces + +### Feature Requests + +Please include: +- Use case description +- Proposed API design +- Performance considerations +- Platform compatibility + +## Review Process + +All contributions go through code review: + +1. **Automated checks**: CI runs tests and linting +2. **Manual review**: Maintainers review code quality and design +3. **Testing**: Changes are tested on multiple platforms +4. **Documentation**: API documentation is reviewed +5. **Performance**: Performance impact is evaluated + +## Getting Help + +- **Documentation**: Check the `docs/` directory +- **Discussions**: Use GitHub Discussions for questions +- **Issues**: Use GitHub Issues for bugs and feature requests +- **Chat**: Join our community chat (link TBD) + +## Recognition + +Contributors will be: +- Listed in release notes for significant contributions +- Added to the `AUTHORS` file +- Mentioned in documentation where appropriate + +Thank you for contributing to Flutter QUIC! 🚀 \ No newline at end of file diff --git a/vendor/flutter_quic/LICENSE b/vendor/flutter_quic/LICENSE new file mode 100644 index 0000000..dfbfece --- /dev/null +++ b/vendor/flutter_quic/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Flutter QUIC Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/flutter_quic/README.md b/vendor/flutter_quic/README.md new file mode 100644 index 0000000..a729819 --- /dev/null +++ b/vendor/flutter_quic/README.md @@ -0,0 +1,408 @@ +# Flutter QUIC + +``` + ███████╗██╗ ██╗ ██╗████████╗████████╗███████╗██████╗ + ██╔════╝██║ ██║ ██║╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ + █████╗ ██║ ██║ ██║ ██║ ██║ █████╗ ██████╔╝ + ██╔══╝ ██║ ██║ ██║ ██║ ██║ ██╔══╝ ██╔══██╗ + ██║ ███████╗╚██████╔╝ ██║ ██║ ███████╗██║ ██║ + ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + + ██████╗ ██╗ ██╗██╗ ██████╗ + ██╔═══██╗██║ ██║██║██╔════╝ + ██║ ██║██║ ██║██║██║ + ██║▄▄ ██║██║ ██║██║██║ + ╚██████╔╝╚██████╔╝██║╚██████╗ + ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═════╝ +``` + +[![Pub Version](https://img.shields.io/pub/v/flutter_quic.svg)](https://pub.dev/packages/flutter_quic) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Platform](https://img.shields.io/badge/platform-flutter-blue.svg)](https://flutter.dev) + +A Flutter plugin providing QUIC protocol support for mobile and desktop applications. Built on the [Quinn](https://github.com/quinn-rs/quinn) Rust library using [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge). + +--- + +## ✨ What This Library Supports + +### 🚀 **Core Features** +- ✅ **QUIC Client & Server** - Complete client/server implementation +- ✅ **TLS 1.3 built-in** - Security is not optional +- ✅ **Multiple streams** - Bidirectional and unidirectional over single connection +- ✅ **QUIC datagrams** - For unreliable messaging +- ✅ **Connection migration** - Seamless WiFi ↔ cellular switching +- ✅ **0-RTT reconnection** - Fast repeat connections +- ✅ **No head-of-line blocking** - Independent streams +- ✅ **HTTP-style API** - Easy adoption with convenience layer +- ✅ **Complete Quinn mapping** - Full access to underlying library + +### 📱 **Platform Support** +| Platform | Status | Notes | +|----------|--------|-------| +| **Android** | ✅ Supported | API 21+ | +| **iOS** | ✅ Supported | iOS 11+ | +| **Windows** | ✅ Supported | Windows 10+ | +| **macOS** | ✅ Supported | macOS 10.14+ | +| **Linux** | ✅ Supported | Modern distributions | +| **Web** | ❌ Not Supported | WebTransport planned | + +### 🎯 **API Design** +- **Convenience API**: HTTP-style interface for common use cases +- **Core API**: Complete 1:1 Quinn mapping for advanced control +- **Async/Await**: Full Flutter integration +- **Memory Safe**: Rust ownership prevents leaks +- **Type Safe**: Generated bindings with compile-time safety + +--- + +## 🤔 What is QUIC? + +QUIC (Quick UDP Internet Connections) is the transport protocol powering HTTP/3. Originally developed by Google, now an IETF standard. Key benefits: + +- **Faster connections** - 1 RTT setup vs 3 RTTs for TCP+TLS +- **Stream multiplexing** - No head-of-line blocking +- **Connection migration** - Survives network changes +- **Built-in encryption** - TLS 1.3 integrated, not layered + +--- + +## 📦 Installation + +Add to your `pubspec.yaml`: + +```yaml +dependencies: + flutter_quic: ^0.1.0-beta.3 +``` + +Initialize in your app: + +```dart +import 'package:flutter_quic/flutter_quic.dart'; + +void main() async { + await RustLib.init(); + runApp(MyApp()); +} +``` + +--- + +## 🚀 Examples + +### Convenience API (HTTP-style) +Perfect for most use cases: + +```dart +import 'package:flutter_quic/flutter_quic.dart'; + +// Create client (handles connection pooling) +final client = await quicClientCreate(); + +// Make requests +final response = await quicClientGet( + client: client, + url: 'https://example.com/api/data' +); +print('Response: ${response.$2}'); + +// POST data +final postResult = await quicClientPost( + client: client, + url: 'https://example.com/api/submit', + data: '{"message": "Hello QUIC!"}' +); +``` + +### Core API (Advanced) +For full Quinn control: + +```dart +// Create endpoint and connect +final endpoint = await createClientEndpoint(); +final connectResult = await endpointConnect( + endpoint: endpoint, + addr: '93.184.216.34:443', + serverName: 'example.com' +); +final connection = connectResult.$2; + +// Open bidirectional stream +final streamResult = await connectionOpenBi(connection: connection); +final sendStream = streamResult.$2; +final recvStream = streamResult.$3; + +// Send and receive data +await sendStreamWriteAll(stream: sendStream, data: 'Hello, QUIC!'.codeUnits); +final readResult = await recvStreamReadToEnd(stream: recvStream, maxLength: BigInt.from(1024)); +``` + +### QUIC Server +Create servers with TLS certificates: + +```dart +// Load certificates (DER format required) +final certChain = [await File('server.crt.der').readAsBytes()]; +final privateKey = await File('server.key.der').readAsBytes(); + +// Create server +final serverConfig = await serverConfigWithSingleCert( + certChain: certChain, + key: privateKey, +); +final serverEndpoint = await createServerEndpoint( + config: serverConfig, + addr: '127.0.0.1:4433', +); +``` + +### Real-World Use Cases + +**Mobile App with Network Switching:** +```dart +class ApiService { + late QuicClient _client; + + Future init() async { + await RustLib.init(); + _client = await quicClientCreate(); + } + + // Automatically handles WiFi ↔ cellular switching + Future getUserProfile(String userId) async { + final result = await quicClientGet( + client: _client, + url: 'https://api.myapp.com/users/$userId' + ); + return UserProfile.fromJson(jsonDecode(result.$2)); + } +} +``` + +**Real-time Gaming (Multiple Streams):** +```dart +// Multiple concurrent streams - no head-of-line blocking +final positionStream = await connectionOpenUni(connection: connection); // High frequency +final chatStream = await connectionOpenBi(connection: connection); // Low frequency +final eventsStream = await connectionOpenBi(connection: connection); // Medium frequency +``` + +**Dashboard Loading (Concurrent Requests):** +```dart +// All requests over single QUIC connection +final results = await Future.wait([ + quicClientGet(client: client, url: 'https://api.com/analytics'), + quicClientGet(client: client, url: 'https://api.com/notifications'), + quicClientGet(client: client, url: 'https://api.com/user-activity'), + quicClientGet(client: client, url: 'https://api.com/system-status'), +]); +``` + +--- + +## 🔐 Certificate Requirements + +**Important**: QUIC requires TLS certificates in **DER format** with **PKCS#8 private keys**. + +### Development & Testing +Use the included certificate generator: + +```bash +# In your example/ directory +./generate_certs.sh +``` + +Creates: +- `server.crt.der` - Certificate in DER format +- `server.key.der` - Private key in PKCS#8 DER format + +```dart +// Load from Flutter assets +final certBytes = await rootBundle.load('certs/server.crt.der'); +final keyBytes = await rootBundle.load('certs/server.key.der'); + +final certChain = [certBytes.buffer.asUint8List()]; +final privateKey = keyBytes.buffer.asUint8List().cast().toList(); +``` + +### Production Certificates +Convert existing certificates: + +```bash +# Convert certificate to DER +openssl x509 -in server.crt -outform DER -out server.crt.der + +# Convert private key to PKCS#8 DER +openssl pkey -in server.key -outform DER -out server.key.der +``` + +**Certificate Sources:** +- 🆓 **Let's Encrypt** - Free automated certificates +- ☁️ **Cloud providers** - AWS ACM, Google Cloud SSL, Azure Key Vault +- 🏢 **Corporate CA** - Your organization's certificate authority +- 🧪 **Development** - Use `./generate_certs.sh` for local testing + +--- + +## 🛡️ Error Handling + +Specific error types for different scenarios: + +```dart +try { + final result = await quicClientGet(client: client, url: 'https://example.com'); +} on QuicError catch (e) { + switch (e) { + case QuicError.connection(field0: final message): + print('Connection failed: $message'); + // Retry logic + + case QuicError.endpoint(field0: final message): + print('Endpoint error: $message'); + // Check network + + case QuicError.stream(field0: final message): + print('Stream error: $message'); + // Handle stream issues + } +} +``` + +--- + +## 🔧 Complete API Reference + +### Convenience API +```dart +// Client creation +Future quicClientCreate() +Future quicClientCreateWithConfig({required QuicClientConfig config}) + +// HTTP-style methods +Future<(QuicClient, String)> quicClientGet({required QuicClient client, required String url}) +Future<(QuicClient, String)> quicClientPost({required QuicClient client, required String url, required String data}) + +// With timeouts +Future<(QuicClient, String)> quicClientGetWithTimeout({required QuicClient client, required String url}) +Future<(QuicClient, String)> quicClientPostWithTimeout({required QuicClient client, required String url, required String data}) + +// Management +Future<(QuicClient, QuicClientConfig)> quicClientConfig({required QuicClient client}) +Future quicClientClearPool({required QuicClient client}) +``` + +### Core API +```dart +// Endpoints +Future createClientEndpoint() +Future createServerEndpoint({required QuicServerConfig config, required String addr}) +Future serverConfigWithSingleCert({required List certChain, required List key}) + +// Connections +Future<(QuicEndpoint, QuicConnection)> endpointConnect({required QuicEndpoint endpoint, required String addr, required String serverName}) +Future<(QuicConnection, QuicSendStream, QuicRecvStream)> connectionOpenBi({required QuicConnection connection}) +Future<(QuicConnection, QuicSendStream)> connectionOpenUni({required QuicConnection connection}) + +// Streams +Future sendStreamWriteAll({required QuicSendStream stream, required List data}) +Future<(QuicRecvStream, Uint8List)> recvStreamReadToEnd({required QuicRecvStream stream, required BigInt maxLength}) +``` + +--- + +## 🎯 Performance & Production Use + +### Built on Quinn's Foundation +Flutter QUIC provides the full power of Quinn (the leading Rust QUIC implementation) with Flutter-friendly APIs: + +- **Proven Performance** - Quinn is used in production by numerous organizations +- **Standards Compliant** - Full IETF QUIC implementation +- **Battle-Tested** - Over 30 releases since 2018, active development +- **Memory Efficient** - Rust's zero-cost abstractions and ownership model +- **Platform Optimized** - Tested on Linux, macOS, and Windows + +### Production Readiness +- ✅ **Memory safe** - Rust ownership prevents leaks and crashes +- ✅ **Complete API coverage** - Full 1:1 mapping of Quinn's capabilities +- ✅ **Async/await support** - Native Flutter integration patterns +- ✅ **Error handling** - Specific error types for different failure modes +- ✅ **Resource cleanup** - Automatic cleanup via Rust Drop traits + +### QUIC Protocol Benefits +Based on IETF standards and Quinn's implementation: + +- **Faster connections** - Reduced handshake overhead vs TCP+TLS +- **Stream multiplexing** - No head-of-line blocking between streams +- **Connection migration** - Maintains connections across network changes +- **Built-in encryption** - TLS 1.3 integrated into the protocol +- **Forward error correction** - Improved reliability over unreliable networks + +### Monitoring & Debugging +Connection introspection and debugging capabilities: + +```dart +// Connection state monitoring +final remoteAddr = connection.remoteAddress; +final localAddr = connection.localAddress; + +// Stream management +final streamCount = await connection.streamCount; + +// Debug logging (when enabled in Quinn) +// Detailed protocol logs available for development +``` + +--- + +## ❌ Current Limitations + +- ❌ **WebTransport** - Web platform not supported (Quinn is native only) +- ❌ **HTTP/3 semantic layer** - Raw QUIC transport only +- ❌ **Connection resumption** across app restarts +- ❌ **Advanced TLS certificate validation** - Basic validation only + +--- + +## 🤝 Contributing + +This project follows strict quality standards: + +- **No Placeholders** - All code must be production-ready +- **Real Implementation** - No mock implementations +- **Comprehensive Testing** - Unit and integration tests required +- **Documentation** - All public APIs documented + +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +--- + +## 📜 License + +MIT License - see [LICENSE](LICENSE) file. + +--- + +## 🙏 Acknowledgments + +- **[Quinn](https://github.com/quinn-rs/quinn)** - QUIC implementation +- **[flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge)** - Rust-Flutter integration +- **Google's QUIC team** - Protocol innovation +- **IETF QUIC Working Group** - Standardization + +--- + +## 🚀 Production Status + +**This library provides production-ready QUIC support by wrapping Quinn.** + +- ✅ **Quinn-powered** - Built on the industry-standard Rust QUIC implementation +- ✅ **Complete coverage** - Full access to Quinn's mature API surface +- ✅ **Flutter integration** - Native async/await patterns and error handling +- ✅ **Semantic versioning** - Stable API with clear upgrade paths +- ✅ **Active maintenance** - Regular updates following Quinn releases +- 🎯 **Performance** - Inherits Quinn's proven performance characteristics +- 📚 **Documentation** - Comprehensive API documentation and examples + +*Ready for production applications requiring modern transport protocols!* + diff --git a/vendor/flutter_quic/analysis_options.yaml b/vendor/flutter_quic/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/vendor/flutter_quic/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/vendor/flutter_quic/android/build.gradle b/vendor/flutter_quic/android/build.gradle new file mode 100644 index 0000000..86439d4 --- /dev/null +++ b/vendor/flutter_quic/android/build.gradle @@ -0,0 +1,56 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.flutter_rust_bridge.flutter_quic' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.flutter_rust_bridge.flutter_quic' + } + + // Bumping the plugin compileSdkVersion requires all clients of this plugin + // to bump the version in their app. + compileSdkVersion 33 + + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion "27.0.12077973" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + +apply from: "../cargokit/gradle/plugin.gradle" +cargokit { + manifestDir = "../rust" + libname = "flutter_quic" +} diff --git a/vendor/flutter_quic/android/settings.gradle b/vendor/flutter_quic/android/settings.gradle new file mode 100644 index 0000000..1fe7174 --- /dev/null +++ b/vendor/flutter_quic/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_quic' diff --git a/vendor/flutter_quic/android/src/main/AndroidManifest.xml b/vendor/flutter_quic/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8e2579f --- /dev/null +++ b/vendor/flutter_quic/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/vendor/flutter_quic/cargokit/LICENSE b/vendor/flutter_quic/cargokit/LICENSE new file mode 100644 index 0000000..d33a5fe --- /dev/null +++ b/vendor/flutter_quic/cargokit/LICENSE @@ -0,0 +1,42 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Copyright 2022 Matej Knopp + +================================================================================ + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +================================================================================ + +APACHE LICENSE, VERSION 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/vendor/flutter_quic/cargokit/README b/vendor/flutter_quic/cargokit/README new file mode 100644 index 0000000..398474d --- /dev/null +++ b/vendor/flutter_quic/cargokit/README @@ -0,0 +1,11 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Experimental repository to provide glue for seamlessly integrating cargo build +with flutter plugins and packages. + +See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ +for a tutorial on how to use Cargokit. + +Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. + diff --git a/vendor/flutter_quic/cargokit/build_pod.sh b/vendor/flutter_quic/cargokit/build_pod.sh new file mode 100755 index 0000000..ed0e0d9 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_pod.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +BASEDIR=$(dirname "$0") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +BASEDIR=$(cd "$BASEDIR" ; pwd -P) + +# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project +NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` + +export PATH=${NEW_PATH%?} # remove trailing : + +env + +# Platform name (macosx, iphoneos, iphonesimulator) +export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME + +# Arctive architectures (arm64, armv7, x86_64), space separated. +export CARGOKIT_DARWIN_ARCHS=$ARCHS + +# Current build configuration (Debug, Release) +export CARGOKIT_CONFIGURATION=$CONFIGURATION + +# Path to directory containing Cargo.toml. +export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 + +# Temporary directory for build artifacts. +export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR + +# Output directory for final artifacts. +export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME + +# Directory to store built tool artifacts. +export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool + +# Directory inside root project. Not necessarily the top level directory of root project. +export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT + +FLUTTER_EXPORT_BUILD_ENVIRONMENT=( + "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS + "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS +) + +for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" +do + if [[ -f "$path" ]]; then + source "$path" + fi +done + +sh "$BASEDIR/run_build_tool.sh" build-pod "$@" + +# Make a symlink from built framework to phony file, which will be used as input to +# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate +# attribute on custom build phase) +ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" +ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/vendor/flutter_quic/cargokit/build_tool/README.md b/vendor/flutter_quic/cargokit/build_tool/README.md new file mode 100644 index 0000000..a878c27 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/README.md @@ -0,0 +1,5 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/vendor/flutter_quic/cargokit/build_tool/analysis_options.yaml b/vendor/flutter_quic/cargokit/build_tool/analysis_options.yaml new file mode 100644 index 0000000..0e16a8b --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/analysis_options.yaml @@ -0,0 +1,34 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + - prefer_relative_imports + - directives_ordering + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/vendor/flutter_quic/cargokit/build_tool/bin/build_tool.dart b/vendor/flutter_quic/cargokit/build_tool/bin/build_tool.dart new file mode 100644 index 0000000..268eb52 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/bin/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:build_tool/build_tool.dart' as build_tool; + +void main(List arguments) { + build_tool.runMain(arguments); +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/build_tool.dart b/vendor/flutter_quic/cargokit/build_tool/lib/build_tool.dart new file mode 100644 index 0000000..7c1bb75 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'src/build_tool.dart' as build_tool; + +Future runMain(List args) async { + return build_tool.runMain(args); +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/android_environment.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/android_environment.dart new file mode 100644 index 0000000..15fc9ee --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/android_environment.dart @@ -0,0 +1,195 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; +import 'package:version/version.dart'; + +import 'target.dart'; +import 'util.dart'; + +class AndroidEnvironment { + AndroidEnvironment({ + required this.sdkPath, + required this.ndkVersion, + required this.minSdkVersion, + required this.targetTempDir, + required this.target, + }); + + static void clangLinkerWrapper(List args) { + final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; + if (clang == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); + } + final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; + if (target == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); + } + + runCommand(clang, [ + target, + ...args, + ]); + } + + /// Full path to Android SDK. + final String sdkPath; + + /// Full version of Android NDK. + final String ndkVersion; + + /// Minimum supported SDK version. + final int minSdkVersion; + + /// Target directory for build artifacts. + final String targetTempDir; + + /// Target being built. + final Target target; + + bool ndkIsInstalled() { + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); + return ndkPackageXml.existsSync(); + } + + void installNdk({ + required String javaHome, + }) { + final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; + final sdkManager = path.join( + sdkPath, + 'cmdline-tools', + 'latest', + 'bin', + 'sdkmanager$sdkManagerExtension', + ); + + log.info('Installing NDK $ndkVersion'); + runCommand(sdkManager, [ + '--install', + 'ndk;$ndkVersion', + ], environment: { + 'JAVA_HOME': javaHome, + }); + } + + Future> buildEnvironment() async { + final hostArch = Platform.isMacOS + ? "darwin-x86_64" + : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); + + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final toolchainPath = path.join( + ndkPath, + 'toolchains', + 'llvm', + 'prebuilt', + hostArch, + 'bin', + ); + + final minSdkVersion = + math.max(target.androidMinSdkVersion!, this.minSdkVersion); + + final exe = Platform.isWindows ? '.exe' : ''; + + final arKey = 'AR_${target.rust}'; + final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] + .map((e) => path.join(toolchainPath, e)) + .firstWhereOrNull((element) => File(element).existsSync()); + if (arValue == null) { + throw Exception('Failed to find ar for $target in $toolchainPath'); + } + + final targetArg = '--target=${target.rust}$minSdkVersion'; + + final ccKey = 'CC_${target.rust}'; + final ccValue = path.join(toolchainPath, 'clang$exe'); + final cfFlagsKey = 'CFLAGS_${target.rust}'; + final cFlagsValue = targetArg; + + final cxxKey = 'CXX_${target.rust}'; + final cxxValue = path.join(toolchainPath, 'clang++$exe'); + final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; + final cxxFlagsValue = targetArg; + + final linkerKey = + 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); + + final ranlibKey = 'RANLIB_${target.rust}'; + final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); + + final ndkVersionParsed = Version.parse(ndkVersion); + final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; + final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); + + final runRustTool = + Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; + + final packagePath = (await Isolate.resolvePackageUri( + Uri.parse('package:build_tool/buildtool.dart')))! + .toFilePath(); + final selfPath = path.canonicalize(path.join( + packagePath, + '..', + '..', + '..', + runRustTool, + )); + + // Make sure that run_build_tool is working properly even initially launched directly + // through dart run. + final toolTempDir = + Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; + + return { + arKey: arValue, + ccKey: ccValue, + cfFlagsKey: cFlagsValue, + cxxKey: cxxValue, + cxxFlagsKey: cxxFlagsValue, + ranlibKey: ranlibValue, + rustFlagsKey: rustFlagsValue, + linkerKey: selfPath, + // Recognized by main() so we know when we're acting as a wrapper + '_CARGOKIT_NDK_LINK_TARGET': targetArg, + '_CARGOKIT_NDK_LINK_CLANG': ccValue, + 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, + }; + } + + // Workaround for libgcc missing in NDK23, inspired by cargo-ndk + String _libGccWorkaround(String buildDir, Version ndkVersion) { + final workaroundDir = path.join( + buildDir, + 'cargokit', + 'libgcc_workaround', + '${ndkVersion.major}', + ); + Directory(workaroundDir).createSync(recursive: true); + if (ndkVersion.major >= 23) { + File(path.join(workaroundDir, 'libgcc.a')) + .writeAsStringSync('INPUT(-lunwind)'); + } else { + // Other way around, untested, forward libgcc.a from libunwind once Rust + // gets updated for NDK23+. + File(path.join(workaroundDir, 'libunwind.a')) + .writeAsStringSync('INPUT(-lgcc)'); + } + + var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; + if (rustFlags.isNotEmpty) { + rustFlags = '$rustFlags\x1f'; + } + rustFlags = '$rustFlags-L\x1f$workaroundDir'; + return rustFlags; + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/artifacts_provider.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/artifacts_provider.dart new file mode 100644 index 0000000..e608cec --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/artifacts_provider.dart @@ -0,0 +1,266 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'builder.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'rustup.dart'; +import 'target.dart'; + +class Artifact { + /// File system location of the artifact. + final String path; + + /// Actual file name that the artifact should have in destination folder. + final String finalFileName; + + AritifactType get type { + if (finalFileName.endsWith('.dll') || + finalFileName.endsWith('.dll.lib') || + finalFileName.endsWith('.pdb') || + finalFileName.endsWith('.so') || + finalFileName.endsWith('.dylib')) { + return AritifactType.dylib; + } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { + return AritifactType.staticlib; + } else { + throw Exception('Unknown artifact type for $finalFileName'); + } + } + + Artifact({ + required this.path, + required this.finalFileName, + }); +} + +final _log = Logger('artifacts_provider'); + +class ArtifactProvider { + ArtifactProvider({ + required this.environment, + required this.userOptions, + }); + + final BuildEnvironment environment; + final CargokitUserOptions userOptions; + + Future>> getArtifacts(List targets) async { + final result = await _getPrecompiledArtifacts(targets); + + final pendingTargets = List.of(targets); + pendingTargets.removeWhere((element) => result.containsKey(element)); + + if (pendingTargets.isEmpty) { + return result; + } + + final rustup = Rustup(); + for (final target in targets) { + final builder = RustBuilder(target: target, environment: environment); + builder.prepare(rustup); + _log.info('Building ${environment.crateInfo.packageName} for $target'); + final targetDir = await builder.build(); + // For local build accept both static and dynamic libraries. + final artifactNames = { + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.dylib, + remote: false, + ), + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.staticlib, + remote: false, + ) + }; + final artifacts = artifactNames + .map((artifactName) => Artifact( + path: path.join(targetDir, artifactName), + finalFileName: artifactName, + )) + .where((element) => File(element.path).existsSync()) + .toList(); + result[target] = artifacts; + } + return result; + } + + Future>> _getPrecompiledArtifacts( + List targets) async { + if (userOptions.usePrecompiledBinaries == false) { + _log.info('Precompiled binaries are disabled'); + return {}; + } + if (environment.crateOptions.precompiledBinaries == null) { + _log.fine('Precompiled binaries not enabled for this crate'); + return {}; + } + + final start = Stopwatch()..start(); + final crateHash = CrateHash.compute(environment.manifestDir, + tempStorage: environment.targetTempDir); + _log.fine( + 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); + + final downloadedArtifactsDir = + path.join(environment.targetTempDir, 'precompiled', crateHash); + Directory(downloadedArtifactsDir).createSync(recursive: true); + + final res = >{}; + + for (final target in targets) { + final requiredArtifacts = getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + remote: true, + ); + final artifactsForTarget = []; + + for (final artifact in requiredArtifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final downloadedPath = path.join(downloadedArtifactsDir, fileName); + if (!File(downloadedPath).existsSync()) { + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + await _tryDownloadArtifacts( + crateHash: crateHash, + fileName: fileName, + signatureFileName: signatureFileName, + finalPath: downloadedPath, + ); + } + if (File(downloadedPath).existsSync()) { + artifactsForTarget.add(Artifact( + path: downloadedPath, + finalFileName: artifact, + )); + } else { + break; + } + } + + // Only provide complete set of artifacts. + if (artifactsForTarget.length == requiredArtifacts.length) { + _log.fine('Found precompiled artifacts for $target'); + res[target] = artifactsForTarget; + } + } + + return res; + } + + static Future _get(Uri url, {Map? headers}) async { + int attempt = 0; + const maxAttempts = 10; + while (true) { + try { + return await get(url, headers: headers); + } on SocketException catch (e) { + // Try to detect reset by peer error and retry. + if (attempt++ < maxAttempts && + (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { + _log.severe( + 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); + await Future.delayed(Duration(seconds: 1)); + continue; + } else { + rethrow; + } + } + } + } + + Future _tryDownloadArtifacts({ + required String crateHash, + required String fileName, + required String signatureFileName, + required String finalPath, + }) async { + final precompiledBinaries = environment.crateOptions.precompiledBinaries!; + final prefix = precompiledBinaries.uriPrefix; + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); + _log.fine('Downloading signature from $signatureUrl'); + final signature = await _get(signatureUrl); + if (signature.statusCode == 404) { + _log.warning( + 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); + return; + } + if (signature.statusCode != 200) { + _log.severe( + 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); + return; + } + _log.fine('Downloading binary from $url'); + final res = await _get(url); + if (res.statusCode != 200) { + _log.severe('Failed to download binary $url: status ${res.statusCode}'); + return; + } + if (verify( + precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { + File(finalPath).writeAsBytesSync(res.bodyBytes); + } else { + _log.shout('Signature verification failed! Ignoring binary.'); + } + } +} + +enum AritifactType { + staticlib, + dylib, +} + +AritifactType artifactTypeForTarget(Target target) { + if (target.darwinPlatform != null) { + return AritifactType.staticlib; + } else { + return AritifactType.dylib; + } +} + +List getArtifactNames({ + required Target target, + required String libraryName, + required bool remote, + AritifactType? aritifactType, +}) { + aritifactType ??= artifactTypeForTarget(target); + if (target.darwinArch != null) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.dylib']; + } + } else if (target.rust.contains('-windows-')) { + if (aritifactType == AritifactType.staticlib) { + return ['$libraryName.lib']; + } else { + return [ + '$libraryName.dll', + '$libraryName.dll.lib', + if (!remote) '$libraryName.pdb' + ]; + } + } else if (target.rust.contains('-linux-')) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.so']; + } + } else { + throw Exception("Unsupported target: ${target.rust}"); + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/build_cmake.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_cmake.dart new file mode 100644 index 0000000..6f3b2a4 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_cmake.dart @@ -0,0 +1,40 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +class BuildCMake { + final CargokitUserOptions userOptions; + + BuildCMake({required this.userOptions}); + + Future build() async { + final targetPlatform = Environment.targetPlatform; + final target = Target.forFlutterName(Environment.targetPlatform); + if (target == null) { + throw Exception("Unknown target platform: $targetPlatform"); + } + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts([target]); + + final libs = artifacts[target]!; + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path) + .copySync(path.join(Environment.outputDir, lib.finalFileName)); + } + } + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/build_gradle.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_gradle.dart new file mode 100644 index 0000000..7e61fcb --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_gradle.dart @@ -0,0 +1,49 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +final log = Logger('build_gradle'); + +class BuildGradle { + BuildGradle({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.targetPlatforms.map((arch) { + final target = Target.forFlutterName(arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: true); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + for (final target in targets) { + final libs = artifacts[target]!; + final outputDir = path.join(Environment.outputDir, target.android!); + Directory(outputDir).createSync(recursive: true); + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); + } + } + } + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/build_pod.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_pod.dart new file mode 100644 index 0000000..8a9c0db --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_pod.dart @@ -0,0 +1,89 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +class BuildPod { + BuildPod({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.darwinArchs.map((arch) { + final target = Target.forDarwin( + platformName: Environment.darwinPlatformName, darwinAarch: arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + void performLipo(String targetFile, Iterable sourceFiles) { + runCommand("lipo", [ + '-create', + ...sourceFiles, + '-output', + targetFile, + ]); + } + + final outputDir = Environment.outputDir; + + Directory(outputDir).createSync(recursive: true); + + final staticLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.staticlib) + .toList(); + final dynamicLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.dylib) + .toList(); + + final libName = environment.crateInfo.packageName; + + // If there is static lib, use it and link it with pod + if (staticLibs.isNotEmpty) { + final finalTargetFile = path.join(outputDir, "lib$libName.a"); + performLipo(finalTargetFile, staticLibs.map((e) => e.path)); + } else { + // Otherwise try to replace bundle dylib with our dylib + final bundlePaths = [ + '$libName.framework/Versions/A/$libName', + '$libName.framework/$libName', + ]; + + for (final bundlePath in bundlePaths) { + final targetFile = path.join(outputDir, bundlePath); + if (File(targetFile).existsSync()) { + performLipo(targetFile, dynamicLibs.map((e) => e.path)); + + // Replace absolute id with @rpath one so that it works properly + // when moved to Frameworks. + runCommand("install_name_tool", [ + '-id', + '@rpath/$bundlePath', + targetFile, + ]); + return; + } + } + throw Exception('Unable to find bundle for dynamic library'); + } + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/build_tool.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_tool.dart new file mode 100644 index 0000000..c8f3698 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/build_tool.dart @@ -0,0 +1,271 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; + +import 'android_environment.dart'; +import 'build_cmake.dart'; +import 'build_gradle.dart'; +import 'build_pod.dart'; +import 'logging.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; +import 'util.dart'; +import 'verify_binaries.dart'; + +final log = Logger('build_tool'); + +abstract class BuildCommand extends Command { + Future runBuildCommand(CargokitUserOptions options); + + @override + Future run() async { + final options = CargokitUserOptions.load(); + + if (options.verboseLogging || + Platform.environment['CARGOKIT_VERBOSE'] == '1') { + enableVerboseLogging(); + } + + await runBuildCommand(options); + } +} + +class BuildPodCommand extends BuildCommand { + @override + final name = 'build-pod'; + + @override + final description = 'Build cocoa pod library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildPod(userOptions: options); + await build.build(); + } +} + +class BuildGradleCommand extends BuildCommand { + @override + final name = 'build-gradle'; + + @override + final description = 'Build android library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildGradle(userOptions: options); + await build.build(); + } +} + +class BuildCMakeCommand extends BuildCommand { + @override + final name = 'build-cmake'; + + @override + final description = 'Build CMake library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildCMake(userOptions: options); + await build.build(); + } +} + +class GenKeyCommand extends Command { + @override + final name = 'gen-key'; + + @override + final description = 'Generate key pair for signing precompiled binaries'; + + @override + void run() { + final kp = generateKey(); + final private = HEX.encode(kp.privateKey.bytes); + final public = HEX.encode(kp.publicKey.bytes); + print("Private Key: $private"); + print("Public Key: $public"); + } +} + +class PrecompileBinariesCommand extends Command { + PrecompileBinariesCommand() { + argParser + ..addOption( + 'repository', + mandatory: true, + help: 'Github repository slug in format owner/name', + ) + ..addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ) + ..addMultiOption('target', + help: 'Rust target triple of artifact to build.\n' + 'Can be specified multiple times or omitted in which case\n' + 'all targets for current platform will be built.') + ..addOption( + 'android-sdk-location', + help: 'Location of Android SDK (if available)', + ) + ..addOption( + 'android-ndk-version', + help: 'Android NDK version (if available)', + ) + ..addOption( + 'android-min-sdk-version', + help: 'Android minimum rquired version (if available)', + ) + ..addOption( + 'temp-dir', + help: 'Directory to store temporary build artifacts', + ) + ..addFlag( + "verbose", + abbr: "v", + defaultsTo: false, + help: "Enable verbose logging", + ); + } + + @override + final name = 'precompile-binaries'; + + @override + final description = 'Prebuild and upload binaries\n' + 'Private key must be passed through PRIVATE_KEY environment variable. ' + 'Use gen_key through generate priave key.\n' + 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; + + @override + Future run() async { + final verbose = argResults!['verbose'] as bool; + if (verbose) { + enableVerboseLogging(); + } + + final privateKeyString = Platform.environment['PRIVATE_KEY']; + if (privateKeyString == null) { + throw ArgumentError('Missing PRIVATE_KEY environment variable'); + } + final githubToken = Platform.environment['GITHUB_TOKEN']; + if (githubToken == null) { + throw ArgumentError('Missing GITHUB_TOKEN environment variable'); + } + final privateKey = HEX.decode(privateKeyString); + if (privateKey.length != 64) { + throw ArgumentError('Private key must be 64 bytes long'); + } + final manifestDir = argResults!['manifest-dir'] as String; + if (!Directory(manifestDir).existsSync()) { + throw ArgumentError('Manifest directory does not exist: $manifestDir'); + } + String? androidMinSdkVersionString = + argResults!['android-min-sdk-version'] as String?; + int? androidMinSdkVersion; + if (androidMinSdkVersionString != null) { + androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); + if (androidMinSdkVersion == null) { + throw ArgumentError( + 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); + } + } + final targetStrigns = argResults!['target'] as List; + final targets = targetStrigns.map((target) { + final res = Target.forRustTriple(target); + if (res == null) { + throw ArgumentError('Invalid target: $target'); + } + return res; + }).toList(growable: false); + final precompileBinaries = PrecompileBinaries( + privateKey: PrivateKey(privateKey), + githubToken: githubToken, + manifestDir: manifestDir, + repositorySlug: RepositorySlug.full(argResults!['repository'] as String), + targets: targets, + androidSdkLocation: argResults!['android-sdk-location'] as String?, + androidNdkVersion: argResults!['android-ndk-version'] as String?, + androidMinSdkVersion: androidMinSdkVersion, + tempDir: argResults!['temp-dir'] as String?, + ); + + await precompileBinaries.run(); + } +} + +class VerifyBinariesCommand extends Command { + VerifyBinariesCommand() { + argParser.addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ); + } + + @override + final name = "verify-binaries"; + + @override + final description = 'Verifies published binaries\n' + 'Checks whether there is a binary published for each targets\n' + 'and checks the signature.'; + + @override + Future run() async { + final manifestDir = argResults!['manifest-dir'] as String; + final verifyBinaries = VerifyBinaries( + manifestDir: manifestDir, + ); + await verifyBinaries.run(); + } +} + +Future runMain(List args) async { + try { + // Init logging before options are loaded + initLogging(); + + if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { + return AndroidEnvironment.clangLinkerWrapper(args); + } + + final runner = CommandRunner('build_tool', 'Cargokit built_tool') + ..addCommand(BuildPodCommand()) + ..addCommand(BuildGradleCommand()) + ..addCommand(BuildCMakeCommand()) + ..addCommand(GenKeyCommand()) + ..addCommand(PrecompileBinariesCommand()) + ..addCommand(VerifyBinariesCommand()); + + await runner.run(args); + } on ArgumentError catch (e) { + stderr.writeln(e.toString()); + exit(1); + } catch (e, s) { + log.severe(kDoubleSeparator); + log.severe('Cargokit BuildTool failed with error:'); + log.severe(kSeparator); + log.severe(e); + // This tells user to install Rust, there's no need to pollute the log with + // stack trace. + if (e is! RustupNotFoundException) { + log.severe(kSeparator); + log.severe(s); + log.severe(kSeparator); + log.severe('BuildTool arguments: $args'); + } + log.severe(kDoubleSeparator); + exit(1); + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/builder.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/builder.dart new file mode 100644 index 0000000..84c46e4 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/builder.dart @@ -0,0 +1,198 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'android_environment.dart'; +import 'cargo.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; +import 'util.dart'; + +final _log = Logger('builder'); + +enum BuildConfiguration { + debug, + release, + profile, +} + +extension on BuildConfiguration { + bool get isDebug => this == BuildConfiguration.debug; + String get rustName => switch (this) { + BuildConfiguration.debug => 'debug', + BuildConfiguration.release => 'release', + BuildConfiguration.profile => 'release', + }; +} + +class BuildException implements Exception { + final String message; + + BuildException(this.message); + + @override + String toString() { + return 'BuildException: $message'; + } +} + +class BuildEnvironment { + final BuildConfiguration configuration; + final CargokitCrateOptions crateOptions; + final String targetTempDir; + final String manifestDir; + final CrateInfo crateInfo; + + final bool isAndroid; + final String? androidSdkPath; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? javaHome; + + BuildEnvironment({ + required this.configuration, + required this.crateOptions, + required this.targetTempDir, + required this.manifestDir, + required this.crateInfo, + required this.isAndroid, + this.androidSdkPath, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.javaHome, + }); + + static BuildConfiguration parseBuildConfiguration(String value) { + // XCode configuration adds the flavor to configuration name. + final firstSegment = value.split('-').first; + final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( + (e) => e.name == firstSegment, + ); + if (buildConfiguration == null) { + _log.warning('Unknown build configuraiton $value, will assume release'); + return BuildConfiguration.release; + } + return buildConfiguration; + } + + static BuildEnvironment fromEnvironment({ + required bool isAndroid, + }) { + final buildConfiguration = + parseBuildConfiguration(Environment.configuration); + final manifestDir = Environment.manifestDir; + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + final crateInfo = CrateInfo.load(manifestDir); + return BuildEnvironment( + configuration: buildConfiguration, + crateOptions: crateOptions, + targetTempDir: Environment.targetTempDir, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: isAndroid, + androidSdkPath: isAndroid ? Environment.sdkPath : null, + androidNdkVersion: isAndroid ? Environment.ndkVersion : null, + androidMinSdkVersion: + isAndroid ? int.parse(Environment.minSdkVersion) : null, + javaHome: isAndroid ? Environment.javaHome : null, + ); + } +} + +class RustBuilder { + final Target target; + final BuildEnvironment environment; + + RustBuilder({ + required this.target, + required this.environment, + }); + + void prepare( + Rustup rustup, + ) { + final toolchain = _toolchain; + if (rustup.installedTargets(toolchain) == null) { + rustup.installToolchain(toolchain); + } + if (toolchain == 'nightly') { + rustup.installRustSrcForNightly(); + } + if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { + rustup.installTarget(target.rust, toolchain: toolchain); + } + } + + CargoBuildOptions? get _buildOptions => + environment.crateOptions.cargo[environment.configuration]; + + String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; + + /// Returns the path of directory containing build artifacts. + Future build() async { + final extraArgs = _buildOptions?.flags ?? []; + final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); + runCommand( + 'rustup', + [ + 'run', + _toolchain, + 'cargo', + 'build', + ...extraArgs, + '--manifest-path', + manifestPath, + '-p', + environment.crateInfo.packageName, + if (!environment.configuration.isDebug) '--release', + '--target', + target.rust, + '--target-dir', + environment.targetTempDir, + ], + environment: await _buildEnvironment(), + ); + return path.join( + environment.targetTempDir, + target.rust, + environment.configuration.rustName, + ); + } + + Future> _buildEnvironment() async { + if (target.android == null) { + return {}; + } else { + final sdkPath = environment.androidSdkPath; + final ndkVersion = environment.androidNdkVersion; + final minSdkVersion = environment.androidMinSdkVersion; + if (sdkPath == null) { + throw BuildException('androidSdkPath is not set'); + } + if (ndkVersion == null) { + throw BuildException('androidNdkVersion is not set'); + } + if (minSdkVersion == null) { + throw BuildException('androidMinSdkVersion is not set'); + } + final env = AndroidEnvironment( + sdkPath: sdkPath, + ndkVersion: ndkVersion, + minSdkVersion: minSdkVersion, + targetTempDir: environment.targetTempDir, + target: target, + ); + if (!env.ndkIsInstalled() && environment.javaHome != null) { + env.installNdk(javaHome: environment.javaHome!); + } + return env.buildEnvironment(); + } + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/cargo.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/cargo.dart new file mode 100644 index 0000000..0d8958f --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/cargo.dart @@ -0,0 +1,48 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +class ManifestException { + ManifestException(this.message, {required this.fileName}); + + final String? fileName; + final String message; + + @override + String toString() { + if (fileName != null) { + return 'Failed to parse package manifest at $fileName: $message'; + } else { + return 'Failed to parse package manifest: $message'; + } + } +} + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + static CrateInfo parseManifest(String manifest, {final String? fileName}) { + final toml = TomlDocument.parse(manifest); + final package = toml.toMap()['package']; + if (package == null) { + throw ManifestException('Missing package section', fileName: fileName); + } + final name = package['name']; + if (name == null) { + throw ManifestException('Missing package name', fileName: fileName); + } + return CrateInfo(packageName: name); + } + + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + return parseManifest(manifest, fileName: manifestFile.path); + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/crate_hash.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/crate_hash.dart new file mode 100644 index 0000000..0c4d88d --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/crate_hash.dart @@ -0,0 +1,124 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; + +class CrateHash { + /// Computes a hash uniquely identifying crate content. This takes into account + /// content all all .rs files inside the src directory, as well as Cargo.toml, + /// Cargo.lock, build.rs and cargokit.yaml. + /// + /// If [tempStorage] is provided, computed hash is stored in a file in that directory + /// and reused on subsequent calls if the crate content hasn't changed. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({ + required this.manifestDir, + required this.tempStorage, + }); + + String _compute() { + final files = getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } else { + return _computeHash(files); + } + } + + /// Computes a quick hash based on files stat (without reading contents). This + /// is used to cache the real hash, which is slower to compute since it involves + /// reading every single file. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + // text Files are hashed by lines in case we're dealing with github checkout + // that auto-converts line endings. + final splitter = LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + + // Truncate to 128bits. + final hash = res.bytes.sublist(0, 16); + return hex.encode(hash); + } + + List getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList(); + files.sortBy((element) => element.path); + void addFile(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFile('Cargo.toml'); + addFile('Cargo.lock'); + addFile('build.rs'); + addFile('cargokit.yaml'); + return files; + } + + final String manifestDir; + final String? tempStorage; +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/environment.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/environment.dart new file mode 100644 index 0000000..996483a --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/environment.dart @@ -0,0 +1,68 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +extension on String { + String resolveSymlink() => File(this).resolveSymbolicLinksSync(); +} + +class Environment { + /// Current build configuration (debug or release). + static String get configuration => + _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); + + static bool get isDebug => configuration == 'debug'; + static bool get isRelease => configuration == 'release'; + + /// Temporary directory where Rust build artifacts are placed. + static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); + + /// Final output directory where the build artifacts are placed. + static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); + + /// Path to the crate manifest (containing Cargo.toml). + static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); + + /// Directory inside root project. Not necessarily root folder. Symlinks are + /// not resolved on purpose. + static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); + + // Pod + + /// Platform name (macosx, iphoneos, iphonesimulator). + static String get darwinPlatformName => + _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); + + /// List of architectures to build for (arm64, armv7, x86_64). + static List get darwinArchs => + _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); + + // Gradle + static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); + static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); + static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); + static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); + static List get targetPlatforms => + _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); + + // CMAKE + static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); + + static String _getEnv(String key) { + final res = Platform.environment[key]; + if (res == null) { + throw Exception("Missing environment variable $key"); + } + return res; + } + + static String _getEnvPath(String key) { + final res = _getEnv(key); + if (Directory(res).existsSync()) { + return res.resolveSymlink(); + } else { + return res; + } + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/logging.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/logging.dart new file mode 100644 index 0000000..5edd4fd --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/logging.dart @@ -0,0 +1,52 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; + +const String kSeparator = "--"; +const String kDoubleSeparator = "=="; + +bool _lastMessageWasSeparator = false; + +void _log(LogRecord rec) { + final prefix = '${rec.level.name}: '; + final out = rec.level == Level.SEVERE ? stderr : stdout; + if (rec.message == kSeparator) { + if (!_lastMessageWasSeparator) { + out.write(prefix); + out.writeln('-' * 80); + _lastMessageWasSeparator = true; + } + return; + } else if (rec.message == kDoubleSeparator) { + out.write(prefix); + out.writeln('=' * 80); + _lastMessageWasSeparator = true; + return; + } + out.write(prefix); + out.writeln(rec.message); + _lastMessageWasSeparator = false; +} + +void initLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((LogRecord rec) { + final lines = rec.message.split('\n'); + for (final line in lines) { + if (line.isNotEmpty || lines.length == 1 || line != lines.last) { + _log(LogRecord( + rec.level, + line, + rec.loggerName, + )); + } + } + }); +} + +void enableVerboseLogging() { + Logger.root.level = Level.ALL; +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/options.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/options.dart new file mode 100644 index 0000000..22aef1d --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/options.dart @@ -0,0 +1,309 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'builder.dart'; +import 'environment.dart'; +import 'rustup.dart'; + +final _log = Logger('options'); + +/// A class for exceptions that have source span information attached. +class SourceSpanException implements Exception { + // This is a getter so that subclasses can override it. + /// A message describing the exception. + String get message => _message; + final String _message; + + // This is a getter so that subclasses can override it. + /// The span associated with this exception. + /// + /// This may be `null` if the source location can't be determined. + SourceSpan? get span => _span; + final SourceSpan? _span; + + SourceSpanException(this._message, this._span); + + /// Returns a string representation of `this`. + /// + /// [color] may either be a [String], a [bool], or `null`. If it's a string, + /// it indicates an ANSI terminal color escape that should be used to + /// highlight the span's text. If it's `true`, it indicates that the text + /// should be highlighted using the default color. If it's `false` or `null`, + /// it indicates that the text shouldn't be highlighted. + @override + String toString({Object? color}) { + if (span == null) return message; + return 'Error on ${span!.message(message, color: color)}'; + } +} + +enum Toolchain { + stable, + beta, + nightly, +} + +class CargoBuildOptions { + final Toolchain toolchain; + final List flags; + + CargoBuildOptions({ + required this.toolchain, + required this.flags, + }); + + static Toolchain _toolchainFromNode(YamlNode node) { + if (node case YamlScalar(value: String name)) { + final toolchain = + Toolchain.values.firstWhereOrNull((element) => element.name == name); + if (toolchain != null) { + return toolchain; + } + } + throw SourceSpanException( + 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', + node.span); + } + + static CargoBuildOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + Toolchain toolchain = Toolchain.stable; + List flags = []; + for (final MapEntry(:key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: 'toolchain')) { + toolchain = _toolchainFromNode(value); + } else if (key case YamlScalar(value: 'extra_flags')) { + if (value case YamlList(nodes: List list)) { + if (list.every((element) { + if (element case YamlScalar(value: String _)) { + return true; + } + return false; + })) { + flags = list.map((e) => e.value as String).toList(); + continue; + } + } + throw SourceSpanException( + 'Extra flags must be a list of strings', value.span); + } else { + throw SourceSpanException( + 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', + key.span); + } + } + return CargoBuildOptions(toolchain: toolchain, flags: flags); + } +} + +extension on YamlMap { + /// Map that extracts keys so that we can do map case check on them. + Map get valueMap => + nodes.map((key, value) => MapEntry(key.value, value)); +} + +class PrecompiledBinaries { + final String uriPrefix; + final PublicKey publicKey; + + PrecompiledBinaries({ + required this.uriPrefix, + required this.publicKey, + }); + + static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { + final bytes = HEX.decode(key); + if (bytes.length != 32) { + throw SourceSpanException( + 'Invalid public key. Must be 32 bytes long.', span); + } + return PublicKey(bytes); + } + + static PrecompiledBinaries parse(YamlNode node) { + if (node case YamlMap(valueMap: Map map)) { + if (map + case { + 'url_prefix': YamlNode urlPrefixNode, + 'public_key': YamlNode publicKeyNode, + }) { + final urlPrefix = switch (urlPrefixNode) { + YamlScalar(value: String urlPrefix) => urlPrefix, + _ => throw SourceSpanException( + 'Invalid URL prefix value.', urlPrefixNode.span), + }; + final publicKey = switch (publicKeyNode) { + YamlScalar(value: String publicKey) => + _publicKeyFromHex(publicKey, publicKeyNode.span), + _ => throw SourceSpanException( + 'Invalid public key value.', publicKeyNode.span), + }; + return PrecompiledBinaries( + uriPrefix: urlPrefix, + publicKey: publicKey, + ); + } + } + throw SourceSpanException( + 'Invalid precompiled binaries value. ' + 'Expected Map with "url_prefix" and "public_key".', + node.span); + } +} + +/// Cargokit options specified for Rust crate. +class CargokitCrateOptions { + CargokitCrateOptions({ + this.cargo = const {}, + this.precompiledBinaries, + }); + + final Map cargo; + final PrecompiledBinaries? precompiledBinaries; + + static CargokitCrateOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + final options = {}; + PrecompiledBinaries? precompiledBinaries; + + for (final entry in node.nodes.entries) { + if (entry + case MapEntry( + key: YamlScalar(value: 'cargo'), + value: YamlNode node, + )) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: String name)) { + final configuration = BuildConfiguration.values + .firstWhereOrNull((element) => element.name == name); + if (configuration != null) { + options[configuration] = CargoBuildOptions.parse(value); + continue; + } + } + throw SourceSpanException( + 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', + key.span); + } + } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { + precompiledBinaries = PrecompiledBinaries.parse(entry.value); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', + entry.key.span); + } + } + return CargokitCrateOptions( + cargo: options, + precompiledBinaries: precompiledBinaries, + ); + } + + static CargokitCrateOptions load({ + required String manifestDir, + }) { + final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); + final file = File.fromUri(uri); + if (file.existsSync()) { + final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); + return parse(contents); + } else { + return CargokitCrateOptions(); + } + } +} + +class CargokitUserOptions { + // When Rustup is installed always build locally unless user opts into + // using precompiled binaries. + static bool defaultUsePrecompiledBinaries() { + return Rustup.executablePath() == null; + } + + CargokitUserOptions({ + required this.usePrecompiledBinaries, + required this.verboseLogging, + }); + + CargokitUserOptions._() + : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), + verboseLogging = false; + + static CargokitUserOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); + bool verboseLogging = false; + + for (final entry in node.nodes.entries) { + if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { + if (entry.value case YamlScalar(value: bool value)) { + usePrecompiledBinaries = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "use_precompiled_binaries". Must be a boolean.', + entry.value.span); + } else if (entry.key case YamlScalar(value: 'verbose_logging')) { + if (entry.value case YamlScalar(value: bool value)) { + verboseLogging = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "verbose_logging". Must be a boolean.', + entry.value.span); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', + entry.key.span); + } + } + return CargokitUserOptions( + usePrecompiledBinaries: usePrecompiledBinaries, + verboseLogging: verboseLogging, + ); + } + + static CargokitUserOptions load() { + String fileName = "cargokit_options.yaml"; + var userProjectDir = Directory(Environment.rootProjectDir); + + while (userProjectDir.parent.path != userProjectDir.path) { + final configFile = File(path.join(userProjectDir.path, fileName)); + if (configFile.existsSync()) { + final contents = loadYamlNode( + configFile.readAsStringSync(), + sourceUrl: configFile.uri, + ); + final res = parse(contents); + if (res.verboseLogging) { + _log.info('Found user options file at ${configFile.path}'); + } + return res; + } + userProjectDir = userProjectDir.parent; + } + return CargokitUserOptions._(); + } + + final bool usePrecompiledBinaries; + final bool verboseLogging; +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/precompile_binaries.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/precompile_binaries.dart new file mode 100644 index 0000000..c27f419 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/precompile_binaries.dart @@ -0,0 +1,202 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; + +final _log = Logger('precompile_binaries'); + +class PrecompileBinaries { + PrecompileBinaries({ + required this.privateKey, + required this.githubToken, + required this.repositorySlug, + required this.manifestDir, + required this.targets, + this.androidSdkLocation, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.tempDir, + }); + + final PrivateKey privateKey; + final String githubToken; + final RepositorySlug repositorySlug; + final String manifestDir; + final List targets; + final String? androidSdkLocation; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? tempDir; + + static String fileName(Target target, String name) { + return '${target.rust}_$name'; + } + + static String signatureFileName(Target target, String name) { + return '${target.rust}_$name.sig'; + } + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final targets = List.of(this.targets); + if (targets.isEmpty) { + targets.addAll([ + ...Target.buildableTargets(), + if (androidSdkLocation != null) ...Target.androidTargets(), + ]); + } + + _log.info('Precompiling binaries for $targets'); + + final hash = CrateHash.compute(manifestDir); + _log.info('Computed crate hash: $hash'); + + final String tagName = 'precompiled_$hash'; + + final github = GitHub(auth: Authentication.withToken(githubToken)); + final repo = github.repositories; + final release = await _getOrCreateRelease( + repo: repo, + tagName: tagName, + packageName: crateInfo.packageName, + hash: hash, + ); + + final tempDir = this.tempDir != null + ? Directory(this.tempDir!) + : Directory.systemTemp.createTempSync('precompiled_'); + + tempDir.createSync(recursive: true); + + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + + final buildEnvironment = BuildEnvironment( + configuration: BuildConfiguration.release, + crateOptions: crateOptions, + targetTempDir: tempDir.path, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: androidSdkLocation != null, + androidSdkPath: androidSdkLocation, + androidNdkVersion: androidNdkVersion, + androidMinSdkVersion: androidMinSdkVersion, + ); + + final rustup = Rustup(); + + for (final target in targets) { + final artifactNames = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + if (artifactNames.every((name) { + final fileName = PrecompileBinaries.fileName(target, name); + return (release.assets ?? []).any((e) => e.name == fileName); + })) { + _log.info("All artifacts for $target already exist - skipping"); + continue; + } + + _log.info('Building for $target'); + + final builder = + RustBuilder(target: target, environment: buildEnvironment); + builder.prepare(rustup); + final res = await builder.build(); + + final assets = []; + for (final name in artifactNames) { + final file = File(path.join(res, name)); + if (!file.existsSync()) { + throw Exception('Missing artifact: ${file.path}'); + } + + final data = file.readAsBytesSync(); + final create = CreateReleaseAsset( + name: PrecompileBinaries.fileName(target, name), + contentType: "application/octet-stream", + assetData: data, + ); + final signature = sign(privateKey, data); + final signatureCreate = CreateReleaseAsset( + name: signatureFileName(target, name), + contentType: "application/octet-stream", + assetData: signature, + ); + bool verified = verify(public(privateKey), data, signature); + if (!verified) { + throw Exception('Signature verification failed'); + } + assets.add(create); + assets.add(signatureCreate); + } + _log.info('Uploading assets: ${assets.map((e) => e.name)}'); + for (final asset in assets) { + // This seems to be failing on CI so do it one by one + int retryCount = 0; + while (true) { + try { + await repo.uploadReleaseAssets(release, [asset]); + break; + } on Exception catch (e) { + if (retryCount == 10) { + rethrow; + } + ++retryCount; + _log.shout( + 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); + await Future.delayed(Duration(seconds: 2)); + } + } + } + } + + _log.info('Cleaning up'); + tempDir.deleteSync(recursive: true); + } + + Future _getOrCreateRelease({ + required RepositoriesService repo, + required String tagName, + required String packageName, + required String hash, + }) async { + Release release; + try { + _log.info('Fetching release $tagName'); + release = await repo.getReleaseByTagName(repositorySlug, tagName); + } on ReleaseNotFound { + _log.info('Release not found - creating release $tagName'); + release = await repo.createRelease( + repositorySlug, + CreateRelease.from( + tagName: tagName, + name: 'Precompiled binaries ${hash.substring(0, 8)}', + targetCommitish: null, + isDraft: false, + isPrerelease: false, + body: 'Precompiled binaries for crate $packageName, ' + 'crate hash $hash.', + )); + } + return release; + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/rustup.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/rustup.dart new file mode 100644 index 0000000..0ac8d08 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/rustup.dart @@ -0,0 +1,136 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; + +import 'util.dart'; + +class _Toolchain { + _Toolchain( + this.name, + this.targets, + ); + + final String name; + final List targets; +} + +class Rustup { + List? installedTargets(String toolchain) { + final targets = _installedTargets(toolchain); + return targets != null ? List.unmodifiable(targets) : null; + } + + void installToolchain(String toolchain) { + log.info("Installing Rust toolchain: $toolchain"); + runCommand("rustup", ['toolchain', 'install', toolchain]); + _installedToolchains + .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); + } + + void installTarget( + String target, { + required String toolchain, + }) { + log.info("Installing Rust target: $target"); + runCommand("rustup", [ + 'target', + 'add', + '--toolchain', + toolchain, + target, + ]); + _installedTargets(toolchain)?.add(target); + } + + final List<_Toolchain> _installedToolchains; + + Rustup() : _installedToolchains = _getInstalledToolchains(); + + List? _installedTargets(String toolchain) => _installedToolchains + .firstWhereOrNull( + (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) + ?.targets; + + static List<_Toolchain> _getInstalledToolchains() { + String extractToolchainName(String line) { + // ignore (default) after toolchain name + final parts = line.split(' '); + return parts[0]; + } + + final res = runCommand("rustup", ['toolchain', 'list']); + + // To list all non-custom toolchains, we need to filter out lines that + // don't start with "stable", "beta", or "nightly". + Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) + .map(extractToolchainName) + .toList(growable: true); + + return lines + .map( + (name) => _Toolchain( + name, + _getInstalledTargets(name), + ), + ) + .toList(growable: true); + } + + static List _getInstalledTargets(String toolchain) { + final res = runCommand("rustup", [ + 'target', + 'list', + '--toolchain', + toolchain, + '--installed', + ]); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty) + .toList(growable: true); + return lines; + } + + bool _didInstallRustSrcForNightly = false; + + void installRustSrcForNightly() { + if (_didInstallRustSrcForNightly) { + return; + } + // Useful for -Z build-std + runCommand( + "rustup", + ['component', 'add', 'rust-src', '--toolchain', 'nightly'], + ); + _didInstallRustSrcForNightly = true; + } + + static String? executablePath() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + final rustupPath = path.join(p, rustup); + if (File(rustupPath).existsSync()) { + return rustupPath; + } + } + return null; + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/target.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/target.dart new file mode 100644 index 0000000..6fbc58b --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/target.dart @@ -0,0 +1,140 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import 'util.dart'; + +class Target { + Target({ + required this.rust, + this.flutter, + this.android, + this.androidMinSdkVersion, + this.darwinPlatform, + this.darwinArch, + }); + + static final all = [ + Target( + rust: 'armv7-linux-androideabi', + flutter: 'android-arm', + android: 'armeabi-v7a', + androidMinSdkVersion: 16, + ), + Target( + rust: 'aarch64-linux-android', + flutter: 'android-arm64', + android: 'arm64-v8a', + androidMinSdkVersion: 21, + ), + Target( + rust: 'i686-linux-android', + flutter: 'android-x86', + android: 'x86', + androidMinSdkVersion: 16, + ), + Target( + rust: 'x86_64-linux-android', + flutter: 'android-x64', + android: 'x86_64', + androidMinSdkVersion: 21, + ), + Target( + rust: 'x86_64-pc-windows-msvc', + flutter: 'windows-x64', + ), + Target( + rust: 'x86_64-unknown-linux-gnu', + flutter: 'linux-x64', + ), + Target( + rust: 'aarch64-unknown-linux-gnu', + flutter: 'linux-arm64', + ), + Target( + rust: 'x86_64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'x86_64', + ), + Target( + rust: 'aarch64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios', + darwinPlatform: 'iphoneos', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios-sim', + darwinPlatform: 'iphonesimulator', + darwinArch: 'arm64', + ), + Target( + rust: 'x86_64-apple-ios', + darwinPlatform: 'iphonesimulator', + darwinArch: 'x86_64', + ), + ]; + + static Target? forFlutterName(String flutterName) { + return all.firstWhereOrNull((element) => element.flutter == flutterName); + } + + static Target? forDarwin({ + required String platformName, + required String darwinAarch, + }) { + return all.firstWhereOrNull((element) => // + element.darwinPlatform == platformName && + element.darwinArch == darwinAarch); + } + + static Target? forRustTriple(String triple) { + return all.firstWhereOrNull((element) => element.rust == triple); + } + + static List androidTargets() { + return all + .where((element) => element.android != null) + .toList(growable: false); + } + + /// Returns buildable targets on current host platform ignoring Android targets. + static List buildableTargets() { + if (Platform.isLinux) { + // Right now we don't support cross-compiling on Linux. So we just return + // the host target. + final arch = runCommand('arch', []).stdout as String; + if (arch.trim() == 'aarch64') { + return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; + } else { + return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; + } + } + return all.where((target) { + if (Platform.isWindows) { + return target.rust.contains('-windows-'); + } else if (Platform.isMacOS) { + return target.darwinPlatform != null; + } + return false; + }).toList(growable: false); + } + + @override + String toString() { + return rust; + } + + final String? flutter; + final String rust; + final String? android; + final int? androidMinSdkVersion; + final String? darwinPlatform; + final String? darwinArch; +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/util.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/util.dart new file mode 100644 index 0000000..8bb6a87 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/util.dart @@ -0,0 +1,172 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'logging.dart'; +import 'rustup.dart'; + +final log = Logger("process"); + +class CommandFailedException implements Exception { + final String executable; + final List arguments; + final ProcessResult result; + + CommandFailedException({ + required this.executable, + required this.arguments, + required this.result, + }); + + @override + String toString() { + final stdout = result.stdout.toString().trim(); + final stderr = result.stderr.toString().trim(); + return [ + "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", + "Returned Exit Code: ${result.exitCode}", + kSeparator, + "STDOUT:", + if (stdout.isNotEmpty) stdout, + kSeparator, + "STDERR:", + if (stderr.isNotEmpty) stderr, + ].join('\n'); + } +} + +class TestRunCommandArgs { + final String executable; + final List arguments; + final String? workingDirectory; + final Map? environment; + final bool includeParentEnvironment; + final bool runInShell; + final Encoding? stdoutEncoding; + final Encoding? stderrEncoding; + + TestRunCommandArgs({ + required this.executable, + required this.arguments, + this.workingDirectory, + this.environment, + this.includeParentEnvironment = true, + this.runInShell = false, + this.stdoutEncoding, + this.stderrEncoding, + }); +} + +class TestRunCommandResult { + TestRunCommandResult({ + this.pid = 1, + this.exitCode = 0, + this.stdout = '', + this.stderr = '', + }); + + final int pid; + final int exitCode; + final String stdout; + final String stderr; +} + +TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; + +ProcessResult runCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, +}) { + if (testRunCommandOverride != null) { + final result = testRunCommandOverride!(TestRunCommandArgs( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + )); + return ProcessResult( + result.pid, + result.exitCode, + result.stdout, + result.stderr, + ); + } + log.finer('Running command $executable ${arguments.join(' ')}'); + final res = Process.runSync( + _resolveExecutable(executable), + arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + ); + if (res.exitCode != 0) { + throw CommandFailedException( + executable: executable, + arguments: arguments, + result: res, + ); + } else { + return res; + } +} + +class RustupNotFoundException implements Exception { + @override + String toString() { + return [ + ' ', + 'rustup not found in PATH.', + ' ', + 'Maybe you need to install Rust? It only takes a minute:', + ' ', + if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', + if (hasHomebrewRustInPath()) ...[ + '\$ brew unlink rust # Unlink homebrew Rust from PATH', + ], + if (!Platform.isWindows) + "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", + ' ', + ].join('\n'); + } + + static bool hasHomebrewRustInPath() { + if (!Platform.isMacOS) { + return false; + } + final envPath = Platform.environment['PATH'] ?? ''; + final paths = envPath.split(':'); + return paths.any((p) { + return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); + }); + } +} + +String _resolveExecutable(String executable) { + if (executable == 'rustup') { + final resolved = Rustup.executablePath(); + if (resolved != null) { + return resolved; + } + throw RustupNotFoundException(); + } else { + return executable; + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/lib/src/verify_binaries.dart b/vendor/flutter_quic/cargokit/build_tool/lib/src/verify_binaries.dart new file mode 100644 index 0000000..2366b57 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/lib/src/verify_binaries.dart @@ -0,0 +1,84 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; + +import 'artifacts_provider.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; + +class VerifyBinaries { + VerifyBinaries({ + required this.manifestDir, + }); + + final String manifestDir; + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final config = CargokitCrateOptions.load(manifestDir: manifestDir); + final precompiledBinaries = config.precompiledBinaries; + if (precompiledBinaries == null) { + stdout.writeln('Crate does not support precompiled binaries.'); + } else { + final crateHash = CrateHash.compute(manifestDir); + stdout.writeln('Crate hash: $crateHash'); + + for (final target in Target.all) { + final message = 'Checking ${target.rust}...'; + stdout.write(message.padRight(40)); + stdout.flush(); + + final artifacts = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + final prefix = precompiledBinaries.uriPrefix; + + bool ok = true; + + for (final artifact in artifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = + Uri.parse('$prefix$crateHash/$signatureFileName'); + + final signature = await get(signatureUrl); + if (signature.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + final asset = await get(url); + if (asset.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + + if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, + signature.bodyBytes)) { + stdout.writeln('INVALID SIGNATURE'); + ok = false; + } + } + + if (ok) { + stdout.writeln('OK'); + } + } + } + } +} diff --git a/vendor/flutter_quic/cargokit/build_tool/pubspec.lock b/vendor/flutter_quic/cargokit/build_tool/pubspec.lock new file mode 100644 index 0000000..343bdd3 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/pubspec.lock @@ -0,0 +1,453 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: "direct main" + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: "direct main" + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + ed25519_edwards: + dependency: "direct main" + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + github: + dependency: "direct main" + description: + name: github + sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" + url: "https://pub.dev" + source: hosted + version: "9.17.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: "direct main" + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + toml: + dependency: "direct main" + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + version: + dependency: "direct main" + description: + name: version + sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + url: "https://pub.dev" + source: hosted + version: "11.9.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/vendor/flutter_quic/cargokit/build_tool/pubspec.yaml b/vendor/flutter_quic/cargokit/build_tool/pubspec.yaml new file mode 100644 index 0000000..18c61e3 --- /dev/null +++ b/vendor/flutter_quic/cargokit/build_tool/pubspec.yaml @@ -0,0 +1,33 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +name: build_tool +description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. +publish_to: none +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + +# Add regular dependencies here. +dependencies: + # these are pinned on purpose because the bundle_tool_runner doesn't have + # pubspec.lock. See run_build_tool.sh + logging: 1.2.0 + path: 1.8.0 + version: 3.0.0 + collection: 1.18.0 + ed25519_edwards: 0.3.1 + hex: 0.2.0 + yaml: 3.1.2 + source_span: 1.10.0 + github: 9.17.0 + args: 2.4.2 + crypto: 3.0.3 + convert: 3.1.1 + http: 1.1.0 + toml: 0.14.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/vendor/flutter_quic/cargokit/cmake/cargokit.cmake b/vendor/flutter_quic/cargokit/cmake/cargokit.cmake new file mode 100644 index 0000000..ddd05df --- /dev/null +++ b/vendor/flutter_quic/cargokit/cmake/cargokit.cmake @@ -0,0 +1,99 @@ +SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) + +if(WIN32) + # REALPATH does not properly resolve symlinks on windows :-/ + execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +# Arguments +# - target: CMAKE target to which rust library is linked +# - manifest_dir: relative path from current folder to directory containing cargo manifest +# - lib_name: cargo package name +# - any_symbol_name: name of any exported symbol from the library. +# used on windows to force linking with library. +function(apply_cargokit target manifest_dir lib_name any_symbol_name) + + set(CARGOKIT_LIB_NAME "${lib_name}") + set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") + if (CMAKE_CONFIGURATION_TYPES) + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") + else() + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") + endif() + set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") + + if (FLUTTER_TARGET_PLATFORM) + set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") + else() + set(CARGOKIT_TARGET_PLATFORM "windows-x64") + endif() + + set(CARGOKIT_ENV + "CARGOKIT_CMAKE=${CMAKE_COMMAND}" + "CARGOKIT_CONFIGURATION=$" + "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" + "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" + "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" + "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" + "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" + "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" + ) + + if (WIN32) + set(SCRIPT_EXTENSION ".cmd") + set(IMPORT_LIB_EXTENSION ".lib") + else() + set(SCRIPT_EXTENSION ".sh") + set(IMPORT_LIB_EXTENSION "") + execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") + endif() + + # Using generators in custom command is only supported in CMake 3.20+ + if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") + foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endforeach() + else() + add_custom_command( + OUTPUT + ${OUTPUT_LIB} + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endif() + + + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) + + if (TARGET ${target}) + # If we have actual cmake target provided create target and make existing + # target depend on it + add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) + add_dependencies("${target}" "${target}_cargokit") + target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") + if(WIN32) + target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") + endif() + else() + # Otherwise (FFI) just use ALL to force building always + add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) + endif() + + # Allow adding the output library to plugin bundled libraries + set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) + +endfunction() diff --git a/vendor/flutter_quic/cargokit/cmake/resolve_symlinks.ps1 b/vendor/flutter_quic/cargokit/cmake/resolve_symlinks.ps1 new file mode 100644 index 0000000..2ac593a --- /dev/null +++ b/vendor/flutter_quic/cargokit/cmake/resolve_symlinks.ps1 @@ -0,0 +1,34 @@ +function Resolve-Symlinks { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string] $Path + ) + + [string] $separator = '/' + [string[]] $parts = $Path.Split($separator) + + [string] $realPath = '' + foreach ($part in $parts) { + if ($realPath -and !$realPath.EndsWith($separator)) { + $realPath += $separator + } + + $realPath += $part.Replace('\', '/') + + # The slash is important when using Get-Item on Drive letters in pwsh. + if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) { + $realPath += '/' + } + + $item = Get-Item $realPath + if ($item.LinkTarget) { + $realPath = $item.LinkTarget.Replace('\', '/') + } + } + $realPath +} + +$path = Resolve-Symlinks -Path $args[0] +Write-Host $path diff --git a/vendor/flutter_quic/cargokit/gradle/plugin.gradle b/vendor/flutter_quic/cargokit/gradle/plugin.gradle new file mode 100644 index 0000000..4af35ee --- /dev/null +++ b/vendor/flutter_quic/cargokit/gradle/plugin.gradle @@ -0,0 +1,179 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import java.nio.file.Paths +import org.apache.tools.ant.taskdefs.condition.Os + +CargoKitPlugin.file = buildscript.sourceFile + +apply plugin: CargoKitPlugin + +class CargoKitExtension { + String manifestDir; // Relative path to folder containing Cargo.toml + String libname; // Library name within Cargo.toml. Must be a cdylib +} + +abstract class CargoKitBuildTask extends DefaultTask { + + @Input + String buildMode + + @Input + String buildDir + + @Input + String outputDir + + @Input + String ndkVersion + + @Input + String sdkDirectory + + @Input + int compileSdkVersion; + + @Input + int minSdkVersion; + + @Input + String pluginFile + + @Input + List targetPlatforms + + @TaskAction + def build() { + if (project.cargokit.manifestDir == null) { + throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); + } + + if (project.cargokit.libname == null) { + throw new GradleException("Property 'libname' must be set on cargokit extension"); + } + + def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" + def path = Paths.get(new File(pluginFile).parent, "..", executableName); + + def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) + + def rootProjectDir = project.rootProject.projectDir + + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { + commandLine 'chmod', '+x', path + } + } + + project.exec { + executable path + args "build-gradle" + environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir + environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" + environment "CARGOKIT_MANIFEST_DIR", manifestDir + environment "CARGOKIT_CONFIGURATION", buildMode + environment "CARGOKIT_TARGET_TEMP_DIR", buildDir + environment "CARGOKIT_OUTPUT_DIR", outputDir + environment "CARGOKIT_NDK_VERSION", ndkVersion + environment "CARGOKIT_SDK_DIR", sdkDirectory + environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion + environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion + environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") + environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] + } + } +} + +class CargoKitPlugin implements Plugin { + + static String file; + + private Plugin findFlutterPlugin(Project rootProject) { + _findFlutterPlugin(rootProject.childProjects) + } + + private Plugin _findFlutterPlugin(Map projects) { + for (project in projects) { + for (plugin in project.value.getPlugins()) { + if (plugin.class.name == "com.flutter.gradle.FlutterPlugin") { + return plugin; + } + } + def plugin = _findFlutterPlugin(project.value.childProjects); + if (plugin != null) { + return plugin; + } + } + return null; + } + + @Override + void apply(Project project) { + def plugin = findFlutterPlugin(project.rootProject); + + project.extensions.create("cargokit", CargoKitExtension) + + if (plugin == null) { + print("Flutter plugin not found, CargoKit plugin will not be applied.") + return; + } + + def cargoBuildDir = "${project.buildDir}/build" + + // Determine if the project is an application or library + def isApplication = plugin.project.plugins.hasPlugin('com.android.application') + def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants + + variants.all { variant -> + + final buildType = variant.buildType.name + + def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; + def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; + jniLibs.srcDir(new File(cargoOutputDir)) + + def platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() + + // Same thing addFlutterDependencies does in flutter.gradle + if (buildType == "debug") { + platforms.add("android-x86") + platforms.add("android-x64") + } + + // The task name depends on plugin properties, which are not available + // at this point + project.getGradle().afterProject { + def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; + + if (project.tasks.findByName(taskName)) { + return + } + + if (plugin.project.android.ndkVersion == null) { + throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") + } + + def task = project.tasks.create(taskName, CargoKitBuildTask.class) { + buildMode = variant.buildType.name + buildDir = cargoBuildDir + outputDir = cargoOutputDir + ndkVersion = plugin.project.android.ndkVersion + sdkDirectory = plugin.project.android.sdkDirectory + minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int + compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int + targetPlatforms = platforms + pluginFile = CargoKitPlugin.file + } + def onTask = { newTask -> + if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { + newTask.dependsOn task + // Fix gradle 7.4.2 not picking up JNI library changes + newTask.outputs.upToDateWhen { false } + } + } + project.tasks.each onTask + project.tasks.whenTaskAdded onTask + } + } + } +} diff --git a/vendor/flutter_quic/cargokit/run_build_tool.cmd b/vendor/flutter_quic/cargokit/run_build_tool.cmd new file mode 100755 index 0000000..c45d0aa --- /dev/null +++ b/vendor/flutter_quic/cargokit/run_build_tool.cmd @@ -0,0 +1,91 @@ +@echo off +setlocal + +setlocal ENABLEDELAYEDEXPANSION + +SET BASEDIR=%~dp0 + +if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( + mkdir "%CARGOKIT_TOOL_TEMP_DIR%" +) +cd /D "%CARGOKIT_TOOL_TEMP_DIR%" + +SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool +SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart + +set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% + +( + echo name: build_tool_runner + echo version: 1.0.0 + echo publish_to: none + echo. + echo environment: + echo sdk: '^>=3.0.0 ^<4.0.0' + echo. + echo dependencies: + echo build_tool: + echo path: %BUILD_TOOL_PKG_DIR_POSIX% +) >pubspec.yaml + +if not exist bin ( + mkdir bin +) + +( + echo import 'package:build_tool/build_tool.dart' as build_tool; + echo void main^(List^ args^) ^{ + echo build_tool.runMain^(args^); + echo ^} +) >bin\build_tool_runner.dart + +SET PRECOMPILED=bin\build_tool_runner.dill + +REM To detect changes in package we compare output of DIR /s (recursive) +set PREV_PACKAGE_INFO=.dart_tool\package_info.prev +set CUR_PACKAGE_INFO=.dart_tool\package_info.cur + +DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" + +REM Last line in dir output is free space on harddrive. That is bound to +REM change between invocation so we need to remove it +( + Set "Line=" + For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( + SetLocal EnableDelayedExpansion + If Defined Line Echo !Line! + EndLocal + Set "Line=%%A") +) >"%CUR_PACKAGE_INFO%" +DEL "%CUR_PACKAGE_INFO%_orig" + +REM Compare current directory listing with previous +FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 + +If %ERRORLEVEL% neq 0 ( + REM Changed - copy current to previous and remove precompiled kernel + if exist "%PREV_PACKAGE_INFO%" ( + DEL "%PREV_PACKAGE_INFO%" + ) + MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" + if exist "%PRECOMPILED%" ( + DEL "%PRECOMPILED%" + ) +) + +REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% +REM which means we need to do pub get and precompile +if not exist "%PRECOMPILED%" ( + echo Running pub get in "%cd%" + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart +) + +"%DART%" "%PRECOMPILED%" %* + +REM 253 means invalid snapshot version. +If %ERRORLEVEL% equ 253 ( + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart + "%DART%" "%PRECOMPILED%" %* +) diff --git a/vendor/flutter_quic/cargokit/run_build_tool.sh b/vendor/flutter_quic/cargokit/run_build_tool.sh new file mode 100755 index 0000000..24b0ed8 --- /dev/null +++ b/vendor/flutter_quic/cargokit/run_build_tool.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$(dirname "$0") + +mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" + +cd "$CARGOKIT_TOOL_TEMP_DIR" + +# Write a very simple bin package in temp folder that depends on build_tool package +# from Cargokit. This is done to ensure that we don't pollute Cargokit folder +# with .dart_tool contents. + +BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" + +if [[ -z $FLUTTER_ROOT ]]; then # not defined + DART=dart +else + DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" +fi + +cat << EOF > "pubspec.yaml" +name: build_tool_runner +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + build_tool: + path: "$BUILD_TOOL_PKG_DIR" +EOF + +mkdir -p "bin" + +cat << EOF > "bin/build_tool_runner.dart" +import 'package:build_tool/build_tool.dart' as build_tool; +void main(List args) { + build_tool.runMain(args); +} +EOF + +# Create alias for `shasum` if it does not exist and `sha1sum` exists +if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then + shopt -s expand_aliases + alias shasum="sha1sum" +fi + +# Dart run will not cache any package that has a path dependency, which +# is the case for our build_tool_runner. So instead we precompile the package +# ourselves. +# To invalidate the cached kernel we use the hash of ls -LR of the build_tool +# package directory. This should be good enough, as the build_tool package +# itself is not meant to have any path dependencies. + +if [[ "$OSTYPE" == "darwin"* ]]; then + PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) +else + PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) +fi + +PACKAGE_HASH_FILE=".package_hash" + +if [ -f "$PACKAGE_HASH_FILE" ]; then + EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") + if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then + rm "$PACKAGE_HASH_FILE" + fi +fi + +# Run pub get if needed. +if [ ! -f "$PACKAGE_HASH_FILE" ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" +fi + +# Rebuild the tool if it was deleted by Android Studio +if [ ! -f "bin/build_tool_runner.dill" ]; then + "$DART" compile kernel bin/build_tool_runner.dart +fi + +set +e + +"$DART" bin/build_tool_runner.dill "$@" + +exit_code=$? + +# 253 means invalid snapshot version. +if [ $exit_code == 253 ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + "$DART" bin/build_tool_runner.dill "$@" + exit_code=$? +fi + +exit $exit_code diff --git a/vendor/flutter_quic/example/README.md b/vendor/flutter_quic/example/README.md new file mode 100644 index 0000000..25ee8fe --- /dev/null +++ b/vendor/flutter_quic/example/README.md @@ -0,0 +1,16 @@ +# flutter_quic_example + +Demonstrates how to use the flutter_quic plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/vendor/flutter_quic/example/analysis_options.yaml b/vendor/flutter_quic/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/vendor/flutter_quic/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/vendor/flutter_quic/example/android/app/build.gradle.kts b/vendor/flutter_quic/example/android/app/build.gradle.kts new file mode 100644 index 0000000..84955c5 --- /dev/null +++ b/vendor/flutter_quic/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.flutter_quic_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.flutter_quic_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/vendor/flutter_quic/example/android/app/src/debug/AndroidManifest.xml b/vendor/flutter_quic/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/vendor/flutter_quic/example/android/app/src/main/AndroidManifest.xml b/vendor/flutter_quic/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..43928f2 --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/flutter_quic/example/android/app/src/main/kotlin/com/example/flutter_quic_example/MainActivity.kt b/vendor/flutter_quic/example/android/app/src/main/kotlin/com/example/flutter_quic_example/MainActivity.kt new file mode 100644 index 0000000..5eb89e4 --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/main/kotlin/com/example/flutter_quic_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_quic_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/vendor/flutter_quic/example/android/app/src/main/res/drawable-v21/launch_background.xml b/vendor/flutter_quic/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/vendor/flutter_quic/example/android/app/src/main/res/drawable/launch_background.xml b/vendor/flutter_quic/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/vendor/flutter_quic/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/vendor/flutter_quic/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/vendor/flutter_quic/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/vendor/flutter_quic/example/android/app/src/main/res/values-night/styles.xml b/vendor/flutter_quic/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/vendor/flutter_quic/example/android/app/src/main/res/values/styles.xml b/vendor/flutter_quic/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/vendor/flutter_quic/example/android/app/src/profile/AndroidManifest.xml b/vendor/flutter_quic/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/vendor/flutter_quic/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/vendor/flutter_quic/example/android/build.gradle.kts b/vendor/flutter_quic/example/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/vendor/flutter_quic/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/vendor/flutter_quic/example/android/gradle.properties b/vendor/flutter_quic/example/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/vendor/flutter_quic/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/vendor/flutter_quic/example/android/gradle/wrapper/gradle-wrapper.properties b/vendor/flutter_quic/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/vendor/flutter_quic/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/vendor/flutter_quic/example/android/settings.gradle.kts b/vendor/flutter_quic/example/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/vendor/flutter_quic/example/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/vendor/flutter_quic/example/generate_certs.sh b/vendor/flutter_quic/example/generate_certs.sh new file mode 100755 index 0000000..b496b74 --- /dev/null +++ b/vendor/flutter_quic/example/generate_certs.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Generate self-signed certificates for QUIC server demo +# This creates certificates that work for localhost testing + +echo "🔐 Generating self-signed certificates for QUIC demo..." + +# Create certificates directory +mkdir -p certs +cd certs + +# Generate private key in PKCS#8 format (required by rustls) +openssl genpkey -algorithm RSA -out server.key + +# Generate certificate signing request +openssl req -new -key server.key -out server.csr -subj "/C=US/ST=Demo/L=Demo/O=FlutterQUIC/CN=localhost" + +# Generate self-signed certificate valid for 365 days +openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 365 -extensions v3_req -extfile <(cat < server.pem +cat server.key >> server.pem + +echo "✅ Certificates generated successfully!" +echo "📁 Files created in certs/ directory:" +echo " - server.key (private key - PEM format)" +echo " - server.key.der (private key - DER format for Flutter)" +echo " - server.crt (certificate - PEM format)" +echo " - server.crt.der (certificate - DER format for Flutter)" +echo " - server.pem (combined PEM for easy use)" +echo "" +echo "🚀 Ready to run QUIC server with these certificates!" + +# Clean up CSR file +rm server.csr \ No newline at end of file diff --git a/vendor/flutter_quic/example/ios/Flutter/AppFrameworkInfo.plist b/vendor/flutter_quic/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/vendor/flutter_quic/example/ios/Flutter/Debug.xcconfig b/vendor/flutter_quic/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/vendor/flutter_quic/example/ios/Flutter/Release.xcconfig b/vendor/flutter_quic/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/vendor/flutter_quic/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/vendor/flutter_quic/example/ios/Podfile b/vendor/flutter_quic/example/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/vendor/flutter_quic/example/ios/Podfile.lock b/vendor/flutter_quic/example/ios/Podfile.lock new file mode 100644 index 0000000..63a52fb --- /dev/null +++ b/vendor/flutter_quic/example/ios/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - Flutter (1.0.0) + - flutter_quic (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_quic (from `.symlinks/plugins/flutter_quic/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_quic: + :path: ".symlinks/plugins/flutter_quic/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_quic: 8297b572bffecea852e967739225ff8a2807009e + +PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 + +COCOAPODS: 1.16.2 diff --git a/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.pbxproj b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e5dd94f --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2B3E86ABB6A64D6E4CB57653 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270F57216661C6E8E450C5FF /* Pods_Runner.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7710C318961397E1FA20B5AC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7F13E1D5787412E15C043FD /* Pods_RunnerTests.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0A3082ACA67F50F1C00E7A56 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 270F57216661C6E8E450C5FF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 46943859DA2359ABE805A99F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 4E170ECAB59D985EF6D9C294 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6B5DC1812CDE97D52B8E5B29 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8F31753E26291563F2261183 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B7F13E1D5787412E15C043FD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D7818F91CD29CB6F043C438E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 197F81E059E08EC901DC9292 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7710C318961397E1FA20B5AC /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B3E86ABB6A64D6E4CB57653 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 4968A565BF00DE5F88546BBC /* Frameworks */ = { + isa = PBXGroup; + children = ( + 270F57216661C6E8E450C5FF /* Pods_Runner.framework */, + B7F13E1D5787412E15C043FD /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + AC6B2DD4091BFA6AF7B7D126 /* Pods */, + 4968A565BF00DE5F88546BBC /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + AC6B2DD4091BFA6AF7B7D126 /* Pods */ = { + isa = PBXGroup; + children = ( + 0A3082ACA67F50F1C00E7A56 /* Pods-Runner.debug.xcconfig */, + 4E170ECAB59D985EF6D9C294 /* Pods-Runner.release.xcconfig */, + D7818F91CD29CB6F043C438E /* Pods-Runner.profile.xcconfig */, + 6B5DC1812CDE97D52B8E5B29 /* Pods-RunnerTests.debug.xcconfig */, + 8F31753E26291563F2261183 /* Pods-RunnerTests.release.xcconfig */, + 46943859DA2359ABE805A99F /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + CFD6B965033B2E1C44CEC86A /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 197F81E059E08EC901DC9292 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 6E92CC5B60DDD31BAB0FA7A8 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C47A065510E4265B8648E106 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 6E92CC5B60DDD31BAB0FA7A8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C47A065510E4265B8648E106 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + CFD6B965033B2E1C44CEC86A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QFMJH93T7C; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6B5DC1812CDE97D52B8E5B29 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8F31753E26291563F2261183 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 46943859DA2359ABE805A99F /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QFMJH93T7C; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QFMJH93T7C; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/vendor/flutter_quic/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/vendor/flutter_quic/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/flutter_quic/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/vendor/flutter_quic/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/vendor/flutter_quic/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/vendor/flutter_quic/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/vendor/flutter_quic/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/vendor/flutter_quic/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/vendor/flutter_quic/example/ios/Runner/AppDelegate.swift b/vendor/flutter_quic/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/vendor/flutter_quic/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/vendor/flutter_quic/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/flutter_quic/example/ios/Runner/Base.lproj/Main.storyboard b/vendor/flutter_quic/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/flutter_quic/example/ios/Runner/Info.plist b/vendor/flutter_quic/example/ios/Runner/Info.plist new file mode 100644 index 0000000..7f3bea3 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flutter Quic + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_quic_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/vendor/flutter_quic/example/ios/Runner/Runner-Bridging-Header.h b/vendor/flutter_quic/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/vendor/flutter_quic/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/vendor/flutter_quic/example/ios/RunnerTests/RunnerTests.swift b/vendor/flutter_quic/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/vendor/flutter_quic/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/vendor/flutter_quic/example/lib/main.dart b/vendor/flutter_quic/example/lib/main.dart new file mode 100644 index 0000000..ab806da --- /dev/null +++ b/vendor/flutter_quic/example/lib/main.dart @@ -0,0 +1,616 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; + +import 'package:flutter_quic/flutter_quic.dart' as flutter_quic; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + late Future future; + + @override + void initState() { + super.initState(); + future = flutter_quic.RustLib.init(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Flutter QUIC - Phase 3 Test')), + body: FutureBuilder( + future: future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + return const QuicTestWidget(); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ), + ); + } +} + +class QuicTestWidget extends StatefulWidget { + const QuicTestWidget({super.key}); + + @override + State createState() => _QuicTestWidgetState(); +} + +class _QuicTestWidgetState extends State { + final StringBuffer _log = StringBuffer(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + // Load certificate and key files for server setup + Future<(List, List)?> _loadCertificates() async { + try { + // Try to load certificates from assets (DER format for binary compatibility) + final certBytes = await rootBundle.load('certs/server.crt.der'); + final keyBytes = await rootBundle.load('certs/server.key.der'); + + // Convert ByteData to Uint8List and List + final certUint8List = certBytes.buffer.asUint8List(); + final keyIntList = keyBytes.buffer.asUint8List().cast().toList(); + + // QUIC expects a certificate chain (list of certificates) + final certChain = [certUint8List]; + + return (certChain, keyIntList); + } catch (e) { + _log2('📝 Certificates not found in assets. Run: ./generate_certs.sh'); + _log2(' Then restart the app to reload assets.'); + return null; + } + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + try { + await flutter_quic.RustLib.init(); + } catch (e) { + debugPrint('Failed to initialize Rust library: $e'); + } + + if (!mounted) return; + + // Run comprehensive Phase 3 tests + _runPhase3Tests(); + } + + void _log2(String message) { + setState(() { + _log.writeln(message); + }); + debugPrint(message); + + // Auto-scroll to bottom + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + // Comprehensive Phase 3 Tests + Future _runPhase3Tests() async { + _log2('🚀 Starting Phase 3 Complete QUIC Test Suite...'); + _log2('══════════════════════════════════════════════'); + + await _testCoreAPI(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testServerAPI(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testClientServerCommunication(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testConvenienceAPIStructure(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testQuicClientCreation(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testQuicClientConfiguration(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testHTTPStyleMethods(); + await Future.delayed(const Duration(milliseconds: 500)); + + await _testErrorHandlingDemo(); + await Future.delayed(const Duration(milliseconds: 500)); + + _showPhase3Summary(); + } + + // Test Core API functionality + Future _testCoreAPI() async { + _log2(''); + _log2('📡 TESTING CORE API (Phase 2)'); + _log2('────────────────────────────────'); + + try { + _log2('🔧 Creating QUIC endpoint...'); + final endpoint = await flutter_quic.createClientEndpoint(); + _log2('✅ QuicEndpoint.client() - SUCCESS'); + _log2(' - Type: ${endpoint.runtimeType}'); + _log2(' - Direct Quinn integration'); + _log2(' - Memory managed by Rust'); + + // Test Core API is available + _log2('🔧 Verifying Core API methods...'); + _log2('✅ Available Core API methods:'); + _log2(' - createClientEndpoint() ✓'); + _log2(' - endpointConnect() ✓'); + _log2(' - connectionOpenBi() ✓'); + _log2(' - connectionOpenUni() ✓'); + _log2(' - sendStreamWrite() ✓'); + _log2(' - sendStreamWriteAll() ✓'); + _log2(' - recvStreamRead() ✓'); + _log2(' - connectionSendDatagram() ✓'); + } catch (e) { + _log2('❌ Core API Error: $e'); + } + } + + // Test Server API functionality + Future _testServerAPI() async { + _log2(''); + _log2('🖥️ TESTING SERVER API (NEW!)'); + _log2('────────────────────────────────'); + + try { + _log2('🔧 Loading TLS certificates...'); + final certs = await _loadCertificates(); + + if (certs == null) { + _log2('⚠️ Skipping server creation (no certificates)'); + _log2('✅ Server API methods available:'); + _log2(' - createServerEndpoint() ✓'); + _log2(' - serverConfigWithSingleCert() ✓'); + _log2('📝 Run "./generate_certs.sh" to enable server demo'); + return; + } + + final (certChain, keyBytes) = certs; + _log2('✅ Certificates loaded successfully'); + _log2(' - Certificate size: ${certChain[0].length} bytes'); + _log2(' - Private key size: ${keyBytes.length} bytes'); + + _log2('🔧 Creating server configuration...'); + final serverConfig = await flutter_quic.serverConfigWithSingleCert( + certChain: certChain, + key: keyBytes, + ); + _log2('✅ QuicServerConfig created - SUCCESS'); + _log2(' - TLS configuration ready'); + _log2(' - Certificate chain validated'); + + _log2('🔧 Creating QUIC server endpoint...'); + final serverEndpoint = await flutter_quic.createServerEndpoint( + config: serverConfig, + addr: '127.0.0.1:4433', + ); + _log2('✅ QuicEndpoint.server() - SUCCESS'); + _log2(' - Type: ${serverEndpoint.runtimeType}'); + _log2(' - Bound to: 127.0.0.1:4433'); + _log2(' - Ready to accept connections'); + _log2(' - Server-side Quinn integration'); + _log2(' - Memory managed by Rust'); + + _log2('🔧 Verifying Server API methods...'); + _log2('✅ Complete Server API:'); + _log2(' - serverConfigWithSingleCert() ✓ WORKING'); + _log2(' - createServerEndpoint() ✓ WORKING'); + _log2(' - endpointAccept() ✓ (for connection handling)'); + _log2(' - connectionAcceptBi() ✓ (via core API)'); + _log2(' - connectionAcceptUni() ✓ (via core API)'); + _log2(' - Same stream operations as client ✓'); + } catch (e) { + _log2('❌ Server API Error: $e'); + _log2('📝 This might be due to address already in use'); + _log2('📝 Or certificate format issues'); + } + } + + // Test client-server communication (loopback demo) + Future _testClientServerCommunication() async { + _log2(''); + _log2('🔄 TESTING CLIENT-SERVER COMMUNICATION'); + _log2('──────────────────────────────────────────'); + + try { + _log2('🔧 Loading certificates for server...'); + final certs = await _loadCertificates(); + + if (certs == null) { + _log2('⚠️ Skipping client-server demo (no certificates)'); + _log2('📝 Run "./generate_certs.sh" first'); + return; + } + + final (certChain, keyBytes) = certs; + + _log2('🔧 Setting up QUIC server...'); + final serverConfig = await flutter_quic.serverConfigWithSingleCert( + certChain: certChain, + key: keyBytes, + ); + final serverEndpoint = await flutter_quic.createServerEndpoint( + config: serverConfig, + addr: '127.0.0.1:4434', // Different port to avoid conflicts + ); + _log2( + '✅ Server created on 127.0.0.1:4434 - ID: ${serverEndpoint.hashCode}', + ); + + _log2('🔧 Setting up QUIC client...'); + final clientEndpoint = await flutter_quic.createClientEndpoint(); + _log2('✅ Client endpoint created'); + + _log2('🔧 Attempting QUIC connection...'); + try { + // This will likely fail due to certificate validation, + // but shows the connection attempt structure + final connection = await flutter_quic.endpointConnect( + endpoint: clientEndpoint, + serverName: 'localhost', + addr: '127.0.0.1:4434', + ); + _log2('🎉 CONNECTION SUCCESS! (Unexpected but amazing!)'); + _log2(' - QUIC connection established'); + _log2(' - Client-server communication working'); + _log2(' - Type: ${connection.runtimeType}'); + } catch (e) { + _log2('✅ Connection attempt made (expected certificate error)'); + _log2(' - Error: ${e.toString().split('\n').first}'); + _log2(' - This is normal with self-signed certificates'); + _log2(' - Shows client-server API structure works!'); + } + + _log2('🔧 Summary of client-server capability...'); + _log2('✅ Full QUIC client-server stack ready:'); + _log2(' - Server: ✓ Created with real certificates'); + _log2(' - Client: ✓ Created and attempted connection'); + _log2(' - Both use Quinn core for real QUIC protocol'); + _log2(' - Certificate validation (needs trusted certs for production)'); + _log2(' - Address binding and connection handling working'); + + _log2('📝 For production client-server communication:'); + _log2(' - Use CA-signed certificates (not self-signed)'); + _log2(' - Configure proper hostname validation'); + _log2(' - Add connection accept loop on server side'); + _log2(' - Handle stream creation and data exchange'); + _log2(' - All APIs are already implemented!'); + } catch (e) { + _log2('❌ Client-Server Communication Error: $e'); + } + } + + // Test Convenience API structure + Future _testConvenienceAPIStructure() async { + _log2(''); + _log2('🎯 TESTING CONVENIENCE API STRUCTURE'); + _log2('─────────────────────────────────────'); + + _log2('🔧 Verifying Convenience API methods...'); + _log2('✅ Available Convenience API methods:'); + _log2(' - quicClientCreate() ✓'); + _log2(' - quicClientCreateWithConfig() ✓'); + _log2(' - quicClientSend() ✓'); + _log2(' - quicClientSendWithTimeout() ✓'); + _log2(' - quicClientGet() ✓'); + _log2(' - quicClientPost() ✓'); + _log2(' - quicClientGetWithTimeout() ✓'); + _log2(' - quicClientPostWithTimeout() ✓'); + _log2(' - quicClientConfig() ✓'); + _log2(' - quicClientClearPool() ✓'); + _log2(' - quicClientConfigNew() ✓'); + } + + // Test QuicClient creation + Future _testQuicClientCreation() async { + _log2(''); + _log2('🏗️ TESTING QUIC CLIENT CREATION'); + _log2('────────────────────────────────'); + + try { + _log2('🔧 Creating QuicClient with default config...'); + final client = await flutter_quic.quicClientCreate(); + _log2('✅ QuicClient.create() - SUCCESS'); + _log2(' - Type: ${client.runtimeType}'); + _log2(' - Built on Core API (QuicEndpoint internally)'); + _log2(' - Connection pooling enabled'); + _log2(' - Retry logic configured'); + + _log2('🔧 Testing client configuration access...'); + final (clientWithConfig, config) = await flutter_quic.quicClientConfig( + client: client, + ); + _log2('✅ QuicClient.config() - SUCCESS'); + _log2(' - Max connections per host: ${config.maxConnectionsPerHost}'); + _log2(' - Connect timeout: ${config.connectTimeoutMs}ms'); + _log2(' - Request timeout: ${config.requestTimeoutMs}ms'); + _log2(' - Retry attempts: ${config.retryAttempts}'); + _log2(' - Retry delay: ${config.retryDelayMs}ms'); + _log2(' - Keep-alive timeout: ${config.keepAliveTimeoutMs}ms'); + } catch (e) { + _log2('❌ QuicClient Creation Error: $e'); + } + } + + // Test QuicClient configuration + Future _testQuicClientConfiguration() async { + _log2(''); + _log2('⚙️ TESTING CUSTOM CONFIGURATION'); + _log2('────────────────────────────────'); + + try { + _log2('🔧 Creating custom QuicClientConfig...'); + final customConfig = await flutter_quic.quicClientConfigNew(); + _log2('✅ QuicClientConfig.new() - SUCCESS'); + + _log2('🔧 Creating QuicClient with custom config...'); + final customClient = await flutter_quic.quicClientCreateWithConfig( + config: customConfig, + ); + _log2( + '✅ QuicClient.createWithConfig() - SUCCESS - ID: ${customClient.hashCode}', + ); + _log2(' - Custom configuration applied'); + _log2(' - Ready for production use'); + } catch (e) { + _log2('❌ Custom Configuration Error: $e'); + } + } + + // Test HTTP-style methods (this will show connection errors, which is expected) + Future _testHTTPStyleMethods() async { + _log2(''); + _log2('🌐 TESTING HTTP-STYLE METHODS'); + _log2('──────────────────────────────'); + + try { + final client = await flutter_quic.quicClientCreate(); + + // Test GET method (will fail due to no server, but shows API works) + _log2('🔧 Testing GET method API...'); + try { + final (clientAfterGet, response) = await flutter_quic.quicClientGet( + client: client, + url: 'https://httpbin.org/get', + ); + _log2('✅ GET request - Unexpected success: $response'); + } catch (e) { + _log2('✅ GET method API working (expected connection error)'); + _log2(' - Error: ${e.toString().split('\n').first}'); + _log2(' - This is normal without a QUIC server'); + _log2(' - API structure is correct'); + } + + // Test POST method + _log2('🔧 Testing POST method API...'); + try { + final (clientAfterPost, response) = await flutter_quic.quicClientPost( + client: client, + url: 'https://httpbin.org/post', + data: '{"test": "data", "phase": 3}', + ); + _log2('✅ POST request - Unexpected success: $response'); + } catch (e) { + _log2('✅ POST method API working (expected connection error)'); + _log2(' - Error: ${e.toString().split('\n').first}'); + _log2(' - Data payload handled correctly'); + _log2(' - API structure is correct'); + } + + // Test timeout methods + _log2('🔧 Testing timeout methods...'); + try { + final (clientTimeout, response) = await flutter_quic + .quicClientGetWithTimeout( + client: client, + url: 'https://httpbin.org/delay/1', + ); + _log2('✅ GET with timeout - Unexpected success: $response'); + } catch (e) { + _log2('✅ GET with timeout API working'); + _log2(' - Timeout protection active'); + _log2(' - Error handling proper'); + } + } catch (e) { + _log2('❌ HTTP-Style Methods Error: $e'); + } + } + + // Test error handling demo + Future _testErrorHandlingDemo() async { + _log2(''); + _log2('🛡️ TESTING ERROR HANDLING & RETRY LOGIC'); + _log2('─────────────────────────────────────────'); + + try { + final client = await flutter_quic.quicClientCreate(); + + _log2('🔧 Testing connection pooling...'); + final poolClearedClient = await flutter_quic.quicClientClearPool( + client: client, + ); + _log2('✅ Connection pool cleared'); + _log2(' - Pool management working'); + _log2(' - Memory cleanup handled'); + + _log2('🔧 Testing retry logic (will demonstrate retries)...'); + try { + final (retriedClient, response) = await flutter_quic.quicClientSend( + client: poolClearedClient, + url: 'https://invalid-quic-server.example.com/test', + data: 'retry test data', + ); + _log2( + '❌ Unexpected success on invalid server - Client: ${retriedClient.hashCode}, Response: $response', + ); + } catch (e) { + _log2('✅ Retry logic activated (expected)'); + _log2(' - Multiple attempts made'); + _log2(' - Proper error classification'); + _log2(' - Exponential backoff applied'); + _log2(' - Final error: ${e.toString().split('\n').first}'); + } + } catch (e) { + _log2('❌ Error Handling Test Error: $e'); + } + } + + // Show Phase 3 completion summary + void _showPhase3Summary() { + _log2(''); + _log2('🎉 PHASE 3 COMPLETE! 🎉'); + _log2('══════════════════════════════════════════════'); + _log2(''); + _log2('✅ COMPLETED FEATURES:'); + _log2(''); + _log2('🖥️ Server API (NEW!):'); + _log2(' ✓ QuicEndpoint.server() creation'); + _log2(' ✓ Server-side connection handling'); + _log2(' ✓ Same stream API as client'); + _log2(' ✓ Full duplex communication'); + _log2(''); + _log2('📱 Convenience API:'); + _log2(' ✓ QuicClient with Dio-style interface'); + _log2(' ✓ HTTP-style methods (get, post)'); + _log2(' ✓ Connection pooling & reuse'); + _log2(' ✓ Automatic retry logic'); + _log2(' ✓ Comprehensive configuration'); + _log2(' ✓ Timeout protection'); + _log2(''); + _log2('⚡ Core API (Built on Quinn):'); + _log2(' ✓ Direct 1:1 Quinn mapping'); + _log2(' ✓ Full QUIC protocol support'); + _log2(' ✓ Client & Server endpoints'); + _log2(' ✓ Stream operations (bi & uni)'); + _log2(' ✓ Datagram support'); + _log2(' ✓ Connection management'); + _log2(''); + _log2('🔧 Production Ready:'); + _log2(' ✓ Memory safety (Rust ownership)'); + _log2(' ✓ Error handling (specific errors)'); + _log2(' ✓ Flutter integration (async/await)'); + _log2(' ✓ Type safety (generated bindings)'); + _log2(' ✓ Performance optimized'); + _log2(' ✓ Client-Server capable'); + _log2(''); + _log2('📊 API COVERAGE:'); + _log2(' ✓ Core API: Complete Quinn mapping (client + server)'); + _log2(' ✓ Convenience API: 80% use cases covered'); + _log2(' ✓ Both APIs available simultaneously'); + _log2(' ✓ Full QUIC ecosystem support'); + _log2(''); + _log2('🚀 READY FOR PRODUCTION USE!'); + _log2(''); + _log2('Next steps:'); + _log2('• Set up real client-server demo with certificates'); + _log2('• Run integration tests with actual traffic'); + _log2('• Performance benchmarks vs HTTP/2 & WebSockets'); + _log2('• Deploy client/server apps to stores'); + _log2('• Build real-time applications (gaming, chat, etc.)'); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Flutter QUIC Phase 3 Test Suite', + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Testing Core API + Convenience API', + style: Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(color: Colors.grey[600]), + ), + const SizedBox(height: 16), + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: SingleChildScrollView( + controller: _scrollController, + child: Text( + _log.toString(), + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Colors.greenAccent, + height: 1.3, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Icon(Icons.info_outline, size: 16, color: Colors.blue[600]), + const SizedBox(width: 8), + Expanded( + child: Text( + 'This test demonstrates API structure. For full testing, deploy a QUIC server.', + style: TextStyle( + fontSize: 12, + color: Colors.blue[600], + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/vendor/flutter_quic/example/linux/CMakeLists.txt b/vendor/flutter_quic/example/linux/CMakeLists.txt new file mode 100644 index 0000000..c42cc16 --- /dev/null +++ b/vendor/flutter_quic/example/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_quic_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.flutter_quic") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/vendor/flutter_quic/example/linux/flutter/CMakeLists.txt b/vendor/flutter_quic/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/vendor/flutter_quic/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/vendor/flutter_quic/example/linux/flutter/generated_plugin_registrant.cc b/vendor/flutter_quic/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/vendor/flutter_quic/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/vendor/flutter_quic/example/linux/flutter/generated_plugin_registrant.h b/vendor/flutter_quic/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/vendor/flutter_quic/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/vendor/flutter_quic/example/linux/flutter/generated_plugins.cmake b/vendor/flutter_quic/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..d8d70c1 --- /dev/null +++ b/vendor/flutter_quic/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_quic +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/vendor/flutter_quic/example/linux/runner/CMakeLists.txt b/vendor/flutter_quic/example/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/vendor/flutter_quic/example/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/vendor/flutter_quic/example/linux/runner/main.cc b/vendor/flutter_quic/example/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/vendor/flutter_quic/example/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/vendor/flutter_quic/example/linux/runner/my_application.cc b/vendor/flutter_quic/example/linux/runner/my_application.cc new file mode 100644 index 0000000..88ec572 --- /dev/null +++ b/vendor/flutter_quic/example/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_quic_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_quic_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/vendor/flutter_quic/example/linux/runner/my_application.h b/vendor/flutter_quic/example/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/vendor/flutter_quic/example/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/vendor/flutter_quic/example/macos/Flutter/Flutter-Debug.xcconfig b/vendor/flutter_quic/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/vendor/flutter_quic/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/vendor/flutter_quic/example/macos/Flutter/Flutter-Release.xcconfig b/vendor/flutter_quic/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/vendor/flutter_quic/example/macos/Flutter/GeneratedPluginRegistrant.swift b/vendor/flutter_quic/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..cccf817 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/vendor/flutter_quic/example/macos/Podfile b/vendor/flutter_quic/example/macos/Podfile new file mode 100644 index 0000000..29c8eb3 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/vendor/flutter_quic/example/macos/Podfile.lock b/vendor/flutter_quic/example/macos/Podfile.lock new file mode 100644 index 0000000..d4a8e2b --- /dev/null +++ b/vendor/flutter_quic/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - flutter_quic (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - flutter_quic (from `Flutter/ephemeral/.symlinks/plugins/flutter_quic/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + flutter_quic: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_quic/macos + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + flutter_quic: b8a9e126e70cd8483535f5126b11ddf8c2c1f85b + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + +PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 + +COCOAPODS: 1.16.2 diff --git a/vendor/flutter_quic/example/macos/Runner.xcodeproj/project.pbxproj b/vendor/flutter_quic/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f7af3d4 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 166B89A6CE7FDA718035153A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37259EC0F166390CEFD0CB86 /* Pods_RunnerTests.framework */; }; + 32489E5934F3135127B19B65 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD0EAF8F341E26230103D0B /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1CD0EAF8F341E26230103D0B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1DCD6F262EEEAB676A988D36 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_quic_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_quic_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 37259EC0F166390CEFD0CB86 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 841F6DB1242344DAF8BB7620 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A21ED45EC3683B716B4C0C8D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + C0FA2B60364FD0F62477CB85 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F7740FA29ABB9846FC4211EA /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + FEDAC2E4C843F1D7827F94C3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 166B89A6CE7FDA718035153A /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 32489E5934F3135127B19B65 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 9B5C8F8709803E40CBC3FDEA /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_quic_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 9B5C8F8709803E40CBC3FDEA /* Pods */ = { + isa = PBXGroup; + children = ( + 841F6DB1242344DAF8BB7620 /* Pods-Runner.debug.xcconfig */, + C0FA2B60364FD0F62477CB85 /* Pods-Runner.release.xcconfig */, + 1DCD6F262EEEAB676A988D36 /* Pods-Runner.profile.xcconfig */, + F7740FA29ABB9846FC4211EA /* Pods-RunnerTests.debug.xcconfig */, + FEDAC2E4C843F1D7827F94C3 /* Pods-RunnerTests.release.xcconfig */, + A21ED45EC3683B716B4C0C8D /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1CD0EAF8F341E26230103D0B /* Pods_Runner.framework */, + 37259EC0F166390CEFD0CB86 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 962AB158373F99AAB84E3C37 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C7B3AEF754C7BFD16611C2CB /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 7BF163CF3139E2C9EB79E357 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_quic_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7BF163CF3139E2C9EB79E357 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 962AB158373F99AAB84E3C37 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C7B3AEF754C7BFD16611C2CB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F7740FA29ABB9846FC4211EA /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_quic_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_quic_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FEDAC2E4C843F1D7827F94C3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_quic_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_quic_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A21ED45EC3683B716B4C0C8D /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_quic_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_quic_example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/vendor/flutter_quic/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/vendor/flutter_quic/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/vendor/flutter_quic/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/vendor/flutter_quic/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..abb0be9 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/flutter_quic/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/vendor/flutter_quic/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/vendor/flutter_quic/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/vendor/flutter_quic/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/vendor/flutter_quic/example/macos/Runner/AppDelegate.swift b/vendor/flutter_quic/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/vendor/flutter_quic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/vendor/flutter_quic/example/macos/Runner/Base.lproj/MainMenu.xib b/vendor/flutter_quic/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/flutter_quic/example/macos/Runner/Configs/AppInfo.xcconfig b/vendor/flutter_quic/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..44af83e --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_quic_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterQuicExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/vendor/flutter_quic/example/macos/Runner/Configs/Debug.xcconfig b/vendor/flutter_quic/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/vendor/flutter_quic/example/macos/Runner/Configs/Release.xcconfig b/vendor/flutter_quic/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/vendor/flutter_quic/example/macos/Runner/Configs/Warnings.xcconfig b/vendor/flutter_quic/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/vendor/flutter_quic/example/macos/Runner/DebugProfile.entitlements b/vendor/flutter_quic/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/vendor/flutter_quic/example/macos/Runner/Info.plist b/vendor/flutter_quic/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/vendor/flutter_quic/example/macos/Runner/MainFlutterWindow.swift b/vendor/flutter_quic/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/vendor/flutter_quic/example/macos/Runner/Release.entitlements b/vendor/flutter_quic/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/vendor/flutter_quic/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/vendor/flutter_quic/example/macos/RunnerTests/RunnerTests.swift b/vendor/flutter_quic/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/vendor/flutter_quic/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/vendor/flutter_quic/example/pubspec.yaml b/vendor/flutter_quic/example/pubspec.yaml new file mode 100644 index 0000000..4941802 --- /dev/null +++ b/vendor/flutter_quic/example/pubspec.yaml @@ -0,0 +1,100 @@ +name: flutter_quic_example +description: "Demonstrates how to use the flutter_quic plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + flutter_quic: + # When depending on this package from a real application you should use: + # flutter_quic: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - certs/server.crt + - certs/server.key + - certs/server.crt.der + - certs/server.key.der + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/vendor/flutter_quic/example/windows/CMakeLists.txt b/vendor/flutter_quic/example/windows/CMakeLists.txt new file mode 100644 index 0000000..f3158dd --- /dev/null +++ b/vendor/flutter_quic/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_quic_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_quic_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/vendor/flutter_quic/example/windows/flutter/CMakeLists.txt b/vendor/flutter_quic/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/vendor/flutter_quic/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/vendor/flutter_quic/example/windows/flutter/generated_plugin_registrant.cc b/vendor/flutter_quic/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/vendor/flutter_quic/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/vendor/flutter_quic/example/windows/flutter/generated_plugin_registrant.h b/vendor/flutter_quic/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/vendor/flutter_quic/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/vendor/flutter_quic/example/windows/flutter/generated_plugins.cmake b/vendor/flutter_quic/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..3ef778a --- /dev/null +++ b/vendor/flutter_quic/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_quic +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/vendor/flutter_quic/example/windows/runner/CMakeLists.txt b/vendor/flutter_quic/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/vendor/flutter_quic/example/windows/runner/Runner.rc b/vendor/flutter_quic/example/windows/runner/Runner.rc new file mode 100644 index 0000000..14cb202 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "flutter_quic_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_quic_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_quic_example.exe" "\0" + VALUE "ProductName", "flutter_quic_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/vendor/flutter_quic/example/windows/runner/flutter_window.cpp b/vendor/flutter_quic/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/vendor/flutter_quic/example/windows/runner/flutter_window.h b/vendor/flutter_quic/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/vendor/flutter_quic/example/windows/runner/main.cpp b/vendor/flutter_quic/example/windows/runner/main.cpp new file mode 100644 index 0000000..b20fdfc --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"flutter_quic_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/vendor/flutter_quic/example/windows/runner/resource.h b/vendor/flutter_quic/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/vendor/flutter_quic/example/windows/runner/resources/app_icon.ico b/vendor/flutter_quic/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/vendor/flutter_quic/example/windows/runner/resources/app_icon.ico differ diff --git a/vendor/flutter_quic/example/windows/runner/runner.exe.manifest b/vendor/flutter_quic/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/vendor/flutter_quic/example/windows/runner/utils.cpp b/vendor/flutter_quic/example/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/vendor/flutter_quic/example/windows/runner/utils.h b/vendor/flutter_quic/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/vendor/flutter_quic/example/windows/runner/win32_window.cpp b/vendor/flutter_quic/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/vendor/flutter_quic/example/windows/runner/win32_window.h b/vendor/flutter_quic/example/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/vendor/flutter_quic/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/vendor/flutter_quic/flutter_rust_bridge.yaml b/vendor/flutter_quic/flutter_rust_bridge.yaml new file mode 100644 index 0000000..7e9f434 --- /dev/null +++ b/vendor/flutter_quic/flutter_rust_bridge.yaml @@ -0,0 +1,3 @@ +rust_input: crate::api::bridge +rust_root: rust/ +dart_output: lib/src/rust \ No newline at end of file diff --git a/vendor/flutter_quic/ios/Classes/dummy_file.c b/vendor/flutter_quic/ios/Classes/dummy_file.c new file mode 100644 index 0000000..e06dab9 --- /dev/null +++ b/vendor/flutter_quic/ios/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/vendor/flutter_quic/ios/flutter_quic.podspec b/vendor/flutter_quic/ios/flutter_quic.podspec new file mode 100644 index 0000000..206c2dc --- /dev/null +++ b/vendor/flutter_quic/ios/flutter_quic.podspec @@ -0,0 +1,45 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_quic.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_quic' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust flutter_quic', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/libflutter_quic.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libflutter_quic.a', + } +end \ No newline at end of file diff --git a/vendor/flutter_quic/lib/flutter_quic.dart b/vendor/flutter_quic/lib/flutter_quic.dart new file mode 100644 index 0000000..090dc11 --- /dev/null +++ b/vendor/flutter_quic/lib/flutter_quic.dart @@ -0,0 +1,35 @@ +/// A Flutter plugin for QUIC protocol support using Quinn Rust library. +/// +/// This library provides a complete wrapper around the Quinn QUIC implementation, +/// exposing both Core API (direct 1:1 Quinn mapping) and Convenience API +/// (simplified wrappers for common use cases). +/// +/// ## Core API +/// Direct mapping to Quinn's API surface: +/// - QuicEndpoint: QUIC endpoint management +/// - QuicConnection: Connection handling +/// - QuicSendStream/QuicRecvStream: Stream operations +/// +/// ## Convenience API +/// Simplified wrappers built on Core API: +/// - QuicClient: High-level client operations +/// - Connection pooling and automatic retry logic +library; +// Flutter QUIC package exports + +// Export the rust bridge API +export 'src/rust/api/bridge.dart'; + +// Export library initialization +export 'src/rust/frb_generated.dart' show RustLib; + +// Export core types +export 'src/rust/core/endpoint.dart'; +export 'src/rust/core/connection.dart'; +export 'src/rust/core/stream.dart'; + +// Export convenience types +export 'src/rust/convenience/client.dart'; + +// Export error types +export 'src/rust/errors.dart'; diff --git a/vendor/flutter_quic/lib/src/rust/api/bridge.dart b/vendor/flutter_quic/lib/src/rust/api/bridge.dart new file mode 100644 index 0000000..e5be22e --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/api/bridge.dart @@ -0,0 +1,353 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../convenience/client.dart'; +import '../core/config.dart'; +import '../core/connection.dart'; +import '../core/endpoint.dart'; +import '../core/stream.dart'; +import '../errors.dart'; +import '../frb_generated.dart'; +import '../models/types.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Create a new QUIC client endpoint +Future createClientEndpoint() => + RustLib.instance.api.crateApiBridgeCreateClientEndpoint(); + +/// Create a new QUIC server endpoint +Future createServerEndpoint({ + required QuicServerConfig config, + required String addr, +}) => RustLib.instance.api.crateApiBridgeCreateServerEndpoint( + config: config, + addr: addr, +); + +/// Write data to a QUIC send stream +/// This exposes the QuicSendStream.write() method to flutter_rust_bridge +Future<(QuicSendStream, BigInt)> sendStreamWrite({ + required QuicSendStream stream, + required List data, +}) => RustLib.instance.api.crateApiBridgeSendStreamWrite( + stream: stream, + data: data, +); + +/// Write all data to a QUIC send stream +/// This exposes the QuicSendStream.write_all() method to flutter_rust_bridge +Future sendStreamWriteAll({ + required QuicSendStream stream, + required List data, +}) => RustLib.instance.api.crateApiBridgeSendStreamWriteAll( + stream: stream, + data: data, +); + +/// Finish a QUIC send stream +/// This exposes the QuicSendStream.finish() method to flutter_rust_bridge +Future sendStreamFinish({required QuicSendStream stream}) => + RustLib.instance.api.crateApiBridgeSendStreamFinish(stream: stream); + +/// Read data from a QUIC recv stream +/// This exposes the QuicRecvStream.read() method to flutter_rust_bridge +Future<(QuicRecvStream, Uint8List?)> recvStreamRead({ + required QuicRecvStream stream, + required BigInt maxLength, +}) => RustLib.instance.api.crateApiBridgeRecvStreamRead( + stream: stream, + maxLength: maxLength, +); + +/// Read all remaining data from a QUIC recv stream +/// This exposes the QuicRecvStream.read_to_end() method to flutter_rust_bridge +Future<(QuicRecvStream, Uint8List)> recvStreamReadToEnd({ + required QuicRecvStream stream, + required BigInt maxLength, +}) => RustLib.instance.api.crateApiBridgeRecvStreamReadToEnd( + stream: stream, + maxLength: maxLength, +); + +/// Open a bidirectional stream on a QUIC connection +/// This exposes the QuicConnection.open_bi() method to flutter_rust_bridge +Future<(QuicConnection, QuicSendStream, QuicRecvStream)> connectionOpenBi({ + required QuicConnection connection, +}) => + RustLib.instance.api.crateApiBridgeConnectionOpenBi(connection: connection); + +/// Accept the next server-initiated bidirectional stream on a QUIC connection. +/// +/// Blocks until the remote peer opens a new bidirectional stream or the +/// connection is closed. Returns `None` (as a missing tuple) when the +/// connection has been terminated. +/// +/// Unlike `connection_open_bi`, this function takes a shared reference so it +/// does NOT consume the `QuicConnection` arc. This allows `accept_bi` to block +/// waiting for a server-initiated stream concurrently with `open_bi` calls +/// without holding the Auto_Owned ownership lock. +/// +/// This exposes the QuicConnection.accept_bi() method to flutter_rust_bridge. +Future<(QuicSendStream?, QuicRecvStream?)> connectionAcceptBi({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionAcceptBi( + connection: connection, +); + +/// Open a unidirectional stream on a QUIC connection +/// This exposes the QuicConnection.open_uni() method to flutter_rust_bridge +Future<(QuicConnection, QuicSendStream)> connectionOpenUni({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionOpenUni( + connection: connection, +); + +/// Connect to a server using a QUIC endpoint. +/// +/// If `qlog_path` is `Some`, Quinn will write a full QUIC event trace (in +/// qlog JSON-SEQ format) to that file for the lifetime of the connection. +/// The resulting file can be analysed offline with tools such as +/// [qvis](https://qvis.quictools.info/) or Wireshark's qlog importer. +/// +/// This exposes the QuicEndpoint.connect() method to flutter_rust_bridge. +Future<(QuicEndpoint, QuicConnection)> endpointConnect({ + required QuicEndpoint endpoint, + required String addr, + required String serverName, + String? qlogPath, +}) => RustLib.instance.api.crateApiBridgeEndpointConnect( + endpoint: endpoint, + addr: addr, + serverName: serverName, + qlogPath: qlogPath, +); + +/// Send a datagram on a QUIC connection +/// This exposes the QuicConnection.send_datagram() method to flutter_rust_bridge +Future connectionSendDatagram({ + required QuicConnection connection, + required List data, +}) => RustLib.instance.api.crateApiBridgeConnectionSendDatagram( + connection: connection, + data: data, +); + +/// Send a datagram with backpressure on a QUIC connection +/// This exposes the QuicConnection.send_datagram_wait() method to flutter_rust_bridge +Future connectionSendDatagramWait({ + required QuicConnection connection, + required List data, +}) => RustLib.instance.api.crateApiBridgeConnectionSendDatagramWait( + connection: connection, + data: data, +); + +/// Read a datagram from a QUIC connection +/// This exposes the QuicConnection.read_datagram() method to flutter_rust_bridge +Future<(QuicConnection, Uint8List?)> connectionReadDatagram({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionReadDatagram( + connection: connection, +); + +/// Get datagram send buffer space +/// This exposes the QuicConnection.datagram_send_buffer_space() method to flutter_rust_bridge +Future<(QuicConnection, BigInt)> connectionDatagramSendBufferSpace({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionDatagramSendBufferSpace( + connection: connection, +); + +/// Get maximum datagram size +/// This exposes the QuicConnection.max_datagram_size() method to flutter_rust_bridge +Future<(QuicConnection, BigInt?)> connectionMaxDatagramSize({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionMaxDatagramSize( + connection: connection, +); + +/// Get the remote address of a QUIC connection +/// This exposes the QuicConnection.remote_address() method to flutter_rust_bridge +Future<(QuicConnection, SocketAddress)> connectionRemoteAddress({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionRemoteAddress( + connection: connection, +); + +/// Get the local IP address of a QUIC connection +/// This exposes the QuicConnection.local_ip() method to flutter_rust_bridge +Future<(QuicConnection, String?)> connectionLocalIp({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionLocalIp( + connection: connection, +); + +/// Get the RTT of a QUIC connection in milliseconds +/// This exposes the QuicConnection.rtt() method to flutter_rust_bridge +Future<(QuicConnection, BigInt)> connectionRttMillis({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionRttMillis( + connection: connection, +); + +/// Get the stable ID of a QUIC connection +/// This exposes the QuicConnection.stable_id() method to flutter_rust_bridge +Future<(QuicConnection, BigInt)> connectionStableId({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionStableId( + connection: connection, +); + +/// Get the close reason of a QUIC connection +/// This exposes the QuicConnection.close_reason() method to flutter_rust_bridge +Future<(QuicConnection, String?)> connectionCloseReason({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeConnectionCloseReason( + connection: connection, +); + +/// Get the statistics of a QUIC connection +/// This exposes the QuicConnection.stats() method to flutter_rust_bridge +Future<(QuicConnection, QuicConnectionStats)> connectionStats({ + required QuicConnection connection, +}) => + RustLib.instance.api.crateApiBridgeConnectionStats(connection: connection); + +/// Get the peer's advertised QUIC transport parameters relevant to stream credits. +/// +/// Exposes [`QuicConnection::peer_transport_params`] across the FFI. Useful for +/// diagnosing `connection_open_bi` hangs caused by the peer advertising a small +/// `initial_max_streams_bidi` and never raising it with `MAX_STREAMS`. +Future<(QuicConnection, QuicPeerTransportParams)> +connectionPeerTransportParams({required QuicConnection connection}) => RustLib + .instance + .api + .crateApiBridgeConnectionPeerTransportParams(connection: connection); + +/// Create a new server config with single certificate +Future serverConfigWithSingleCert({ + required List certChain, + required List key, +}) => RustLib.instance.api.crateApiBridgeServerConfigWithSingleCert( + certChain: certChain, + key: key, +); + +/// Create a new transport config +Future transportConfigNew() => + RustLib.instance.api.crateApiBridgeTransportConfigNew(); + +/// Create a new endpoint config +Future endpointConfigNew() => + RustLib.instance.api.crateApiBridgeEndpointConfigNew(); + +Future exposeTypesForFrbGeneration() => + RustLib.instance.api.crateApiBridgeExposeTypesForFrbGeneration(); + +Future exposeConnectionType({ + required QuicConnection connection, +}) => RustLib.instance.api.crateApiBridgeExposeConnectionType( + connection: connection, +); + +Future exposeSendStreamType({required QuicSendStream stream}) => + RustLib.instance.api.crateApiBridgeExposeSendStreamType(stream: stream); + +Future exposeRecvStreamType({required QuicRecvStream stream}) => + RustLib.instance.api.crateApiBridgeExposeRecvStreamType(stream: stream); + +/// Create a new QuicClient with default configuration +Future quicClientCreate() => + RustLib.instance.api.crateApiBridgeQuicClientCreate(); + +/// Create a new QuicClient with custom configuration +Future quicClientCreateWithConfig({ + required QuicClientConfig config, +}) => RustLib.instance.api.crateApiBridgeQuicClientCreateWithConfig( + config: config, +); + +/// Send data using QuicClient and return response +Future<(QuicClient, String)> quicClientSend({ + required QuicClient client, + required String url, + required String data, +}) => RustLib.instance.api.crateApiBridgeQuicClientSend( + client: client, + url: url, + data: data, +); + +/// Send data with timeout using QuicClient +Future<(QuicClient, String)> quicClientSendWithTimeout({ + required QuicClient client, + required String url, + required String data, +}) => RustLib.instance.api.crateApiBridgeQuicClientSendWithTimeout( + client: client, + url: url, + data: data, +); + +/// Send a GET request using QuicClient +Future<(QuicClient, String)> quicClientGet({ + required QuicClient client, + required String url, +}) => + RustLib.instance.api.crateApiBridgeQuicClientGet(client: client, url: url); + +/// Send a POST request using QuicClient +Future<(QuicClient, String)> quicClientPost({ + required QuicClient client, + required String url, + required String data, +}) => RustLib.instance.api.crateApiBridgeQuicClientPost( + client: client, + url: url, + data: data, +); + +/// Send a GET request with timeout using QuicClient +Future<(QuicClient, String)> quicClientGetWithTimeout({ + required QuicClient client, + required String url, +}) => RustLib.instance.api.crateApiBridgeQuicClientGetWithTimeout( + client: client, + url: url, +); + +/// Send a POST request with timeout using QuicClient +Future<(QuicClient, String)> quicClientPostWithTimeout({ + required QuicClient client, + required String url, + required String data, +}) => RustLib.instance.api.crateApiBridgeQuicClientPostWithTimeout( + client: client, + url: url, + data: data, +); + +/// Get QuicClient configuration +Future<(QuicClient, QuicClientConfig)> quicClientConfig({ + required QuicClient client, +}) => RustLib.instance.api.crateApiBridgeQuicClientConfig(client: client); + +/// Clear QuicClient connection pool +Future quicClientClearPool({required QuicClient client}) => + RustLib.instance.api.crateApiBridgeQuicClientClearPool(client: client); + +/// Create a new QuicClientConfig with default values +Future quicClientConfigNew() => + RustLib.instance.api.crateApiBridgeQuicClientConfigNew(); + +/// Expose QuicClient and QuicClientConfig types for flutter_rust_bridge +Future exposeQuicClientType({required QuicClient client}) => + RustLib.instance.api.crateApiBridgeExposeQuicClientType(client: client); + +Future exposeQuicClientConfigType({ + required QuicClientConfig config, +}) => RustLib.instance.api.crateApiBridgeExposeQuicClientConfigType( + config: config, +); diff --git a/vendor/flutter_quic/lib/src/rust/convenience/client.dart b/vendor/flutter_quic/lib/src/rust/convenience/client.dart new file mode 100644 index 0000000..58829bf --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/convenience/client.dart @@ -0,0 +1,61 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueMoi> +abstract class QuicClient implements RustOpaqueInterface {} + +/// Configuration for QuicClient +class QuicClientConfig { + /// Maximum number of connections per host + final BigInt maxConnectionsPerHost; + + /// Connection timeout in milliseconds + final BigInt connectTimeoutMs; + + /// Request timeout in milliseconds + final BigInt requestTimeoutMs; + + /// Number of retry attempts for failed requests + final int retryAttempts; + + /// Retry delay in milliseconds + final BigInt retryDelayMs; + + /// Keep-alive timeout for connections in milliseconds + final BigInt keepAliveTimeoutMs; + + const QuicClientConfig({ + required this.maxConnectionsPerHost, + required this.connectTimeoutMs, + required this.requestTimeoutMs, + required this.retryAttempts, + required this.retryDelayMs, + required this.keepAliveTimeoutMs, + }); + + @override + int get hashCode => + maxConnectionsPerHost.hashCode ^ + connectTimeoutMs.hashCode ^ + requestTimeoutMs.hashCode ^ + retryAttempts.hashCode ^ + retryDelayMs.hashCode ^ + keepAliveTimeoutMs.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuicClientConfig && + runtimeType == other.runtimeType && + maxConnectionsPerHost == other.maxConnectionsPerHost && + connectTimeoutMs == other.connectTimeoutMs && + requestTimeoutMs == other.requestTimeoutMs && + retryAttempts == other.retryAttempts && + retryDelayMs == other.retryDelayMs && + keepAliveTimeoutMs == other.keepAliveTimeoutMs; +} diff --git a/vendor/flutter_quic/lib/src/rust/core/config.dart b/vendor/flutter_quic/lib/src/rust/core/config.dart new file mode 100644 index 0000000..19e5437 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/core/config.dart @@ -0,0 +1,16 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueMoi> +abstract class QuicEndpointConfig implements RustOpaqueInterface {} + +// Rust type: RustOpaqueMoi> +abstract class QuicServerConfig implements RustOpaqueInterface {} + +// Rust type: RustOpaqueMoi> +abstract class QuicTransportConfig implements RustOpaqueInterface {} diff --git a/vendor/flutter_quic/lib/src/rust/core/connection.dart b/vendor/flutter_quic/lib/src/rust/core/connection.dart new file mode 100644 index 0000000..8372217 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/core/connection.dart @@ -0,0 +1,255 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueMoi> +abstract class QuicConnection implements RustOpaqueInterface {} + +/// Connection statistics from Quinn +class QuicConnectionStats { + final QuicPathStats path; + final QuicFrameStats frameTx; + final QuicFrameStats frameRx; + final QuicUdpStats udpTx; + final QuicUdpStats udpRx; + + const QuicConnectionStats({ + required this.path, + required this.frameTx, + required this.frameRx, + required this.udpTx, + required this.udpRx, + }); + + @override + int get hashCode => + path.hashCode ^ + frameTx.hashCode ^ + frameRx.hashCode ^ + udpTx.hashCode ^ + udpRx.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuicConnectionStats && + runtimeType == other.runtimeType && + path == other.path && + frameTx == other.frameTx && + frameRx == other.frameRx && + udpTx == other.udpTx && + udpRx == other.udpRx; +} + +/// Frame transmission/reception statistics +class QuicFrameStats { + final BigInt acks; + final BigInt crypto; + final BigInt connectionClose; + final BigInt dataBlocked; + final BigInt datagram; + final BigInt handshakeDone; + final BigInt maxData; + final BigInt maxStreamData; + final BigInt maxStreamsBidi; + final BigInt maxStreamsUni; + final BigInt newConnectionId; + final BigInt newToken; + final BigInt pathChallenge; + final BigInt pathResponse; + final BigInt ping; + final BigInt resetStream; + final BigInt retireConnectionId; + final BigInt stream; + final BigInt streamDataBlocked; + final BigInt streamsBlockedBidi; + final BigInt streamsBlockedUni; + final BigInt stopSending; + + const QuicFrameStats({ + required this.acks, + required this.crypto, + required this.connectionClose, + required this.dataBlocked, + required this.datagram, + required this.handshakeDone, + required this.maxData, + required this.maxStreamData, + required this.maxStreamsBidi, + required this.maxStreamsUni, + required this.newConnectionId, + required this.newToken, + required this.pathChallenge, + required this.pathResponse, + required this.ping, + required this.resetStream, + required this.retireConnectionId, + required this.stream, + required this.streamDataBlocked, + required this.streamsBlockedBidi, + required this.streamsBlockedUni, + required this.stopSending, + }); + + @override + int get hashCode => + acks.hashCode ^ + crypto.hashCode ^ + connectionClose.hashCode ^ + dataBlocked.hashCode ^ + datagram.hashCode ^ + handshakeDone.hashCode ^ + maxData.hashCode ^ + maxStreamData.hashCode ^ + maxStreamsBidi.hashCode ^ + maxStreamsUni.hashCode ^ + newConnectionId.hashCode ^ + newToken.hashCode ^ + pathChallenge.hashCode ^ + pathResponse.hashCode ^ + ping.hashCode ^ + resetStream.hashCode ^ + retireConnectionId.hashCode ^ + stream.hashCode ^ + streamDataBlocked.hashCode ^ + streamsBlockedBidi.hashCode ^ + streamsBlockedUni.hashCode ^ + stopSending.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuicFrameStats && + runtimeType == other.runtimeType && + acks == other.acks && + crypto == other.crypto && + connectionClose == other.connectionClose && + dataBlocked == other.dataBlocked && + datagram == other.datagram && + handshakeDone == other.handshakeDone && + maxData == other.maxData && + maxStreamData == other.maxStreamData && + maxStreamsBidi == other.maxStreamsBidi && + maxStreamsUni == other.maxStreamsUni && + newConnectionId == other.newConnectionId && + newToken == other.newToken && + pathChallenge == other.pathChallenge && + pathResponse == other.pathResponse && + ping == other.ping && + resetStream == other.resetStream && + retireConnectionId == other.retireConnectionId && + stream == other.stream && + streamDataBlocked == other.streamDataBlocked && + streamsBlockedBidi == other.streamsBlockedBidi && + streamsBlockedUni == other.streamsBlockedUni && + stopSending == other.stopSending; +} + +/// Path-specific statistics +class QuicPathStats { + final BigInt rttMillis; + final BigInt cwnd; + final BigInt lostPackets; + final BigInt lostBytes; + final BigInt sentPackets; + final BigInt congestionEvents; + + const QuicPathStats({ + required this.rttMillis, + required this.cwnd, + required this.lostPackets, + required this.lostBytes, + required this.sentPackets, + required this.congestionEvents, + }); + + @override + int get hashCode => + rttMillis.hashCode ^ + cwnd.hashCode ^ + lostPackets.hashCode ^ + lostBytes.hashCode ^ + sentPackets.hashCode ^ + congestionEvents.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuicPathStats && + runtimeType == other.runtimeType && + rttMillis == other.rttMillis && + cwnd == other.cwnd && + lostPackets == other.lostPackets && + lostBytes == other.lostBytes && + sentPackets == other.sentPackets && + congestionEvents == other.congestionEvents; +} + +/// Peer-advertised QUIC transport parameters relevant to stream/data credits. +/// +/// These come from the remote peer's `transport_parameters` TLS extension and +/// represent the *initial* limits the peer granted us at handshake time. They +/// may be raised at runtime by `MAX_STREAMS` / `MAX_DATA` frames, which are +/// reflected in [`QuicFrameStats`] (`max_streams_bidi`, `max_streams_uni`, +/// `max_data`), not here. +class QuicPeerTransportParams { + /// Peer's `initial_max_streams_bidi`: the total number of client-initiated + /// bidirectional streams we may open before needing a `MAX_STREAMS` update. + final BigInt initialMaxStreamsBidi; + + /// Peer's `initial_max_streams_uni`. + final BigInt initialMaxStreamsUni; + + /// Peer's `initial_max_data` (connection-level flow-control window, bytes). + final BigInt initialMaxData; + + const QuicPeerTransportParams({ + required this.initialMaxStreamsBidi, + required this.initialMaxStreamsUni, + required this.initialMaxData, + }); + + @override + int get hashCode => + initialMaxStreamsBidi.hashCode ^ + initialMaxStreamsUni.hashCode ^ + initialMaxData.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuicPeerTransportParams && + runtimeType == other.runtimeType && + initialMaxStreamsBidi == other.initialMaxStreamsBidi && + initialMaxStreamsUni == other.initialMaxStreamsUni && + initialMaxData == other.initialMaxData; +} + +/// UDP-level statistics +class QuicUdpStats { + final BigInt datagrams; + final BigInt bytes; + final BigInt ios; + + const QuicUdpStats({ + required this.datagrams, + required this.bytes, + required this.ios, + }); + + @override + int get hashCode => datagrams.hashCode ^ bytes.hashCode ^ ios.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuicUdpStats && + runtimeType == other.runtimeType && + datagrams == other.datagrams && + bytes == other.bytes && + ios == other.ios; +} diff --git a/vendor/flutter_quic/lib/src/rust/core/endpoint.dart b/vendor/flutter_quic/lib/src/rust/core/endpoint.dart new file mode 100644 index 0000000..02f2dfe --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/core/endpoint.dart @@ -0,0 +1,10 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueMoi> +abstract class QuicEndpoint implements RustOpaqueInterface {} diff --git a/vendor/flutter_quic/lib/src/rust/core/stream.dart b/vendor/flutter_quic/lib/src/rust/core/stream.dart new file mode 100644 index 0000000..b77b350 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/core/stream.dart @@ -0,0 +1,13 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueMoi> +abstract class QuicRecvStream implements RustOpaqueInterface {} + +// Rust type: RustOpaqueMoi> +abstract class QuicSendStream implements RustOpaqueInterface {} diff --git a/vendor/flutter_quic/lib/src/rust/errors.dart b/vendor/flutter_quic/lib/src/rust/errors.dart new file mode 100644 index 0000000..07d18be --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/errors.dart @@ -0,0 +1,78 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:freezed_annotation/freezed_annotation.dart' hide protected; +part 'errors.freezed.dart'; + +@freezed +sealed class QuicDatagramException + with _$QuicDatagramException + implements FrbException { + const QuicDatagramException._(); + + const factory QuicDatagramException.unsupportedByPeer() = + QuicDatagramException_UnsupportedByPeer; + const factory QuicDatagramException.tooLarge({required BigInt maxSize}) = + QuicDatagramException_TooLarge; + const factory QuicDatagramException.connectionLost(String field0) = + QuicDatagramException_ConnectionLost; +} + +@freezed +sealed class QuicError with _$QuicError implements FrbException { + const QuicError._(); + + const factory QuicError.connection(String field0) = QuicError_Connection; + const factory QuicError.endpoint(String field0) = QuicError_Endpoint; + const factory QuicError.stream(String field0) = QuicError_Stream; + const factory QuicError.tls(String field0) = QuicError_Tls; + const factory QuicError.config(String field0) = QuicError_Config; + const factory QuicError.network(String field0) = QuicError_Network; + const factory QuicError.write(String field0) = QuicError_Write; +} + +@freezed +sealed class QuicReadException + with _$QuicReadException + implements FrbException { + const QuicReadException._(); + + const factory QuicReadException.reset(BigInt field0) = + QuicReadException_Reset; + const factory QuicReadException.connectionLost(String field0) = + QuicReadException_ConnectionLost; + const factory QuicReadException.zeroRttRejected() = + QuicReadException_ZeroRttRejected; + const factory QuicReadException.closedStream() = + QuicReadException_ClosedStream; + const factory QuicReadException.illegalOrderedRead() = + QuicReadException_IllegalOrderedRead; +} + +@freezed +sealed class QuicReadToEndException + with _$QuicReadToEndException + implements FrbException { + const QuicReadToEndException._(); + + const factory QuicReadToEndException.read(QuicReadException field0) = + QuicReadToEndException_Read; + const factory QuicReadToEndException.tooLong() = + QuicReadToEndException_TooLong; +} + +@freezed +sealed class QuicWriteException + with _$QuicWriteException + implements FrbException { + const QuicWriteException._(); + + const factory QuicWriteException.stopped(BigInt field0) = + QuicWriteException_Stopped; + const factory QuicWriteException.connectionLost(String field0) = + QuicWriteException_ConnectionLost; +} diff --git a/vendor/flutter_quic/lib/src/rust/errors.freezed.dart b/vendor/flutter_quic/lib/src/rust/errors.freezed.dart new file mode 100644 index 0000000..2a34ce4 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/errors.freezed.dart @@ -0,0 +1,1992 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'errors.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$QuicDatagramException { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicDatagramException); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicDatagramException()'; +} + + +} + +/// @nodoc +class $QuicDatagramExceptionCopyWith<$Res> { +$QuicDatagramExceptionCopyWith(QuicDatagramException _, $Res Function(QuicDatagramException) __); +} + + +/// Adds pattern-matching-related methods to [QuicDatagramException]. +extension QuicDatagramExceptionPatterns on QuicDatagramException { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( QuicDatagramException_UnsupportedByPeer value)? unsupportedByPeer,TResult Function( QuicDatagramException_TooLarge value)? tooLarge,TResult Function( QuicDatagramException_ConnectionLost value)? connectionLost,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case QuicDatagramException_UnsupportedByPeer() when unsupportedByPeer != null: +return unsupportedByPeer(_that);case QuicDatagramException_TooLarge() when tooLarge != null: +return tooLarge(_that);case QuicDatagramException_ConnectionLost() when connectionLost != null: +return connectionLost(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( QuicDatagramException_UnsupportedByPeer value) unsupportedByPeer,required TResult Function( QuicDatagramException_TooLarge value) tooLarge,required TResult Function( QuicDatagramException_ConnectionLost value) connectionLost,}){ +final _that = this; +switch (_that) { +case QuicDatagramException_UnsupportedByPeer(): +return unsupportedByPeer(_that);case QuicDatagramException_TooLarge(): +return tooLarge(_that);case QuicDatagramException_ConnectionLost(): +return connectionLost(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( QuicDatagramException_UnsupportedByPeer value)? unsupportedByPeer,TResult? Function( QuicDatagramException_TooLarge value)? tooLarge,TResult? Function( QuicDatagramException_ConnectionLost value)? connectionLost,}){ +final _that = this; +switch (_that) { +case QuicDatagramException_UnsupportedByPeer() when unsupportedByPeer != null: +return unsupportedByPeer(_that);case QuicDatagramException_TooLarge() when tooLarge != null: +return tooLarge(_that);case QuicDatagramException_ConnectionLost() when connectionLost != null: +return connectionLost(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function()? unsupportedByPeer,TResult Function( BigInt maxSize)? tooLarge,TResult Function( String field0)? connectionLost,required TResult orElse(),}) {final _that = this; +switch (_that) { +case QuicDatagramException_UnsupportedByPeer() when unsupportedByPeer != null: +return unsupportedByPeer();case QuicDatagramException_TooLarge() when tooLarge != null: +return tooLarge(_that.maxSize);case QuicDatagramException_ConnectionLost() when connectionLost != null: +return connectionLost(_that.field0);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function() unsupportedByPeer,required TResult Function( BigInt maxSize) tooLarge,required TResult Function( String field0) connectionLost,}) {final _that = this; +switch (_that) { +case QuicDatagramException_UnsupportedByPeer(): +return unsupportedByPeer();case QuicDatagramException_TooLarge(): +return tooLarge(_that.maxSize);case QuicDatagramException_ConnectionLost(): +return connectionLost(_that.field0);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function()? unsupportedByPeer,TResult? Function( BigInt maxSize)? tooLarge,TResult? Function( String field0)? connectionLost,}) {final _that = this; +switch (_that) { +case QuicDatagramException_UnsupportedByPeer() when unsupportedByPeer != null: +return unsupportedByPeer();case QuicDatagramException_TooLarge() when tooLarge != null: +return tooLarge(_that.maxSize);case QuicDatagramException_ConnectionLost() when connectionLost != null: +return connectionLost(_that.field0);case _: + return null; + +} +} + +} + +/// @nodoc + + +class QuicDatagramException_UnsupportedByPeer extends QuicDatagramException { + const QuicDatagramException_UnsupportedByPeer(): super._(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicDatagramException_UnsupportedByPeer); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicDatagramException.unsupportedByPeer()'; +} + + +} + + + + +/// @nodoc + + +class QuicDatagramException_TooLarge extends QuicDatagramException { + const QuicDatagramException_TooLarge({required this.maxSize}): super._(); + + + final BigInt maxSize; + +/// Create a copy of QuicDatagramException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicDatagramException_TooLargeCopyWith get copyWith => _$QuicDatagramException_TooLargeCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicDatagramException_TooLarge&&(identical(other.maxSize, maxSize) || other.maxSize == maxSize)); +} + + +@override +int get hashCode => Object.hash(runtimeType,maxSize); + +@override +String toString() { + return 'QuicDatagramException.tooLarge(maxSize: $maxSize)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicDatagramException_TooLargeCopyWith<$Res> implements $QuicDatagramExceptionCopyWith<$Res> { + factory $QuicDatagramException_TooLargeCopyWith(QuicDatagramException_TooLarge value, $Res Function(QuicDatagramException_TooLarge) _then) = _$QuicDatagramException_TooLargeCopyWithImpl; +@useResult +$Res call({ + BigInt maxSize +}); + + + + +} +/// @nodoc +class _$QuicDatagramException_TooLargeCopyWithImpl<$Res> + implements $QuicDatagramException_TooLargeCopyWith<$Res> { + _$QuicDatagramException_TooLargeCopyWithImpl(this._self, this._then); + + final QuicDatagramException_TooLarge _self; + final $Res Function(QuicDatagramException_TooLarge) _then; + +/// Create a copy of QuicDatagramException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? maxSize = null,}) { + return _then(QuicDatagramException_TooLarge( +maxSize: null == maxSize ? _self.maxSize : maxSize // ignore: cast_nullable_to_non_nullable +as BigInt, + )); +} + + +} + +/// @nodoc + + +class QuicDatagramException_ConnectionLost extends QuicDatagramException { + const QuicDatagramException_ConnectionLost(this.field0): super._(); + + + final String field0; + +/// Create a copy of QuicDatagramException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicDatagramException_ConnectionLostCopyWith get copyWith => _$QuicDatagramException_ConnectionLostCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicDatagramException_ConnectionLost&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicDatagramException.connectionLost(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicDatagramException_ConnectionLostCopyWith<$Res> implements $QuicDatagramExceptionCopyWith<$Res> { + factory $QuicDatagramException_ConnectionLostCopyWith(QuicDatagramException_ConnectionLost value, $Res Function(QuicDatagramException_ConnectionLost) _then) = _$QuicDatagramException_ConnectionLostCopyWithImpl; +@useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicDatagramException_ConnectionLostCopyWithImpl<$Res> + implements $QuicDatagramException_ConnectionLostCopyWith<$Res> { + _$QuicDatagramException_ConnectionLostCopyWithImpl(this._self, this._then); + + final QuicDatagramException_ConnectionLost _self; + final $Res Function(QuicDatagramException_ConnectionLost) _then; + +/// Create a copy of QuicDatagramException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicDatagramException_ConnectionLost( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc +mixin _$QuicError { + + String get field0; +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicErrorCopyWith get copyWith => _$QuicErrorCopyWithImpl(this as QuicError, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicErrorCopyWith<$Res> { + factory $QuicErrorCopyWith(QuicError value, $Res Function(QuicError) _then) = _$QuicErrorCopyWithImpl; +@useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicErrorCopyWithImpl<$Res> + implements $QuicErrorCopyWith<$Res> { + _$QuicErrorCopyWithImpl(this._self, this._then); + + final QuicError _self; + final $Res Function(QuicError) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? field0 = null,}) { + return _then(_self.copyWith( +field0: null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [QuicError]. +extension QuicErrorPatterns on QuicError { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( QuicError_Connection value)? connection,TResult Function( QuicError_Endpoint value)? endpoint,TResult Function( QuicError_Stream value)? stream,TResult Function( QuicError_Tls value)? tls,TResult Function( QuicError_Config value)? config,TResult Function( QuicError_Network value)? network,TResult Function( QuicError_Write value)? write,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case QuicError_Connection() when connection != null: +return connection(_that);case QuicError_Endpoint() when endpoint != null: +return endpoint(_that);case QuicError_Stream() when stream != null: +return stream(_that);case QuicError_Tls() when tls != null: +return tls(_that);case QuicError_Config() when config != null: +return config(_that);case QuicError_Network() when network != null: +return network(_that);case QuicError_Write() when write != null: +return write(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( QuicError_Connection value) connection,required TResult Function( QuicError_Endpoint value) endpoint,required TResult Function( QuicError_Stream value) stream,required TResult Function( QuicError_Tls value) tls,required TResult Function( QuicError_Config value) config,required TResult Function( QuicError_Network value) network,required TResult Function( QuicError_Write value) write,}){ +final _that = this; +switch (_that) { +case QuicError_Connection(): +return connection(_that);case QuicError_Endpoint(): +return endpoint(_that);case QuicError_Stream(): +return stream(_that);case QuicError_Tls(): +return tls(_that);case QuicError_Config(): +return config(_that);case QuicError_Network(): +return network(_that);case QuicError_Write(): +return write(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( QuicError_Connection value)? connection,TResult? Function( QuicError_Endpoint value)? endpoint,TResult? Function( QuicError_Stream value)? stream,TResult? Function( QuicError_Tls value)? tls,TResult? Function( QuicError_Config value)? config,TResult? Function( QuicError_Network value)? network,TResult? Function( QuicError_Write value)? write,}){ +final _that = this; +switch (_that) { +case QuicError_Connection() when connection != null: +return connection(_that);case QuicError_Endpoint() when endpoint != null: +return endpoint(_that);case QuicError_Stream() when stream != null: +return stream(_that);case QuicError_Tls() when tls != null: +return tls(_that);case QuicError_Config() when config != null: +return config(_that);case QuicError_Network() when network != null: +return network(_that);case QuicError_Write() when write != null: +return write(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( String field0)? connection,TResult Function( String field0)? endpoint,TResult Function( String field0)? stream,TResult Function( String field0)? tls,TResult Function( String field0)? config,TResult Function( String field0)? network,TResult Function( String field0)? write,required TResult orElse(),}) {final _that = this; +switch (_that) { +case QuicError_Connection() when connection != null: +return connection(_that.field0);case QuicError_Endpoint() when endpoint != null: +return endpoint(_that.field0);case QuicError_Stream() when stream != null: +return stream(_that.field0);case QuicError_Tls() when tls != null: +return tls(_that.field0);case QuicError_Config() when config != null: +return config(_that.field0);case QuicError_Network() when network != null: +return network(_that.field0);case QuicError_Write() when write != null: +return write(_that.field0);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( String field0) connection,required TResult Function( String field0) endpoint,required TResult Function( String field0) stream,required TResult Function( String field0) tls,required TResult Function( String field0) config,required TResult Function( String field0) network,required TResult Function( String field0) write,}) {final _that = this; +switch (_that) { +case QuicError_Connection(): +return connection(_that.field0);case QuicError_Endpoint(): +return endpoint(_that.field0);case QuicError_Stream(): +return stream(_that.field0);case QuicError_Tls(): +return tls(_that.field0);case QuicError_Config(): +return config(_that.field0);case QuicError_Network(): +return network(_that.field0);case QuicError_Write(): +return write(_that.field0);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String field0)? connection,TResult? Function( String field0)? endpoint,TResult? Function( String field0)? stream,TResult? Function( String field0)? tls,TResult? Function( String field0)? config,TResult? Function( String field0)? network,TResult? Function( String field0)? write,}) {final _that = this; +switch (_that) { +case QuicError_Connection() when connection != null: +return connection(_that.field0);case QuicError_Endpoint() when endpoint != null: +return endpoint(_that.field0);case QuicError_Stream() when stream != null: +return stream(_that.field0);case QuicError_Tls() when tls != null: +return tls(_that.field0);case QuicError_Config() when config != null: +return config(_that.field0);case QuicError_Network() when network != null: +return network(_that.field0);case QuicError_Write() when write != null: +return write(_that.field0);case _: + return null; + +} +} + +} + +/// @nodoc + + +class QuicError_Connection extends QuicError { + const QuicError_Connection(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_ConnectionCopyWith get copyWith => _$QuicError_ConnectionCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Connection&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.connection(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_ConnectionCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_ConnectionCopyWith(QuicError_Connection value, $Res Function(QuicError_Connection) _then) = _$QuicError_ConnectionCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_ConnectionCopyWithImpl<$Res> + implements $QuicError_ConnectionCopyWith<$Res> { + _$QuicError_ConnectionCopyWithImpl(this._self, this._then); + + final QuicError_Connection _self; + final $Res Function(QuicError_Connection) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Connection( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicError_Endpoint extends QuicError { + const QuicError_Endpoint(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_EndpointCopyWith get copyWith => _$QuicError_EndpointCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Endpoint&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.endpoint(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_EndpointCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_EndpointCopyWith(QuicError_Endpoint value, $Res Function(QuicError_Endpoint) _then) = _$QuicError_EndpointCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_EndpointCopyWithImpl<$Res> + implements $QuicError_EndpointCopyWith<$Res> { + _$QuicError_EndpointCopyWithImpl(this._self, this._then); + + final QuicError_Endpoint _self; + final $Res Function(QuicError_Endpoint) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Endpoint( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicError_Stream extends QuicError { + const QuicError_Stream(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_StreamCopyWith get copyWith => _$QuicError_StreamCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Stream&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.stream(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_StreamCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_StreamCopyWith(QuicError_Stream value, $Res Function(QuicError_Stream) _then) = _$QuicError_StreamCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_StreamCopyWithImpl<$Res> + implements $QuicError_StreamCopyWith<$Res> { + _$QuicError_StreamCopyWithImpl(this._self, this._then); + + final QuicError_Stream _self; + final $Res Function(QuicError_Stream) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Stream( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicError_Tls extends QuicError { + const QuicError_Tls(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_TlsCopyWith get copyWith => _$QuicError_TlsCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Tls&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.tls(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_TlsCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_TlsCopyWith(QuicError_Tls value, $Res Function(QuicError_Tls) _then) = _$QuicError_TlsCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_TlsCopyWithImpl<$Res> + implements $QuicError_TlsCopyWith<$Res> { + _$QuicError_TlsCopyWithImpl(this._self, this._then); + + final QuicError_Tls _self; + final $Res Function(QuicError_Tls) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Tls( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicError_Config extends QuicError { + const QuicError_Config(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_ConfigCopyWith get copyWith => _$QuicError_ConfigCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Config&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.config(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_ConfigCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_ConfigCopyWith(QuicError_Config value, $Res Function(QuicError_Config) _then) = _$QuicError_ConfigCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_ConfigCopyWithImpl<$Res> + implements $QuicError_ConfigCopyWith<$Res> { + _$QuicError_ConfigCopyWithImpl(this._self, this._then); + + final QuicError_Config _self; + final $Res Function(QuicError_Config) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Config( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicError_Network extends QuicError { + const QuicError_Network(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_NetworkCopyWith get copyWith => _$QuicError_NetworkCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Network&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.network(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_NetworkCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_NetworkCopyWith(QuicError_Network value, $Res Function(QuicError_Network) _then) = _$QuicError_NetworkCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_NetworkCopyWithImpl<$Res> + implements $QuicError_NetworkCopyWith<$Res> { + _$QuicError_NetworkCopyWithImpl(this._self, this._then); + + final QuicError_Network _self; + final $Res Function(QuicError_Network) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Network( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicError_Write extends QuicError { + const QuicError_Write(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicError_WriteCopyWith get copyWith => _$QuicError_WriteCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicError_Write&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicError.write(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicError_WriteCopyWith<$Res> implements $QuicErrorCopyWith<$Res> { + factory $QuicError_WriteCopyWith(QuicError_Write value, $Res Function(QuicError_Write) _then) = _$QuicError_WriteCopyWithImpl; +@override @useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicError_WriteCopyWithImpl<$Res> + implements $QuicError_WriteCopyWith<$Res> { + _$QuicError_WriteCopyWithImpl(this._self, this._then); + + final QuicError_Write _self; + final $Res Function(QuicError_Write) _then; + +/// Create a copy of QuicError +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicError_Write( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc +mixin _$QuicReadException { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadException); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicReadException()'; +} + + +} + +/// @nodoc +class $QuicReadExceptionCopyWith<$Res> { +$QuicReadExceptionCopyWith(QuicReadException _, $Res Function(QuicReadException) __); +} + + +/// Adds pattern-matching-related methods to [QuicReadException]. +extension QuicReadExceptionPatterns on QuicReadException { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( QuicReadException_Reset value)? reset,TResult Function( QuicReadException_ConnectionLost value)? connectionLost,TResult Function( QuicReadException_ZeroRttRejected value)? zeroRttRejected,TResult Function( QuicReadException_ClosedStream value)? closedStream,TResult Function( QuicReadException_IllegalOrderedRead value)? illegalOrderedRead,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case QuicReadException_Reset() when reset != null: +return reset(_that);case QuicReadException_ConnectionLost() when connectionLost != null: +return connectionLost(_that);case QuicReadException_ZeroRttRejected() when zeroRttRejected != null: +return zeroRttRejected(_that);case QuicReadException_ClosedStream() when closedStream != null: +return closedStream(_that);case QuicReadException_IllegalOrderedRead() when illegalOrderedRead != null: +return illegalOrderedRead(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( QuicReadException_Reset value) reset,required TResult Function( QuicReadException_ConnectionLost value) connectionLost,required TResult Function( QuicReadException_ZeroRttRejected value) zeroRttRejected,required TResult Function( QuicReadException_ClosedStream value) closedStream,required TResult Function( QuicReadException_IllegalOrderedRead value) illegalOrderedRead,}){ +final _that = this; +switch (_that) { +case QuicReadException_Reset(): +return reset(_that);case QuicReadException_ConnectionLost(): +return connectionLost(_that);case QuicReadException_ZeroRttRejected(): +return zeroRttRejected(_that);case QuicReadException_ClosedStream(): +return closedStream(_that);case QuicReadException_IllegalOrderedRead(): +return illegalOrderedRead(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( QuicReadException_Reset value)? reset,TResult? Function( QuicReadException_ConnectionLost value)? connectionLost,TResult? Function( QuicReadException_ZeroRttRejected value)? zeroRttRejected,TResult? Function( QuicReadException_ClosedStream value)? closedStream,TResult? Function( QuicReadException_IllegalOrderedRead value)? illegalOrderedRead,}){ +final _that = this; +switch (_that) { +case QuicReadException_Reset() when reset != null: +return reset(_that);case QuicReadException_ConnectionLost() when connectionLost != null: +return connectionLost(_that);case QuicReadException_ZeroRttRejected() when zeroRttRejected != null: +return zeroRttRejected(_that);case QuicReadException_ClosedStream() when closedStream != null: +return closedStream(_that);case QuicReadException_IllegalOrderedRead() when illegalOrderedRead != null: +return illegalOrderedRead(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( BigInt field0)? reset,TResult Function( String field0)? connectionLost,TResult Function()? zeroRttRejected,TResult Function()? closedStream,TResult Function()? illegalOrderedRead,required TResult orElse(),}) {final _that = this; +switch (_that) { +case QuicReadException_Reset() when reset != null: +return reset(_that.field0);case QuicReadException_ConnectionLost() when connectionLost != null: +return connectionLost(_that.field0);case QuicReadException_ZeroRttRejected() when zeroRttRejected != null: +return zeroRttRejected();case QuicReadException_ClosedStream() when closedStream != null: +return closedStream();case QuicReadException_IllegalOrderedRead() when illegalOrderedRead != null: +return illegalOrderedRead();case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( BigInt field0) reset,required TResult Function( String field0) connectionLost,required TResult Function() zeroRttRejected,required TResult Function() closedStream,required TResult Function() illegalOrderedRead,}) {final _that = this; +switch (_that) { +case QuicReadException_Reset(): +return reset(_that.field0);case QuicReadException_ConnectionLost(): +return connectionLost(_that.field0);case QuicReadException_ZeroRttRejected(): +return zeroRttRejected();case QuicReadException_ClosedStream(): +return closedStream();case QuicReadException_IllegalOrderedRead(): +return illegalOrderedRead();} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( BigInt field0)? reset,TResult? Function( String field0)? connectionLost,TResult? Function()? zeroRttRejected,TResult? Function()? closedStream,TResult? Function()? illegalOrderedRead,}) {final _that = this; +switch (_that) { +case QuicReadException_Reset() when reset != null: +return reset(_that.field0);case QuicReadException_ConnectionLost() when connectionLost != null: +return connectionLost(_that.field0);case QuicReadException_ZeroRttRejected() when zeroRttRejected != null: +return zeroRttRejected();case QuicReadException_ClosedStream() when closedStream != null: +return closedStream();case QuicReadException_IllegalOrderedRead() when illegalOrderedRead != null: +return illegalOrderedRead();case _: + return null; + +} +} + +} + +/// @nodoc + + +class QuicReadException_Reset extends QuicReadException { + const QuicReadException_Reset(this.field0): super._(); + + + final BigInt field0; + +/// Create a copy of QuicReadException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicReadException_ResetCopyWith get copyWith => _$QuicReadException_ResetCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadException_Reset&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicReadException.reset(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicReadException_ResetCopyWith<$Res> implements $QuicReadExceptionCopyWith<$Res> { + factory $QuicReadException_ResetCopyWith(QuicReadException_Reset value, $Res Function(QuicReadException_Reset) _then) = _$QuicReadException_ResetCopyWithImpl; +@useResult +$Res call({ + BigInt field0 +}); + + + + +} +/// @nodoc +class _$QuicReadException_ResetCopyWithImpl<$Res> + implements $QuicReadException_ResetCopyWith<$Res> { + _$QuicReadException_ResetCopyWithImpl(this._self, this._then); + + final QuicReadException_Reset _self; + final $Res Function(QuicReadException_Reset) _then; + +/// Create a copy of QuicReadException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicReadException_Reset( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as BigInt, + )); +} + + +} + +/// @nodoc + + +class QuicReadException_ConnectionLost extends QuicReadException { + const QuicReadException_ConnectionLost(this.field0): super._(); + + + final String field0; + +/// Create a copy of QuicReadException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicReadException_ConnectionLostCopyWith get copyWith => _$QuicReadException_ConnectionLostCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadException_ConnectionLost&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicReadException.connectionLost(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicReadException_ConnectionLostCopyWith<$Res> implements $QuicReadExceptionCopyWith<$Res> { + factory $QuicReadException_ConnectionLostCopyWith(QuicReadException_ConnectionLost value, $Res Function(QuicReadException_ConnectionLost) _then) = _$QuicReadException_ConnectionLostCopyWithImpl; +@useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicReadException_ConnectionLostCopyWithImpl<$Res> + implements $QuicReadException_ConnectionLostCopyWith<$Res> { + _$QuicReadException_ConnectionLostCopyWithImpl(this._self, this._then); + + final QuicReadException_ConnectionLost _self; + final $Res Function(QuicReadException_ConnectionLost) _then; + +/// Create a copy of QuicReadException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicReadException_ConnectionLost( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class QuicReadException_ZeroRttRejected extends QuicReadException { + const QuicReadException_ZeroRttRejected(): super._(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadException_ZeroRttRejected); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicReadException.zeroRttRejected()'; +} + + +} + + + + +/// @nodoc + + +class QuicReadException_ClosedStream extends QuicReadException { + const QuicReadException_ClosedStream(): super._(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadException_ClosedStream); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicReadException.closedStream()'; +} + + +} + + + + +/// @nodoc + + +class QuicReadException_IllegalOrderedRead extends QuicReadException { + const QuicReadException_IllegalOrderedRead(): super._(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadException_IllegalOrderedRead); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicReadException.illegalOrderedRead()'; +} + + +} + + + + +/// @nodoc +mixin _$QuicReadToEndException { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadToEndException); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicReadToEndException()'; +} + + +} + +/// @nodoc +class $QuicReadToEndExceptionCopyWith<$Res> { +$QuicReadToEndExceptionCopyWith(QuicReadToEndException _, $Res Function(QuicReadToEndException) __); +} + + +/// Adds pattern-matching-related methods to [QuicReadToEndException]. +extension QuicReadToEndExceptionPatterns on QuicReadToEndException { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( QuicReadToEndException_Read value)? read,TResult Function( QuicReadToEndException_TooLong value)? tooLong,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case QuicReadToEndException_Read() when read != null: +return read(_that);case QuicReadToEndException_TooLong() when tooLong != null: +return tooLong(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( QuicReadToEndException_Read value) read,required TResult Function( QuicReadToEndException_TooLong value) tooLong,}){ +final _that = this; +switch (_that) { +case QuicReadToEndException_Read(): +return read(_that);case QuicReadToEndException_TooLong(): +return tooLong(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( QuicReadToEndException_Read value)? read,TResult? Function( QuicReadToEndException_TooLong value)? tooLong,}){ +final _that = this; +switch (_that) { +case QuicReadToEndException_Read() when read != null: +return read(_that);case QuicReadToEndException_TooLong() when tooLong != null: +return tooLong(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( QuicReadException field0)? read,TResult Function()? tooLong,required TResult orElse(),}) {final _that = this; +switch (_that) { +case QuicReadToEndException_Read() when read != null: +return read(_that.field0);case QuicReadToEndException_TooLong() when tooLong != null: +return tooLong();case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( QuicReadException field0) read,required TResult Function() tooLong,}) {final _that = this; +switch (_that) { +case QuicReadToEndException_Read(): +return read(_that.field0);case QuicReadToEndException_TooLong(): +return tooLong();} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( QuicReadException field0)? read,TResult? Function()? tooLong,}) {final _that = this; +switch (_that) { +case QuicReadToEndException_Read() when read != null: +return read(_that.field0);case QuicReadToEndException_TooLong() when tooLong != null: +return tooLong();case _: + return null; + +} +} + +} + +/// @nodoc + + +class QuicReadToEndException_Read extends QuicReadToEndException { + const QuicReadToEndException_Read(this.field0): super._(); + + + final QuicReadException field0; + +/// Create a copy of QuicReadToEndException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicReadToEndException_ReadCopyWith get copyWith => _$QuicReadToEndException_ReadCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadToEndException_Read&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicReadToEndException.read(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicReadToEndException_ReadCopyWith<$Res> implements $QuicReadToEndExceptionCopyWith<$Res> { + factory $QuicReadToEndException_ReadCopyWith(QuicReadToEndException_Read value, $Res Function(QuicReadToEndException_Read) _then) = _$QuicReadToEndException_ReadCopyWithImpl; +@useResult +$Res call({ + QuicReadException field0 +}); + + +$QuicReadExceptionCopyWith<$Res> get field0; + +} +/// @nodoc +class _$QuicReadToEndException_ReadCopyWithImpl<$Res> + implements $QuicReadToEndException_ReadCopyWith<$Res> { + _$QuicReadToEndException_ReadCopyWithImpl(this._self, this._then); + + final QuicReadToEndException_Read _self; + final $Res Function(QuicReadToEndException_Read) _then; + +/// Create a copy of QuicReadToEndException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicReadToEndException_Read( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as QuicReadException, + )); +} + +/// Create a copy of QuicReadToEndException +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$QuicReadExceptionCopyWith<$Res> get field0 { + + return $QuicReadExceptionCopyWith<$Res>(_self.field0, (value) { + return _then(_self.copyWith(field0: value)); + }); +} +} + +/// @nodoc + + +class QuicReadToEndException_TooLong extends QuicReadToEndException { + const QuicReadToEndException_TooLong(): super._(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicReadToEndException_TooLong); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QuicReadToEndException.tooLong()'; +} + + +} + + + + +/// @nodoc +mixin _$QuicWriteException { + + Object get field0; + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicWriteException&&const DeepCollectionEquality().equals(other.field0, field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(field0)); + +@override +String toString() { + return 'QuicWriteException(field0: $field0)'; +} + + +} + +/// @nodoc +class $QuicWriteExceptionCopyWith<$Res> { +$QuicWriteExceptionCopyWith(QuicWriteException _, $Res Function(QuicWriteException) __); +} + + +/// Adds pattern-matching-related methods to [QuicWriteException]. +extension QuicWriteExceptionPatterns on QuicWriteException { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( QuicWriteException_Stopped value)? stopped,TResult Function( QuicWriteException_ConnectionLost value)? connectionLost,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case QuicWriteException_Stopped() when stopped != null: +return stopped(_that);case QuicWriteException_ConnectionLost() when connectionLost != null: +return connectionLost(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( QuicWriteException_Stopped value) stopped,required TResult Function( QuicWriteException_ConnectionLost value) connectionLost,}){ +final _that = this; +switch (_that) { +case QuicWriteException_Stopped(): +return stopped(_that);case QuicWriteException_ConnectionLost(): +return connectionLost(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( QuicWriteException_Stopped value)? stopped,TResult? Function( QuicWriteException_ConnectionLost value)? connectionLost,}){ +final _that = this; +switch (_that) { +case QuicWriteException_Stopped() when stopped != null: +return stopped(_that);case QuicWriteException_ConnectionLost() when connectionLost != null: +return connectionLost(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( BigInt field0)? stopped,TResult Function( String field0)? connectionLost,required TResult orElse(),}) {final _that = this; +switch (_that) { +case QuicWriteException_Stopped() when stopped != null: +return stopped(_that.field0);case QuicWriteException_ConnectionLost() when connectionLost != null: +return connectionLost(_that.field0);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( BigInt field0) stopped,required TResult Function( String field0) connectionLost,}) {final _that = this; +switch (_that) { +case QuicWriteException_Stopped(): +return stopped(_that.field0);case QuicWriteException_ConnectionLost(): +return connectionLost(_that.field0);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( BigInt field0)? stopped,TResult? Function( String field0)? connectionLost,}) {final _that = this; +switch (_that) { +case QuicWriteException_Stopped() when stopped != null: +return stopped(_that.field0);case QuicWriteException_ConnectionLost() when connectionLost != null: +return connectionLost(_that.field0);case _: + return null; + +} +} + +} + +/// @nodoc + + +class QuicWriteException_Stopped extends QuicWriteException { + const QuicWriteException_Stopped(this.field0): super._(); + + +@override final BigInt field0; + +/// Create a copy of QuicWriteException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicWriteException_StoppedCopyWith get copyWith => _$QuicWriteException_StoppedCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicWriteException_Stopped&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicWriteException.stopped(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicWriteException_StoppedCopyWith<$Res> implements $QuicWriteExceptionCopyWith<$Res> { + factory $QuicWriteException_StoppedCopyWith(QuicWriteException_Stopped value, $Res Function(QuicWriteException_Stopped) _then) = _$QuicWriteException_StoppedCopyWithImpl; +@useResult +$Res call({ + BigInt field0 +}); + + + + +} +/// @nodoc +class _$QuicWriteException_StoppedCopyWithImpl<$Res> + implements $QuicWriteException_StoppedCopyWith<$Res> { + _$QuicWriteException_StoppedCopyWithImpl(this._self, this._then); + + final QuicWriteException_Stopped _self; + final $Res Function(QuicWriteException_Stopped) _then; + +/// Create a copy of QuicWriteException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicWriteException_Stopped( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as BigInt, + )); +} + + +} + +/// @nodoc + + +class QuicWriteException_ConnectionLost extends QuicWriteException { + const QuicWriteException_ConnectionLost(this.field0): super._(); + + +@override final String field0; + +/// Create a copy of QuicWriteException +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuicWriteException_ConnectionLostCopyWith get copyWith => _$QuicWriteException_ConnectionLostCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QuicWriteException_ConnectionLost&&(identical(other.field0, field0) || other.field0 == field0)); +} + + +@override +int get hashCode => Object.hash(runtimeType,field0); + +@override +String toString() { + return 'QuicWriteException.connectionLost(field0: $field0)'; +} + + +} + +/// @nodoc +abstract mixin class $QuicWriteException_ConnectionLostCopyWith<$Res> implements $QuicWriteExceptionCopyWith<$Res> { + factory $QuicWriteException_ConnectionLostCopyWith(QuicWriteException_ConnectionLost value, $Res Function(QuicWriteException_ConnectionLost) _then) = _$QuicWriteException_ConnectionLostCopyWithImpl; +@useResult +$Res call({ + String field0 +}); + + + + +} +/// @nodoc +class _$QuicWriteException_ConnectionLostCopyWithImpl<$Res> + implements $QuicWriteException_ConnectionLostCopyWith<$Res> { + _$QuicWriteException_ConnectionLostCopyWithImpl(this._self, this._then); + + final QuicWriteException_ConnectionLost _self; + final $Res Function(QuicWriteException_ConnectionLost) _then; + +/// Create a copy of QuicWriteException +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? field0 = null,}) { + return _then(QuicWriteException_ConnectionLost( +null == field0 ? _self.field0 : field0 // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/vendor/flutter_quic/lib/src/rust/frb_generated.dart b/vendor/flutter_quic/lib/src/rust/frb_generated.dart new file mode 100644 index 0000000..599ab01 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/frb_generated.dart @@ -0,0 +1,4792 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api/bridge.dart'; +import 'convenience/client.dart'; +import 'core/config.dart'; +import 'core/connection.dart'; +import 'core/endpoint.dart'; +import 'core/stream.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'errors.dart'; +import 'frb_generated.dart'; +import 'frb_generated.io.dart' + if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'models/types.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Main entrypoint of the Rust API +class RustLib extends BaseEntrypoint { + @internal + static final instance = RustLib._(); + + RustLib._(); + + /// Initialize flutter_rust_bridge + static Future init({ + RustLibApi? api, + BaseHandler? handler, + ExternalLibrary? externalLibrary, + bool forceSameCodegenVersion = true, + }) async { + await instance.initImpl( + api: api, + handler: handler, + externalLibrary: externalLibrary, + forceSameCodegenVersion: forceSameCodegenVersion, + ); + } + + /// Initialize flutter_rust_bridge in mock mode. + /// No libraries for FFI are loaded. + static void initMock({required RustLibApi api}) { + instance.initMockImpl(api: api); + } + + /// Dispose flutter_rust_bridge + /// + /// The call to this function is optional, since flutter_rust_bridge (and everything else) + /// is automatically disposed when the app stops. + static void dispose() => instance.disposeImpl(); + + @override + ApiImplConstructor get apiImplConstructor => + RustLibApiImpl.new; + + @override + WireConstructor get wireConstructor => + RustLibWire.fromExternalLibrary; + + @override + Future executeRustInitializers() async { + await api.crateApiBridgeInitApp(); + } + + @override + ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => + kDefaultExternalLibraryLoaderConfig; + + @override + String get codegenVersion => '2.11.1'; + + @override + int get rustContentHash => 421219948; + + static const kDefaultExternalLibraryLoaderConfig = + ExternalLibraryLoaderConfig( + stem: 'flutter_quic', + ioDirectory: 'rust/target/release/', + webPrefix: 'pkg/', + ); +} + +abstract class RustLibApi extends BaseApi { + Future crateApiBridgeExposeConnectionType({ + required QuicConnection connection, + }); + + Future crateApiBridgeExposeQuicClientConfigType({ + required QuicClientConfig config, + }); + + Future crateApiBridgeExposeQuicClientType({ + required QuicClient client, + }); + + Future crateApiBridgeExposeRecvStreamType({ + required QuicRecvStream stream, + }); + + Future crateApiBridgeExposeSendStreamType({ + required QuicSendStream stream, + }); + + Future crateApiBridgeExposeTypesForFrbGeneration(); + + Future<(QuicSendStream?, QuicRecvStream?)> crateApiBridgeConnectionAcceptBi({ + required QuicConnection connection, + }); + + Future<(QuicConnection, String?)> crateApiBridgeConnectionCloseReason({ + required QuicConnection connection, + }); + + Future<(QuicConnection, BigInt)> + crateApiBridgeConnectionDatagramSendBufferSpace({ + required QuicConnection connection, + }); + + Future<(QuicConnection, String?)> crateApiBridgeConnectionLocalIp({ + required QuicConnection connection, + }); + + Future<(QuicConnection, BigInt?)> crateApiBridgeConnectionMaxDatagramSize({ + required QuicConnection connection, + }); + + Future<(QuicConnection, QuicSendStream, QuicRecvStream)> + crateApiBridgeConnectionOpenBi({required QuicConnection connection}); + + Future<(QuicConnection, QuicSendStream)> crateApiBridgeConnectionOpenUni({ + required QuicConnection connection, + }); + + Future<(QuicConnection, QuicPeerTransportParams)> + crateApiBridgeConnectionPeerTransportParams({ + required QuicConnection connection, + }); + + Future<(QuicConnection, Uint8List?)> crateApiBridgeConnectionReadDatagram({ + required QuicConnection connection, + }); + + Future<(QuicConnection, SocketAddress)> + crateApiBridgeConnectionRemoteAddress({required QuicConnection connection}); + + Future<(QuicConnection, BigInt)> crateApiBridgeConnectionRttMillis({ + required QuicConnection connection, + }); + + Future crateApiBridgeConnectionSendDatagram({ + required QuicConnection connection, + required List data, + }); + + Future crateApiBridgeConnectionSendDatagramWait({ + required QuicConnection connection, + required List data, + }); + + Future<(QuicConnection, BigInt)> crateApiBridgeConnectionStableId({ + required QuicConnection connection, + }); + + Future<(QuicConnection, QuicConnectionStats)> crateApiBridgeConnectionStats({ + required QuicConnection connection, + }); + + Future crateApiBridgeCreateClientEndpoint(); + + Future crateApiBridgeCreateServerEndpoint({ + required QuicServerConfig config, + required String addr, + }); + + Future crateApiBridgeEndpointConfigNew(); + + Future<(QuicEndpoint, QuicConnection)> crateApiBridgeEndpointConnect({ + required QuicEndpoint endpoint, + required String addr, + required String serverName, + String? qlogPath, + }); + + Future crateApiBridgeInitApp(); + + Future crateApiBridgeQuicClientClearPool({ + required QuicClient client, + }); + + Future<(QuicClient, QuicClientConfig)> crateApiBridgeQuicClientConfig({ + required QuicClient client, + }); + + Future crateApiBridgeQuicClientConfigNew(); + + Future crateApiBridgeQuicClientCreate(); + + Future crateApiBridgeQuicClientCreateWithConfig({ + required QuicClientConfig config, + }); + + Future<(QuicClient, String)> crateApiBridgeQuicClientGet({ + required QuicClient client, + required String url, + }); + + Future<(QuicClient, String)> crateApiBridgeQuicClientGetWithTimeout({ + required QuicClient client, + required String url, + }); + + Future<(QuicClient, String)> crateApiBridgeQuicClientPost({ + required QuicClient client, + required String url, + required String data, + }); + + Future<(QuicClient, String)> crateApiBridgeQuicClientPostWithTimeout({ + required QuicClient client, + required String url, + required String data, + }); + + Future<(QuicClient, String)> crateApiBridgeQuicClientSend({ + required QuicClient client, + required String url, + required String data, + }); + + Future<(QuicClient, String)> crateApiBridgeQuicClientSendWithTimeout({ + required QuicClient client, + required String url, + required String data, + }); + + Future<(QuicRecvStream, Uint8List?)> crateApiBridgeRecvStreamRead({ + required QuicRecvStream stream, + required BigInt maxLength, + }); + + Future<(QuicRecvStream, Uint8List)> crateApiBridgeRecvStreamReadToEnd({ + required QuicRecvStream stream, + required BigInt maxLength, + }); + + Future crateApiBridgeSendStreamFinish({ + required QuicSendStream stream, + }); + + Future<(QuicSendStream, BigInt)> crateApiBridgeSendStreamWrite({ + required QuicSendStream stream, + required List data, + }); + + Future crateApiBridgeSendStreamWriteAll({ + required QuicSendStream stream, + required List data, + }); + + Future crateApiBridgeServerConfigWithSingleCert({ + required List certChain, + required List key, + }); + + Future crateApiBridgeTransportConfigNew(); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicClient; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicClient; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_QuicClientPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicConnection; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicConnection; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicConnectionPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicEndpoint; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicEndpoint; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_QuicEndpointPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicEndpointConfig; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicEndpointConfig; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicEndpointConfigPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicRecvStream; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicRecvStream; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicRecvStreamPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicSendStream; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicSendStream; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicSendStreamPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicServerConfig; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicServerConfig; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicServerConfigPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicTransportConfig; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicTransportConfig; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicTransportConfigPtr; +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + Future crateApiBridgeExposeConnectionType({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 1, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeExposeConnectionTypeConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeExposeConnectionTypeConstMeta => + const TaskConstMeta( + debugName: "_expose_connection_type", + argNames: ["connection"], + ); + + @override + Future crateApiBridgeExposeQuicClientConfigType({ + required QuicClientConfig config, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_quic_client_config(config, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 2, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_quic_client_config, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeExposeQuicClientConfigTypeConstMeta, + argValues: [config], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeExposeQuicClientConfigTypeConstMeta => + const TaskConstMeta( + debugName: "_expose_quic_client_config_type", + argNames: ["config"], + ); + + @override + Future crateApiBridgeExposeQuicClientType({ + required QuicClient client, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeExposeQuicClientTypeConstMeta, + argValues: [client], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeExposeQuicClientTypeConstMeta => + const TaskConstMeta( + debugName: "_expose_quic_client_type", + argNames: ["client"], + ); + + @override + Future crateApiBridgeExposeRecvStreamType({ + required QuicRecvStream stream, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + stream, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 4, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeExposeRecvStreamTypeConstMeta, + argValues: [stream], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeExposeRecvStreamTypeConstMeta => + const TaskConstMeta( + debugName: "_expose_recv_stream_type", + argNames: ["stream"], + ); + + @override + Future crateApiBridgeExposeSendStreamType({ + required QuicSendStream stream, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + stream, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeExposeSendStreamTypeConstMeta, + argValues: [stream], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeExposeSendStreamTypeConstMeta => + const TaskConstMeta( + debugName: "_expose_send_stream_type", + argNames: ["stream"], + ); + + @override + Future crateApiBridgeExposeTypesForFrbGeneration() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 6, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeExposeTypesForFrbGenerationConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeExposeTypesForFrbGenerationConstMeta => + const TaskConstMeta( + debugName: "_expose_types_for_frb_generation", + argNames: [], + ); + + @override + Future<(QuicSendStream?, QuicRecvStream?)> crateApiBridgeConnectionAcceptBi({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 7, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeConnectionAcceptBiConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionAcceptBiConstMeta => + const TaskConstMeta( + debugName: "connection_accept_bi", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, String?)> crateApiBridgeConnectionCloseReason({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 8, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionCloseReasonConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionCloseReasonConstMeta => + const TaskConstMeta( + debugName: "connection_close_reason", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, BigInt)> + crateApiBridgeConnectionDatagramSendBufferSpace({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 9, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionDatagramSendBufferSpaceConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionDatagramSendBufferSpaceConstMeta => + const TaskConstMeta( + debugName: "connection_datagram_send_buffer_space", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, String?)> crateApiBridgeConnectionLocalIp({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 10, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionLocalIpConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionLocalIpConstMeta => + const TaskConstMeta( + debugName: "connection_local_ip", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, BigInt?)> crateApiBridgeConnectionMaxDatagramSize({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 11, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionMaxDatagramSizeConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionMaxDatagramSizeConstMeta => + const TaskConstMeta( + debugName: "connection_max_datagram_size", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, QuicSendStream, QuicRecvStream)> + crateApiBridgeConnectionOpenBi({required QuicConnection connection}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 12, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeConnectionOpenBiConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionOpenBiConstMeta => + const TaskConstMeta( + debugName: "connection_open_bi", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, QuicSendStream)> crateApiBridgeConnectionOpenUni({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 13, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeConnectionOpenUniConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionOpenUniConstMeta => + const TaskConstMeta( + debugName: "connection_open_uni", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, QuicPeerTransportParams)> + crateApiBridgeConnectionPeerTransportParams({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 14, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionPeerTransportParamsConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionPeerTransportParamsConstMeta => + const TaskConstMeta( + debugName: "connection_peer_transport_params", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, Uint8List?)> crateApiBridgeConnectionReadDatagram({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 15, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionReadDatagramConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionReadDatagramConstMeta => + const TaskConstMeta( + debugName: "connection_read_datagram", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, SocketAddress)> + crateApiBridgeConnectionRemoteAddress({required QuicConnection connection}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 16, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionRemoteAddressConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionRemoteAddressConstMeta => + const TaskConstMeta( + debugName: "connection_remote_address", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, BigInt)> crateApiBridgeConnectionRttMillis({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 17, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionRttMillisConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionRttMillisConstMeta => + const TaskConstMeta( + debugName: "connection_rtt_millis", + argNames: ["connection"], + ); + + @override + Future crateApiBridgeConnectionSendDatagram({ + required QuicConnection connection, + required List data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + sse_encode_list_prim_u_8_loose(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 18, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection, + decodeErrorData: sse_decode_quic_datagram_exception, + ), + constMeta: kCrateApiBridgeConnectionSendDatagramConstMeta, + argValues: [connection, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionSendDatagramConstMeta => + const TaskConstMeta( + debugName: "connection_send_datagram", + argNames: ["connection", "data"], + ); + + @override + Future crateApiBridgeConnectionSendDatagramWait({ + required QuicConnection connection, + required List data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + sse_encode_list_prim_u_8_loose(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 19, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection, + decodeErrorData: sse_decode_quic_datagram_exception, + ), + constMeta: kCrateApiBridgeConnectionSendDatagramWaitConstMeta, + argValues: [connection, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionSendDatagramWaitConstMeta => + const TaskConstMeta( + debugName: "connection_send_datagram_wait", + argNames: ["connection", "data"], + ); + + @override + Future<(QuicConnection, BigInt)> crateApiBridgeConnectionStableId({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 20, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionStableIdConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionStableIdConstMeta => + const TaskConstMeta( + debugName: "connection_stable_id", + argNames: ["connection"], + ); + + @override + Future<(QuicConnection, QuicConnectionStats)> crateApiBridgeConnectionStats({ + required QuicConnection connection, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + connection, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 21, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeConnectionStatsConstMeta, + argValues: [connection], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeConnectionStatsConstMeta => + const TaskConstMeta( + debugName: "connection_stats", + argNames: ["connection"], + ); + + @override + Future crateApiBridgeCreateClientEndpoint() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 22, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeCreateClientEndpointConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeCreateClientEndpointConstMeta => + const TaskConstMeta(debugName: "create_client_endpoint", argNames: []); + + @override + Future crateApiBridgeCreateServerEndpoint({ + required QuicServerConfig config, + required String addr, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + config, + serializer, + ); + sse_encode_String(addr, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 23, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeCreateServerEndpointConstMeta, + argValues: [config, addr], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeCreateServerEndpointConstMeta => + const TaskConstMeta( + debugName: "create_server_endpoint", + argNames: ["config", "addr"], + ); + + @override + Future crateApiBridgeEndpointConfigNew() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 24, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeEndpointConfigNewConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeEndpointConfigNewConstMeta => + const TaskConstMeta(debugName: "endpoint_config_new", argNames: []); + + @override + Future<(QuicEndpoint, QuicConnection)> crateApiBridgeEndpointConnect({ + required QuicEndpoint endpoint, + required String addr, + required String serverName, + String? qlogPath, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + endpoint, + serializer, + ); + sse_encode_String(addr, serializer); + sse_encode_String(serverName, serializer); + sse_encode_opt_String(qlogPath, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 25, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeEndpointConnectConstMeta, + argValues: [endpoint, addr, serverName, qlogPath], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeEndpointConnectConstMeta => + const TaskConstMeta( + debugName: "endpoint_connect", + argNames: ["endpoint", "addr", "serverName", "qlogPath"], + ); + + @override + Future crateApiBridgeInitApp() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 26, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeInitAppConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeInitAppConstMeta => + const TaskConstMeta(debugName: "init_app", argNames: []); + + @override + Future crateApiBridgeQuicClientClearPool({ + required QuicClient client, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 27, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeQuicClientClearPoolConstMeta, + argValues: [client], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientClearPoolConstMeta => + const TaskConstMeta( + debugName: "quic_client_clear_pool", + argNames: ["client"], + ); + + @override + Future<(QuicClient, QuicClientConfig)> crateApiBridgeQuicClientConfig({ + required QuicClient client, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 28, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeQuicClientConfigConstMeta, + argValues: [client], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientConfigConstMeta => + const TaskConstMeta( + debugName: "quic_client_config", + argNames: ["client"], + ); + + @override + Future crateApiBridgeQuicClientConfigNew() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 29, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_quic_client_config, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeQuicClientConfigNewConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientConfigNewConstMeta => + const TaskConstMeta(debugName: "quic_client_config_new", argNames: []); + + @override + Future crateApiBridgeQuicClientCreate() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 30, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientCreateConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientCreateConstMeta => + const TaskConstMeta(debugName: "quic_client_create", argNames: []); + + @override + Future crateApiBridgeQuicClientCreateWithConfig({ + required QuicClientConfig config, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_quic_client_config(config, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 31, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientCreateWithConfigConstMeta, + argValues: [config], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientCreateWithConfigConstMeta => + const TaskConstMeta( + debugName: "quic_client_create_with_config", + argNames: ["config"], + ); + + @override + Future<(QuicClient, String)> crateApiBridgeQuicClientGet({ + required QuicClient client, + required String url, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + sse_encode_String(url, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 32, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientGetConstMeta, + argValues: [client, url], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientGetConstMeta => + const TaskConstMeta( + debugName: "quic_client_get", + argNames: ["client", "url"], + ); + + @override + Future<(QuicClient, String)> crateApiBridgeQuicClientGetWithTimeout({ + required QuicClient client, + required String url, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + sse_encode_String(url, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 33, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientGetWithTimeoutConstMeta, + argValues: [client, url], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientGetWithTimeoutConstMeta => + const TaskConstMeta( + debugName: "quic_client_get_with_timeout", + argNames: ["client", "url"], + ); + + @override + Future<(QuicClient, String)> crateApiBridgeQuicClientPost({ + required QuicClient client, + required String url, + required String data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + sse_encode_String(url, serializer); + sse_encode_String(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 34, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientPostConstMeta, + argValues: [client, url, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientPostConstMeta => + const TaskConstMeta( + debugName: "quic_client_post", + argNames: ["client", "url", "data"], + ); + + @override + Future<(QuicClient, String)> crateApiBridgeQuicClientPostWithTimeout({ + required QuicClient client, + required String url, + required String data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + sse_encode_String(url, serializer); + sse_encode_String(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 35, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientPostWithTimeoutConstMeta, + argValues: [client, url, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientPostWithTimeoutConstMeta => + const TaskConstMeta( + debugName: "quic_client_post_with_timeout", + argNames: ["client", "url", "data"], + ); + + @override + Future<(QuicClient, String)> crateApiBridgeQuicClientSend({ + required QuicClient client, + required String url, + required String data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + sse_encode_String(url, serializer); + sse_encode_String(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 36, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientSendConstMeta, + argValues: [client, url, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientSendConstMeta => + const TaskConstMeta( + debugName: "quic_client_send", + argNames: ["client", "url", "data"], + ); + + @override + Future<(QuicClient, String)> crateApiBridgeQuicClientSendWithTimeout({ + required QuicClient client, + required String url, + required String data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + client, + serializer, + ); + sse_encode_String(url, serializer); + sse_encode_String(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 37, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string, + decodeErrorData: sse_decode_quic_error, + ), + constMeta: kCrateApiBridgeQuicClientSendWithTimeoutConstMeta, + argValues: [client, url, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeQuicClientSendWithTimeoutConstMeta => + const TaskConstMeta( + debugName: "quic_client_send_with_timeout", + argNames: ["client", "url", "data"], + ); + + @override + Future<(QuicRecvStream, Uint8List?)> crateApiBridgeRecvStreamRead({ + required QuicRecvStream stream, + required BigInt maxLength, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + stream, + serializer, + ); + sse_encode_usize(maxLength, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 38, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict, + decodeErrorData: sse_decode_quic_read_exception, + ), + constMeta: kCrateApiBridgeRecvStreamReadConstMeta, + argValues: [stream, maxLength], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeRecvStreamReadConstMeta => + const TaskConstMeta( + debugName: "recv_stream_read", + argNames: ["stream", "maxLength"], + ); + + @override + Future<(QuicRecvStream, Uint8List)> crateApiBridgeRecvStreamReadToEnd({ + required QuicRecvStream stream, + required BigInt maxLength, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + stream, + serializer, + ); + sse_encode_usize(maxLength, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 39, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict, + decodeErrorData: sse_decode_quic_read_to_end_exception, + ), + constMeta: kCrateApiBridgeRecvStreamReadToEndConstMeta, + argValues: [stream, maxLength], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeRecvStreamReadToEndConstMeta => + const TaskConstMeta( + debugName: "recv_stream_read_to_end", + argNames: ["stream", "maxLength"], + ); + + @override + Future crateApiBridgeSendStreamFinish({ + required QuicSendStream stream, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + stream, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 40, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream, + decodeErrorData: sse_decode_quic_write_exception, + ), + constMeta: kCrateApiBridgeSendStreamFinishConstMeta, + argValues: [stream], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeSendStreamFinishConstMeta => + const TaskConstMeta( + debugName: "send_stream_finish", + argNames: ["stream"], + ); + + @override + Future<(QuicSendStream, BigInt)> crateApiBridgeSendStreamWrite({ + required QuicSendStream stream, + required List data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + stream, + serializer, + ); + sse_encode_list_prim_u_8_loose(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 41, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize, + decodeErrorData: sse_decode_quic_write_exception, + ), + constMeta: kCrateApiBridgeSendStreamWriteConstMeta, + argValues: [stream, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeSendStreamWriteConstMeta => + const TaskConstMeta( + debugName: "send_stream_write", + argNames: ["stream", "data"], + ); + + @override + Future crateApiBridgeSendStreamWriteAll({ + required QuicSendStream stream, + required List data, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + stream, + serializer, + ); + sse_encode_list_prim_u_8_loose(data, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 42, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream, + decodeErrorData: sse_decode_quic_write_exception, + ), + constMeta: kCrateApiBridgeSendStreamWriteAllConstMeta, + argValues: [stream, data], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeSendStreamWriteAllConstMeta => + const TaskConstMeta( + debugName: "send_stream_write_all", + argNames: ["stream", "data"], + ); + + @override + Future crateApiBridgeServerConfigWithSingleCert({ + required List certChain, + required List key, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_list_prim_u_8_strict(certChain, serializer); + sse_encode_list_prim_u_8_loose(key, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 43, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiBridgeServerConfigWithSingleCertConstMeta, + argValues: [certChain, key], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeServerConfigWithSingleCertConstMeta => + const TaskConstMeta( + debugName: "server_config_with_single_cert", + argNames: ["certChain", "key"], + ); + + @override + Future crateApiBridgeTransportConfigNew() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 44, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig, + decodeErrorData: null, + ), + constMeta: kCrateApiBridgeTransportConfigNewConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBridgeTransportConfigNewConstMeta => + const TaskConstMeta(debugName: "transport_config_new", argNames: []); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicClient => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicClient => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicConnection => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicConnection => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicEndpoint => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicEndpoint => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicEndpointConfig => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicEndpointConfig => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicRecvStream => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicRecvStream => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicSendStream => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicSendStream => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicServerConfig => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicServerConfig => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_QuicTransportConfig => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_QuicTransportConfig => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig; + + @protected + QuicClient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicClientImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicConnection + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicConnectionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicEndpoint + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicEndpointImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicEndpointConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicEndpointConfigImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicRecvStream + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicRecvStreamImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicSendStream + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicSendStreamImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicServerConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicServerConfigImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicTransportConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicTransportConfigImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicConnection + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicConnectionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicClient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicClientImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicConnection + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicConnectionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicEndpoint + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicEndpointImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicEndpointConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicEndpointConfigImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicRecvStream + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicRecvStreamImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicSendStream + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicSendStreamImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicServerConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicServerConfigImpl.frbInternalDcoDecode(raw as List); + } + + @protected + QuicTransportConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return QuicTransportConfigImpl.frbInternalDcoDecode(raw as List); + } + + @protected + String dco_decode_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as String; + } + + @protected + QuicRecvStream + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + raw, + ); + } + + @protected + QuicSendStream + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + raw, + ); + } + + @protected + QuicClientConfig dco_decode_box_autoadd_quic_client_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_quic_client_config(raw); + } + + @protected + QuicReadException dco_decode_box_autoadd_quic_read_exception(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_quic_read_exception(raw); + } + + @protected + BigInt dco_decode_box_autoadd_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_usize(raw); + } + + @protected + List dco_decode_list_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_list_prim_u_8_strict).toList(); + } + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as List; + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + + @protected + QuicRecvStream? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + raw, + ); + } + + @protected + QuicSendStream? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + raw, + ); + } + + @protected + BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_usize(raw); + } + + @protected + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_list_prim_u_8_strict(raw); + } + + @protected + QuicClientConfig dco_decode_quic_client_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 6) + throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + return QuicClientConfig( + maxConnectionsPerHost: dco_decode_usize(arr[0]), + connectTimeoutMs: dco_decode_u_64(arr[1]), + requestTimeoutMs: dco_decode_u_64(arr[2]), + retryAttempts: dco_decode_u_32(arr[3]), + retryDelayMs: dco_decode_u_64(arr[4]), + keepAliveTimeoutMs: dco_decode_u_64(arr[5]), + ); + } + + @protected + QuicConnectionStats dco_decode_quic_connection_stats(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return QuicConnectionStats( + path: dco_decode_quic_path_stats(arr[0]), + frameTx: dco_decode_quic_frame_stats(arr[1]), + frameRx: dco_decode_quic_frame_stats(arr[2]), + udpTx: dco_decode_quic_udp_stats(arr[3]), + udpRx: dco_decode_quic_udp_stats(arr[4]), + ); + } + + @protected + QuicDatagramException dco_decode_quic_datagram_exception(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return QuicDatagramException_UnsupportedByPeer(); + case 1: + return QuicDatagramException_TooLarge( + maxSize: dco_decode_usize(raw[1]), + ); + case 2: + return QuicDatagramException_ConnectionLost(dco_decode_String(raw[1])); + default: + throw Exception("unreachable"); + } + } + + @protected + QuicError dco_decode_quic_error(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return QuicError_Connection(dco_decode_String(raw[1])); + case 1: + return QuicError_Endpoint(dco_decode_String(raw[1])); + case 2: + return QuicError_Stream(dco_decode_String(raw[1])); + case 3: + return QuicError_Tls(dco_decode_String(raw[1])); + case 4: + return QuicError_Config(dco_decode_String(raw[1])); + case 5: + return QuicError_Network(dco_decode_String(raw[1])); + case 6: + return QuicError_Write(dco_decode_String(raw[1])); + default: + throw Exception("unreachable"); + } + } + + @protected + QuicFrameStats dco_decode_quic_frame_stats(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 22) + throw Exception('unexpected arr length: expect 22 but see ${arr.length}'); + return QuicFrameStats( + acks: dco_decode_u_64(arr[0]), + crypto: dco_decode_u_64(arr[1]), + connectionClose: dco_decode_u_64(arr[2]), + dataBlocked: dco_decode_u_64(arr[3]), + datagram: dco_decode_u_64(arr[4]), + handshakeDone: dco_decode_u_64(arr[5]), + maxData: dco_decode_u_64(arr[6]), + maxStreamData: dco_decode_u_64(arr[7]), + maxStreamsBidi: dco_decode_u_64(arr[8]), + maxStreamsUni: dco_decode_u_64(arr[9]), + newConnectionId: dco_decode_u_64(arr[10]), + newToken: dco_decode_u_64(arr[11]), + pathChallenge: dco_decode_u_64(arr[12]), + pathResponse: dco_decode_u_64(arr[13]), + ping: dco_decode_u_64(arr[14]), + resetStream: dco_decode_u_64(arr[15]), + retireConnectionId: dco_decode_u_64(arr[16]), + stream: dco_decode_u_64(arr[17]), + streamDataBlocked: dco_decode_u_64(arr[18]), + streamsBlockedBidi: dco_decode_u_64(arr[19]), + streamsBlockedUni: dco_decode_u_64(arr[20]), + stopSending: dco_decode_u_64(arr[21]), + ); + } + + @protected + QuicPathStats dco_decode_quic_path_stats(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 6) + throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + return QuicPathStats( + rttMillis: dco_decode_u_64(arr[0]), + cwnd: dco_decode_u_64(arr[1]), + lostPackets: dco_decode_u_64(arr[2]), + lostBytes: dco_decode_u_64(arr[3]), + sentPackets: dco_decode_u_64(arr[4]), + congestionEvents: dco_decode_u_64(arr[5]), + ); + } + + @protected + QuicPeerTransportParams dco_decode_quic_peer_transport_params(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return QuicPeerTransportParams( + initialMaxStreamsBidi: dco_decode_u_64(arr[0]), + initialMaxStreamsUni: dco_decode_u_64(arr[1]), + initialMaxData: dco_decode_u_64(arr[2]), + ); + } + + @protected + QuicReadException dco_decode_quic_read_exception(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return QuicReadException_Reset(dco_decode_u_64(raw[1])); + case 1: + return QuicReadException_ConnectionLost(dco_decode_String(raw[1])); + case 2: + return QuicReadException_ZeroRttRejected(); + case 3: + return QuicReadException_ClosedStream(); + case 4: + return QuicReadException_IllegalOrderedRead(); + default: + throw Exception("unreachable"); + } + } + + @protected + QuicReadToEndException dco_decode_quic_read_to_end_exception(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return QuicReadToEndException_Read( + dco_decode_box_autoadd_quic_read_exception(raw[1]), + ); + case 1: + return QuicReadToEndException_TooLong(); + default: + throw Exception("unreachable"); + } + } + + @protected + QuicUdpStats dco_decode_quic_udp_stats(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return QuicUdpStats( + datagrams: dco_decode_u_64(arr[0]), + bytes: dco_decode_u_64(arr[1]), + ios: dco_decode_u_64(arr[2]), + ); + } + + @protected + QuicWriteException dco_decode_quic_write_exception(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return QuicWriteException_Stopped(dco_decode_u_64(raw[1])); + case 1: + return QuicWriteException_ConnectionLost(dco_decode_String(raw[1])); + default: + throw Exception("unreachable"); + } + } + + @protected + (QuicClient, QuicClientConfig) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + arr[0], + ), + dco_decode_quic_client_config(arr[1]), + ); + } + + @protected + (QuicClient, String) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + arr[0], + ), + dco_decode_String(arr[1]), + ); + } + + @protected + (QuicConnection, QuicSendStream) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + arr[1], + ), + ); + } + + @protected + (QuicConnection, QuicSendStream, QuicRecvStream) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) { + throw Exception('Expected 3 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + arr[1], + ), + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + arr[2], + ), + ); + } + + @protected + (QuicConnection, BigInt?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_opt_box_autoadd_usize(arr[1]), + ); + } + + @protected + (QuicConnection, Uint8List?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_opt_list_prim_u_8_strict(arr[1]), + ); + } + + @protected + (QuicConnection, String?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_opt_String(arr[1]), + ); + } + + @protected + (QuicConnection, QuicConnectionStats) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_quic_connection_stats(arr[1]), + ); + } + + @protected + (QuicConnection, QuicPeerTransportParams) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_quic_peer_transport_params(arr[1]), + ); + } + + @protected + (QuicConnection, SocketAddress) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_socket_address(arr[1]), + ); + } + + @protected + (QuicConnection, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_u_64(arr[1]), + ); + } + + @protected + (QuicConnection, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[0], + ), + dco_decode_usize(arr[1]), + ); + } + + @protected + (QuicEndpoint, QuicConnection) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + arr[0], + ), + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + arr[1], + ), + ); + } + + @protected + (QuicRecvStream, Uint8List) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + arr[0], + ), + dco_decode_list_prim_u_8_strict(arr[1]), + ); + } + + @protected + (QuicRecvStream, Uint8List?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + arr[0], + ), + dco_decode_opt_list_prim_u_8_strict(arr[1]), + ); + } + + @protected + (QuicSendStream, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + arr[0], + ), + dco_decode_usize(arr[1]), + ); + } + + @protected + (QuicSendStream?, QuicRecvStream?) + dco_decode_record_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + arr[0], + ), + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + arr[1], + ), + ); + } + + @protected + SocketAddress dco_decode_socket_address(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return SocketAddress( + ip: dco_decode_String(arr[0]), + port: dco_decode_u_16(arr[1]), + ); + } + + @protected + int dco_decode_u_16(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + int dco_decode_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + BigInt dco_decode_u_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + QuicClient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicClientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicConnection + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicConnectionImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicEndpoint + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicEndpointImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicEndpointConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicEndpointConfigImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicRecvStream + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicRecvStreamImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicSendStream + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicSendStreamImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicServerConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicServerConfigImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicTransportConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicTransportConfigImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicConnection + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicConnectionImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicClient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicClientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicConnection + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicConnectionImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicEndpoint + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicEndpointImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicEndpointConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicEndpointConfigImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicRecvStream + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicRecvStreamImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicSendStream + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicSendStreamImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicServerConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicServerConfigImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + QuicTransportConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return QuicTransportConfigImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + QuicRecvStream + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + deserializer, + )); + } + + @protected + QuicSendStream + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + deserializer, + )); + } + + @protected + QuicClientConfig sse_decode_box_autoadd_quic_client_config( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_quic_client_config(deserializer)); + } + + @protected + QuicReadException sse_decode_box_autoadd_quic_read_exception( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_quic_read_exception(deserializer)); + } + + @protected + BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_usize(deserializer)); + } + + @protected + List sse_decode_list_list_prim_u_8_strict( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_list_prim_u_8_strict(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_String(deserializer)); + } else { + return null; + } + } + + @protected + QuicRecvStream? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + deserializer, + )); + } else { + return null; + } + } + + @protected + QuicSendStream? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + deserializer, + )); + } else { + return null; + } + } + + @protected + BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_usize(deserializer)); + } else { + return null; + } + } + + @protected + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_prim_u_8_strict(deserializer)); + } else { + return null; + } + } + + @protected + QuicClientConfig sse_decode_quic_client_config(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_maxConnectionsPerHost = sse_decode_usize(deserializer); + var var_connectTimeoutMs = sse_decode_u_64(deserializer); + var var_requestTimeoutMs = sse_decode_u_64(deserializer); + var var_retryAttempts = sse_decode_u_32(deserializer); + var var_retryDelayMs = sse_decode_u_64(deserializer); + var var_keepAliveTimeoutMs = sse_decode_u_64(deserializer); + return QuicClientConfig( + maxConnectionsPerHost: var_maxConnectionsPerHost, + connectTimeoutMs: var_connectTimeoutMs, + requestTimeoutMs: var_requestTimeoutMs, + retryAttempts: var_retryAttempts, + retryDelayMs: var_retryDelayMs, + keepAliveTimeoutMs: var_keepAliveTimeoutMs, + ); + } + + @protected + QuicConnectionStats sse_decode_quic_connection_stats( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_path = sse_decode_quic_path_stats(deserializer); + var var_frameTx = sse_decode_quic_frame_stats(deserializer); + var var_frameRx = sse_decode_quic_frame_stats(deserializer); + var var_udpTx = sse_decode_quic_udp_stats(deserializer); + var var_udpRx = sse_decode_quic_udp_stats(deserializer); + return QuicConnectionStats( + path: var_path, + frameTx: var_frameTx, + frameRx: var_frameRx, + udpTx: var_udpTx, + udpRx: var_udpRx, + ); + } + + @protected + QuicDatagramException sse_decode_quic_datagram_exception( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + return QuicDatagramException_UnsupportedByPeer(); + case 1: + var var_maxSize = sse_decode_usize(deserializer); + return QuicDatagramException_TooLarge(maxSize: var_maxSize); + case 2: + var var_field0 = sse_decode_String(deserializer); + return QuicDatagramException_ConnectionLost(var_field0); + default: + throw UnimplementedError(''); + } + } + + @protected + QuicError sse_decode_quic_error(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Connection(var_field0); + case 1: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Endpoint(var_field0); + case 2: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Stream(var_field0); + case 3: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Tls(var_field0); + case 4: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Config(var_field0); + case 5: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Network(var_field0); + case 6: + var var_field0 = sse_decode_String(deserializer); + return QuicError_Write(var_field0); + default: + throw UnimplementedError(''); + } + } + + @protected + QuicFrameStats sse_decode_quic_frame_stats(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_acks = sse_decode_u_64(deserializer); + var var_crypto = sse_decode_u_64(deserializer); + var var_connectionClose = sse_decode_u_64(deserializer); + var var_dataBlocked = sse_decode_u_64(deserializer); + var var_datagram = sse_decode_u_64(deserializer); + var var_handshakeDone = sse_decode_u_64(deserializer); + var var_maxData = sse_decode_u_64(deserializer); + var var_maxStreamData = sse_decode_u_64(deserializer); + var var_maxStreamsBidi = sse_decode_u_64(deserializer); + var var_maxStreamsUni = sse_decode_u_64(deserializer); + var var_newConnectionId = sse_decode_u_64(deserializer); + var var_newToken = sse_decode_u_64(deserializer); + var var_pathChallenge = sse_decode_u_64(deserializer); + var var_pathResponse = sse_decode_u_64(deserializer); + var var_ping = sse_decode_u_64(deserializer); + var var_resetStream = sse_decode_u_64(deserializer); + var var_retireConnectionId = sse_decode_u_64(deserializer); + var var_stream = sse_decode_u_64(deserializer); + var var_streamDataBlocked = sse_decode_u_64(deserializer); + var var_streamsBlockedBidi = sse_decode_u_64(deserializer); + var var_streamsBlockedUni = sse_decode_u_64(deserializer); + var var_stopSending = sse_decode_u_64(deserializer); + return QuicFrameStats( + acks: var_acks, + crypto: var_crypto, + connectionClose: var_connectionClose, + dataBlocked: var_dataBlocked, + datagram: var_datagram, + handshakeDone: var_handshakeDone, + maxData: var_maxData, + maxStreamData: var_maxStreamData, + maxStreamsBidi: var_maxStreamsBidi, + maxStreamsUni: var_maxStreamsUni, + newConnectionId: var_newConnectionId, + newToken: var_newToken, + pathChallenge: var_pathChallenge, + pathResponse: var_pathResponse, + ping: var_ping, + resetStream: var_resetStream, + retireConnectionId: var_retireConnectionId, + stream: var_stream, + streamDataBlocked: var_streamDataBlocked, + streamsBlockedBidi: var_streamsBlockedBidi, + streamsBlockedUni: var_streamsBlockedUni, + stopSending: var_stopSending, + ); + } + + @protected + QuicPathStats sse_decode_quic_path_stats(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_rttMillis = sse_decode_u_64(deserializer); + var var_cwnd = sse_decode_u_64(deserializer); + var var_lostPackets = sse_decode_u_64(deserializer); + var var_lostBytes = sse_decode_u_64(deserializer); + var var_sentPackets = sse_decode_u_64(deserializer); + var var_congestionEvents = sse_decode_u_64(deserializer); + return QuicPathStats( + rttMillis: var_rttMillis, + cwnd: var_cwnd, + lostPackets: var_lostPackets, + lostBytes: var_lostBytes, + sentPackets: var_sentPackets, + congestionEvents: var_congestionEvents, + ); + } + + @protected + QuicPeerTransportParams sse_decode_quic_peer_transport_params( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_initialMaxStreamsBidi = sse_decode_u_64(deserializer); + var var_initialMaxStreamsUni = sse_decode_u_64(deserializer); + var var_initialMaxData = sse_decode_u_64(deserializer); + return QuicPeerTransportParams( + initialMaxStreamsBidi: var_initialMaxStreamsBidi, + initialMaxStreamsUni: var_initialMaxStreamsUni, + initialMaxData: var_initialMaxData, + ); + } + + @protected + QuicReadException sse_decode_quic_read_exception( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_field0 = sse_decode_u_64(deserializer); + return QuicReadException_Reset(var_field0); + case 1: + var var_field0 = sse_decode_String(deserializer); + return QuicReadException_ConnectionLost(var_field0); + case 2: + return QuicReadException_ZeroRttRejected(); + case 3: + return QuicReadException_ClosedStream(); + case 4: + return QuicReadException_IllegalOrderedRead(); + default: + throw UnimplementedError(''); + } + } + + @protected + QuicReadToEndException sse_decode_quic_read_to_end_exception( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_field0 = sse_decode_box_autoadd_quic_read_exception( + deserializer, + ); + return QuicReadToEndException_Read(var_field0); + case 1: + return QuicReadToEndException_TooLong(); + default: + throw UnimplementedError(''); + } + } + + @protected + QuicUdpStats sse_decode_quic_udp_stats(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_datagrams = sse_decode_u_64(deserializer); + var var_bytes = sse_decode_u_64(deserializer); + var var_ios = sse_decode_u_64(deserializer); + return QuicUdpStats( + datagrams: var_datagrams, + bytes: var_bytes, + ios: var_ios, + ); + } + + @protected + QuicWriteException sse_decode_quic_write_exception( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_field0 = sse_decode_u_64(deserializer); + return QuicWriteException_Stopped(var_field0); + case 1: + var var_field0 = sse_decode_String(deserializer); + return QuicWriteException_ConnectionLost(var_field0); + default: + throw UnimplementedError(''); + } + } + + @protected + (QuicClient, QuicClientConfig) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + deserializer, + ); + var var_field1 = sse_decode_quic_client_config(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicClient, String) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + deserializer, + ); + var var_field1 = sse_decode_String(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, QuicSendStream) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + deserializer, + ); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, QuicSendStream, QuicRecvStream) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + deserializer, + ); + var var_field2 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + deserializer, + ); + return (var_field0, var_field1, var_field2); + } + + @protected + (QuicConnection, BigInt?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_opt_box_autoadd_usize(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, Uint8List?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_opt_list_prim_u_8_strict(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, String?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_opt_String(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, QuicConnectionStats) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_quic_connection_stats(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, QuicPeerTransportParams) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_quic_peer_transport_params(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, SocketAddress) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_socket_address(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_u_64(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicConnection, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + var var_field1 = sse_decode_usize(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicEndpoint, QuicConnection) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + deserializer, + ); + var var_field1 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + deserializer, + ); + return (var_field0, var_field1); + } + + @protected + (QuicRecvStream, Uint8List) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + deserializer, + ); + var var_field1 = sse_decode_list_prim_u_8_strict(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicRecvStream, Uint8List?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + deserializer, + ); + var var_field1 = sse_decode_opt_list_prim_u_8_strict(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicSendStream, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + deserializer, + ); + var var_field1 = sse_decode_usize(deserializer); + return (var_field0, var_field1); + } + + @protected + (QuicSendStream?, QuicRecvStream?) + sse_decode_record_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + deserializer, + ); + var var_field1 = + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + deserializer, + ); + return (var_field0, var_field1); + } + + @protected + SocketAddress sse_decode_socket_address(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_ip = sse_decode_String(deserializer); + var var_port = sse_decode_u_16(deserializer); + return SocketAddress(ip: var_ip, port: var_port); + } + + @protected + int sse_decode_u_16(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint16(); + } + + @protected + int sse_decode_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint32(); + } + + @protected + BigInt sse_decode_u_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + int sse_decode_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8(); + } + + @protected + void sse_decode_unit(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + + @protected + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + QuicClient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicClientImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicConnectionImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + QuicEndpoint self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicEndpointImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + QuicEndpointConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicEndpointConfigImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicRecvStreamImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicSendStreamImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + QuicServerConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicServerConfigImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + QuicTransportConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicTransportConfigImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicConnectionImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + QuicClient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicClientImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicConnectionImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + QuicEndpoint self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicEndpointImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + QuicEndpointConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicEndpointConfigImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicRecvStreamImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicSendStreamImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + QuicServerConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicServerConfigImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + QuicTransportConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as QuicTransportConfigImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + self, + serializer, + ); + } + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + self, + serializer, + ); + } + + @protected + void sse_encode_box_autoadd_quic_client_config( + QuicClientConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_quic_client_config(self, serializer); + } + + @protected + void sse_encode_box_autoadd_quic_read_exception( + QuicReadException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_quic_read_exception(self, serializer); + } + + @protected + void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize(self, serializer); + } + + @protected + void sse_encode_list_list_prim_u_8_strict( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_list_prim_u_8_strict(item, serializer); + } + } + + @protected + void sse_encode_list_prim_u_8_loose( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List( + self is Uint8List ? self : Uint8List.fromList(self), + ); + } + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List(self); + } + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_String(self, serializer); + } + } + + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + self, + serializer, + ); + } + } + + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + self, + serializer, + ); + } + } + + @protected + void sse_encode_opt_box_autoadd_usize( + BigInt? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_usize(self, serializer); + } + } + + @protected + void sse_encode_opt_list_prim_u_8_strict( + Uint8List? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_list_prim_u_8_strict(self, serializer); + } + } + + @protected + void sse_encode_quic_client_config( + QuicClientConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize(self.maxConnectionsPerHost, serializer); + sse_encode_u_64(self.connectTimeoutMs, serializer); + sse_encode_u_64(self.requestTimeoutMs, serializer); + sse_encode_u_32(self.retryAttempts, serializer); + sse_encode_u_64(self.retryDelayMs, serializer); + sse_encode_u_64(self.keepAliveTimeoutMs, serializer); + } + + @protected + void sse_encode_quic_connection_stats( + QuicConnectionStats self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_quic_path_stats(self.path, serializer); + sse_encode_quic_frame_stats(self.frameTx, serializer); + sse_encode_quic_frame_stats(self.frameRx, serializer); + sse_encode_quic_udp_stats(self.udpTx, serializer); + sse_encode_quic_udp_stats(self.udpRx, serializer); + } + + @protected + void sse_encode_quic_datagram_exception( + QuicDatagramException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case QuicDatagramException_UnsupportedByPeer(): + sse_encode_i_32(0, serializer); + case QuicDatagramException_TooLarge(maxSize: final maxSize): + sse_encode_i_32(1, serializer); + sse_encode_usize(maxSize, serializer); + case QuicDatagramException_ConnectionLost(field0: final field0): + sse_encode_i_32(2, serializer); + sse_encode_String(field0, serializer); + } + } + + @protected + void sse_encode_quic_error(QuicError self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case QuicError_Connection(field0: final field0): + sse_encode_i_32(0, serializer); + sse_encode_String(field0, serializer); + case QuicError_Endpoint(field0: final field0): + sse_encode_i_32(1, serializer); + sse_encode_String(field0, serializer); + case QuicError_Stream(field0: final field0): + sse_encode_i_32(2, serializer); + sse_encode_String(field0, serializer); + case QuicError_Tls(field0: final field0): + sse_encode_i_32(3, serializer); + sse_encode_String(field0, serializer); + case QuicError_Config(field0: final field0): + sse_encode_i_32(4, serializer); + sse_encode_String(field0, serializer); + case QuicError_Network(field0: final field0): + sse_encode_i_32(5, serializer); + sse_encode_String(field0, serializer); + case QuicError_Write(field0: final field0): + sse_encode_i_32(6, serializer); + sse_encode_String(field0, serializer); + } + } + + @protected + void sse_encode_quic_frame_stats( + QuicFrameStats self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self.acks, serializer); + sse_encode_u_64(self.crypto, serializer); + sse_encode_u_64(self.connectionClose, serializer); + sse_encode_u_64(self.dataBlocked, serializer); + sse_encode_u_64(self.datagram, serializer); + sse_encode_u_64(self.handshakeDone, serializer); + sse_encode_u_64(self.maxData, serializer); + sse_encode_u_64(self.maxStreamData, serializer); + sse_encode_u_64(self.maxStreamsBidi, serializer); + sse_encode_u_64(self.maxStreamsUni, serializer); + sse_encode_u_64(self.newConnectionId, serializer); + sse_encode_u_64(self.newToken, serializer); + sse_encode_u_64(self.pathChallenge, serializer); + sse_encode_u_64(self.pathResponse, serializer); + sse_encode_u_64(self.ping, serializer); + sse_encode_u_64(self.resetStream, serializer); + sse_encode_u_64(self.retireConnectionId, serializer); + sse_encode_u_64(self.stream, serializer); + sse_encode_u_64(self.streamDataBlocked, serializer); + sse_encode_u_64(self.streamsBlockedBidi, serializer); + sse_encode_u_64(self.streamsBlockedUni, serializer); + sse_encode_u_64(self.stopSending, serializer); + } + + @protected + void sse_encode_quic_path_stats( + QuicPathStats self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self.rttMillis, serializer); + sse_encode_u_64(self.cwnd, serializer); + sse_encode_u_64(self.lostPackets, serializer); + sse_encode_u_64(self.lostBytes, serializer); + sse_encode_u_64(self.sentPackets, serializer); + sse_encode_u_64(self.congestionEvents, serializer); + } + + @protected + void sse_encode_quic_peer_transport_params( + QuicPeerTransportParams self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self.initialMaxStreamsBidi, serializer); + sse_encode_u_64(self.initialMaxStreamsUni, serializer); + sse_encode_u_64(self.initialMaxData, serializer); + } + + @protected + void sse_encode_quic_read_exception( + QuicReadException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case QuicReadException_Reset(field0: final field0): + sse_encode_i_32(0, serializer); + sse_encode_u_64(field0, serializer); + case QuicReadException_ConnectionLost(field0: final field0): + sse_encode_i_32(1, serializer); + sse_encode_String(field0, serializer); + case QuicReadException_ZeroRttRejected(): + sse_encode_i_32(2, serializer); + case QuicReadException_ClosedStream(): + sse_encode_i_32(3, serializer); + case QuicReadException_IllegalOrderedRead(): + sse_encode_i_32(4, serializer); + } + } + + @protected + void sse_encode_quic_read_to_end_exception( + QuicReadToEndException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case QuicReadToEndException_Read(field0: final field0): + sse_encode_i_32(0, serializer); + sse_encode_box_autoadd_quic_read_exception(field0, serializer); + case QuicReadToEndException_TooLong(): + sse_encode_i_32(1, serializer); + } + } + + @protected + void sse_encode_quic_udp_stats(QuicUdpStats self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self.datagrams, serializer); + sse_encode_u_64(self.bytes, serializer); + sse_encode_u_64(self.ios, serializer); + } + + @protected + void sse_encode_quic_write_exception( + QuicWriteException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case QuicWriteException_Stopped(field0: final field0): + sse_encode_i_32(0, serializer); + sse_encode_u_64(field0, serializer); + case QuicWriteException_ConnectionLost(field0: final field0): + sse_encode_i_32(1, serializer); + sse_encode_String(field0, serializer); + } + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + (QuicClient, QuicClientConfig) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + self.$1, + serializer, + ); + sse_encode_quic_client_config(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + (QuicClient, String) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + self.$1, + serializer, + ); + sse_encode_String(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + (QuicConnection, QuicSendStream) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + self.$2, + serializer, + ); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + (QuicConnection, QuicSendStream, QuicRecvStream) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + self.$2, + serializer, + ); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + self.$3, + serializer, + ); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + (QuicConnection, BigInt?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_opt_box_autoadd_usize(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + (QuicConnection, Uint8List?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + (QuicConnection, String?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_opt_String(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + (QuicConnection, QuicConnectionStats) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_quic_connection_stats(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + (QuicConnection, QuicPeerTransportParams) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_quic_peer_transport_params(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + (QuicConnection, SocketAddress) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_socket_address(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + (QuicConnection, BigInt) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_u_64(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + (QuicConnection, BigInt) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$1, + serializer, + ); + sse_encode_usize(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + (QuicEndpoint, QuicConnection) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + self.$1, + serializer, + ); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + self.$2, + serializer, + ); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + (QuicRecvStream, Uint8List) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + self.$1, + serializer, + ); + sse_encode_list_prim_u_8_strict(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + (QuicRecvStream, Uint8List?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + self.$1, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(self.$2, serializer); + } + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + (QuicSendStream, BigInt) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + self.$1, + serializer, + ); + sse_encode_usize(self.$2, serializer); + } + + @protected + void + sse_encode_record_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_opt_box_autoadd_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + (QuicSendStream?, QuicRecvStream?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + self.$1, + serializer, + ); + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + self.$2, + serializer, + ); + } + + @protected + void sse_encode_socket_address(SocketAddress self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.ip, serializer); + sse_encode_u_16(self.port, serializer); + } + + @protected + void sse_encode_u_16(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint16(self); + } + + @protected + void sse_encode_u_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint32(self); + } + + @protected + void sse_encode_u_64(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } + + @protected + void sse_encode_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self); + } + + @protected + void sse_encode_unit(void self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } + + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } +} + +@sealed +class QuicClientImpl extends RustOpaque implements QuicClient { + // Not to be used by end users + QuicClientImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicClientImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicClient, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicClient, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicClientPtr, + ); +} + +@sealed +class QuicConnectionImpl extends RustOpaque implements QuicConnection { + // Not to be used by end users + QuicConnectionImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicConnectionImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicConnection, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicConnection, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicConnectionPtr, + ); +} + +@sealed +class QuicEndpointConfigImpl extends RustOpaque implements QuicEndpointConfig { + // Not to be used by end users + QuicEndpointConfigImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicEndpointConfigImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicEndpointConfig, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicEndpointConfig, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_QuicEndpointConfigPtr, + ); +} + +@sealed +class QuicEndpointImpl extends RustOpaque implements QuicEndpoint { + // Not to be used by end users + QuicEndpointImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicEndpointImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicEndpoint, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicEndpoint, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicEndpointPtr, + ); +} + +@sealed +class QuicRecvStreamImpl extends RustOpaque implements QuicRecvStream { + // Not to be used by end users + QuicRecvStreamImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicRecvStreamImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicRecvStream, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicRecvStream, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicRecvStreamPtr, + ); +} + +@sealed +class QuicSendStreamImpl extends RustOpaque implements QuicSendStream { + // Not to be used by end users + QuicSendStreamImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicSendStreamImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicSendStream, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicSendStream, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicSendStreamPtr, + ); +} + +@sealed +class QuicServerConfigImpl extends RustOpaque implements QuicServerConfig { + // Not to be used by end users + QuicServerConfigImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicServerConfigImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_QuicServerConfig, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_QuicServerConfig, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_QuicServerConfigPtr, + ); +} + +@sealed +class QuicTransportConfigImpl extends RustOpaque + implements QuicTransportConfig { + // Not to be used by end users + QuicTransportConfigImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + QuicTransportConfigImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: RustLib + .instance + .api + .rust_arc_increment_strong_count_QuicTransportConfig, + rustArcDecrementStrongCount: RustLib + .instance + .api + .rust_arc_decrement_strong_count_QuicTransportConfig, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_QuicTransportConfigPtr, + ); +} diff --git a/vendor/flutter_quic/lib/src/rust/frb_generated.io.dart b/vendor/flutter_quic/lib/src/rust/frb_generated.io.dart new file mode 100644 index 0000000..ceb1a28 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/frb_generated.io.dart @@ -0,0 +1,1275 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api/bridge.dart'; +import 'convenience/client.dart'; +import 'core/config.dart'; +import 'core/connection.dart'; +import 'core/endpoint.dart'; +import 'core/stream.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'errors.dart'; +import 'frb_generated.dart'; +import 'models/types.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicClientPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClientPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicConnectionPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnectionPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicEndpointPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicEndpointConfigPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfigPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicRecvStreamPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStreamPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicSendStreamPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStreamPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicServerConfigPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfigPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicTransportConfigPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfigPtr; + + @protected + QuicClient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + dynamic raw, + ); + + @protected + QuicConnection + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ); + + @protected + QuicEndpoint + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + dynamic raw, + ); + + @protected + QuicEndpointConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + dynamic raw, + ); + + @protected + QuicRecvStream + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ); + + @protected + QuicSendStream + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ); + + @protected + QuicServerConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + dynamic raw, + ); + + @protected + QuicTransportConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + dynamic raw, + ); + + @protected + QuicClient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + dynamic raw, + ); + + @protected + QuicConnection + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ); + + @protected + QuicEndpoint + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + dynamic raw, + ); + + @protected + QuicEndpointConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + dynamic raw, + ); + + @protected + QuicRecvStream + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ); + + @protected + QuicSendStream + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ); + + @protected + QuicServerConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + dynamic raw, + ); + + @protected + QuicTransportConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + QuicClientConfig dco_decode_box_autoadd_quic_client_config(dynamic raw); + + @protected + QuicReadException dco_decode_box_autoadd_quic_read_exception(dynamic raw); + + @protected + BigInt dco_decode_box_autoadd_usize(dynamic raw); + + @protected + List dco_decode_list_list_prim_u_8_strict(dynamic raw); + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); + + @protected + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + + @protected + QuicClientConfig dco_decode_quic_client_config(dynamic raw); + + @protected + QuicConnectionStats dco_decode_quic_connection_stats(dynamic raw); + + @protected + QuicDatagramException dco_decode_quic_datagram_exception(dynamic raw); + + @protected + QuicError dco_decode_quic_error(dynamic raw); + + @protected + QuicFrameStats dco_decode_quic_frame_stats(dynamic raw); + + @protected + QuicPathStats dco_decode_quic_path_stats(dynamic raw); + + @protected + QuicPeerTransportParams dco_decode_quic_peer_transport_params(dynamic raw); + + @protected + QuicReadException dco_decode_quic_read_exception(dynamic raw); + + @protected + QuicReadToEndException dco_decode_quic_read_to_end_exception(dynamic raw); + + @protected + QuicUdpStats dco_decode_quic_udp_stats(dynamic raw); + + @protected + QuicWriteException dco_decode_quic_write_exception(dynamic raw); + + @protected + (QuicClient, QuicClientConfig) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + dynamic raw, + ); + + @protected + (QuicClient, String) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + dynamic raw, + ); + + @protected + (QuicConnection, QuicSendStream) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + dynamic raw, + ); + + @protected + (QuicConnection, QuicSendStream, QuicRecvStream) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + dynamic raw, + ); + + @protected + (QuicConnection, BigInt?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + dynamic raw, + ); + + @protected + (QuicConnection, Uint8List?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (QuicConnection, String?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + dynamic raw, + ); + + @protected + (QuicConnection, QuicConnectionStats) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + dynamic raw, + ); + + @protected + (QuicConnection, QuicPeerTransportParams) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + dynamic raw, + ); + + @protected + (QuicConnection, SocketAddress) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + dynamic raw, + ); + + @protected + (QuicConnection, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + dynamic raw, + ); + + @protected + (QuicConnection, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + dynamic raw, + ); + + @protected + (QuicEndpoint, QuicConnection) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + dynamic raw, + ); + + @protected + (QuicRecvStream, Uint8List) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (QuicRecvStream, Uint8List?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (QuicSendStream, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + dynamic raw, + ); + + @protected + SocketAddress dco_decode_socket_address(dynamic raw); + + @protected + int dco_decode_u_16(dynamic raw); + + @protected + int dco_decode_u_32(dynamic raw); + + @protected + BigInt dco_decode_u_64(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + QuicClient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + SseDeserializer deserializer, + ); + + @protected + QuicConnection + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ); + + @protected + QuicEndpoint + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + SseDeserializer deserializer, + ); + + @protected + QuicEndpointConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + SseDeserializer deserializer, + ); + + @protected + QuicRecvStream + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ); + + @protected + QuicSendStream + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ); + + @protected + QuicServerConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + SseDeserializer deserializer, + ); + + @protected + QuicTransportConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + SseDeserializer deserializer, + ); + + @protected + QuicClient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + SseDeserializer deserializer, + ); + + @protected + QuicConnection + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ); + + @protected + QuicEndpoint + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + SseDeserializer deserializer, + ); + + @protected + QuicEndpointConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + SseDeserializer deserializer, + ); + + @protected + QuicRecvStream + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ); + + @protected + QuicSendStream + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ); + + @protected + QuicServerConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + SseDeserializer deserializer, + ); + + @protected + QuicTransportConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + QuicClientConfig sse_decode_box_autoadd_quic_client_config( + SseDeserializer deserializer, + ); + + @protected + QuicReadException sse_decode_box_autoadd_quic_read_exception( + SseDeserializer deserializer, + ); + + @protected + BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); + + @protected + List sse_decode_list_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); + + @protected + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + QuicClientConfig sse_decode_quic_client_config(SseDeserializer deserializer); + + @protected + QuicConnectionStats sse_decode_quic_connection_stats( + SseDeserializer deserializer, + ); + + @protected + QuicDatagramException sse_decode_quic_datagram_exception( + SseDeserializer deserializer, + ); + + @protected + QuicError sse_decode_quic_error(SseDeserializer deserializer); + + @protected + QuicFrameStats sse_decode_quic_frame_stats(SseDeserializer deserializer); + + @protected + QuicPathStats sse_decode_quic_path_stats(SseDeserializer deserializer); + + @protected + QuicPeerTransportParams sse_decode_quic_peer_transport_params( + SseDeserializer deserializer, + ); + + @protected + QuicReadException sse_decode_quic_read_exception( + SseDeserializer deserializer, + ); + + @protected + QuicReadToEndException sse_decode_quic_read_to_end_exception( + SseDeserializer deserializer, + ); + + @protected + QuicUdpStats sse_decode_quic_udp_stats(SseDeserializer deserializer); + + @protected + QuicWriteException sse_decode_quic_write_exception( + SseDeserializer deserializer, + ); + + @protected + (QuicClient, QuicClientConfig) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + SseDeserializer deserializer, + ); + + @protected + (QuicClient, String) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicSendStream) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicSendStream, QuicRecvStream) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, BigInt?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, Uint8List?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, String?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicConnectionStats) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicPeerTransportParams) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, SocketAddress) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + SseDeserializer deserializer, + ); + + @protected + (QuicEndpoint, QuicConnection) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + SseDeserializer deserializer, + ); + + @protected + (QuicRecvStream, Uint8List) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (QuicRecvStream, Uint8List?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (QuicSendStream, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + SseDeserializer deserializer, + ); + + @protected + SocketAddress sse_decode_socket_address(SseDeserializer deserializer); + + @protected + int sse_decode_u_16(SseDeserializer deserializer); + + @protected + int sse_decode_u_32(SseDeserializer deserializer); + + @protected + BigInt sse_decode_u_64(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + QuicClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + QuicEndpoint self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + QuicEndpointConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + QuicServerConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + QuicTransportConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + QuicClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + QuicEndpoint self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + QuicEndpointConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + QuicServerConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + QuicTransportConfig self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_quic_client_config( + QuicClientConfig self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_quic_read_exception( + QuicReadException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_list_list_prim_u_8_strict( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); + + @protected + void sse_encode_opt_list_prim_u_8_strict( + Uint8List? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_client_config( + QuicClientConfig self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_connection_stats( + QuicConnectionStats self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_datagram_exception( + QuicDatagramException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_error(QuicError self, SseSerializer serializer); + + @protected + void sse_encode_quic_frame_stats( + QuicFrameStats self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_path_stats(QuicPathStats self, SseSerializer serializer); + + @protected + void sse_encode_quic_peer_transport_params( + QuicPeerTransportParams self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_read_exception( + QuicReadException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_read_to_end_exception( + QuicReadToEndException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_udp_stats(QuicUdpStats self, SseSerializer serializer); + + @protected + void sse_encode_quic_write_exception( + QuicWriteException self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + (QuicClient, QuicClientConfig) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + (QuicClient, String) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + (QuicConnection, QuicSendStream) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + (QuicConnection, QuicSendStream, QuicRecvStream) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + (QuicConnection, BigInt?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + (QuicConnection, Uint8List?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + (QuicConnection, String?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + (QuicConnection, QuicConnectionStats) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + (QuicConnection, QuicPeerTransportParams) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + (QuicConnection, SocketAddress) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + (QuicConnection, BigInt) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + (QuicConnection, BigInt) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + (QuicEndpoint, QuicConnection) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + (QuicRecvStream, Uint8List) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + (QuicRecvStream, Uint8List?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + (QuicSendStream, BigInt) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_socket_address(SocketAddress self, SseSerializer serializer); + + @protected + void sse_encode_u_16(int self, SseSerializer serializer); + + @protected + void sse_encode_u_32(int self, SseSerializer serializer); + + @protected + void sse_encode_u_64(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => + RustLibWire(lib.ffiDynamicLibrary); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustLibWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClientPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClientPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClientPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClientPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnectionPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnectionPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnectionPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnectionPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfigPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfigPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfigPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfigPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStreamPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStreamPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStreamPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStreamPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStreamPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStreamPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStreamPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStreamPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfigPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfigPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfigPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfigPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfigPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfigPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfigPtr = + _lookup)>>( + 'frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfigPtr + .asFunction)>(); +} diff --git a/vendor/flutter_quic/lib/src/rust/frb_generated.web.dart b/vendor/flutter_quic/lib/src/rust/frb_generated.web.dart new file mode 100644 index 0000000..b406ffe --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/frb_generated.web.dart @@ -0,0 +1,1211 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +// Static analysis wrongly picks the IO variant, thus ignore this +// ignore_for_file: argument_type_not_assignable + +import 'api/bridge.dart'; +import 'convenience/client.dart'; +import 'core/config.dart'; +import 'core/connection.dart'; +import 'core/endpoint.dart'; +import 'core/stream.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'errors.dart'; +import 'frb_generated.dart'; +import 'models/types.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicClientPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicConnectionPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicEndpointPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicEndpointConfigPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicRecvStreamPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicSendStreamPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicServerConfigPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_QuicTransportConfigPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig; + + @protected + QuicClient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + dynamic raw, + ); + + @protected + QuicConnection + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ); + + @protected + QuicEndpoint + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + dynamic raw, + ); + + @protected + QuicEndpointConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + dynamic raw, + ); + + @protected + QuicRecvStream + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ); + + @protected + QuicSendStream + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ); + + @protected + QuicServerConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + dynamic raw, + ); + + @protected + QuicTransportConfig + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + dynamic raw, + ); + + @protected + QuicClient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + dynamic raw, + ); + + @protected + QuicConnection + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + dynamic raw, + ); + + @protected + QuicEndpoint + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + dynamic raw, + ); + + @protected + QuicEndpointConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + dynamic raw, + ); + + @protected + QuicRecvStream + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + dynamic raw, + ); + + @protected + QuicSendStream + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + dynamic raw, + ); + + @protected + QuicServerConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + dynamic raw, + ); + + @protected + QuicTransportConfig + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + QuicClientConfig dco_decode_box_autoadd_quic_client_config(dynamic raw); + + @protected + QuicReadException dco_decode_box_autoadd_quic_read_exception(dynamic raw); + + @protected + BigInt dco_decode_box_autoadd_usize(dynamic raw); + + @protected + List dco_decode_list_list_prim_u_8_strict(dynamic raw); + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); + + @protected + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + + @protected + QuicClientConfig dco_decode_quic_client_config(dynamic raw); + + @protected + QuicConnectionStats dco_decode_quic_connection_stats(dynamic raw); + + @protected + QuicDatagramException dco_decode_quic_datagram_exception(dynamic raw); + + @protected + QuicError dco_decode_quic_error(dynamic raw); + + @protected + QuicFrameStats dco_decode_quic_frame_stats(dynamic raw); + + @protected + QuicPathStats dco_decode_quic_path_stats(dynamic raw); + + @protected + QuicPeerTransportParams dco_decode_quic_peer_transport_params(dynamic raw); + + @protected + QuicReadException dco_decode_quic_read_exception(dynamic raw); + + @protected + QuicReadToEndException dco_decode_quic_read_to_end_exception(dynamic raw); + + @protected + QuicUdpStats dco_decode_quic_udp_stats(dynamic raw); + + @protected + QuicWriteException dco_decode_quic_write_exception(dynamic raw); + + @protected + (QuicClient, QuicClientConfig) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + dynamic raw, + ); + + @protected + (QuicClient, String) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + dynamic raw, + ); + + @protected + (QuicConnection, QuicSendStream) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + dynamic raw, + ); + + @protected + (QuicConnection, QuicSendStream, QuicRecvStream) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + dynamic raw, + ); + + @protected + (QuicConnection, BigInt?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + dynamic raw, + ); + + @protected + (QuicConnection, Uint8List?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (QuicConnection, String?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + dynamic raw, + ); + + @protected + (QuicConnection, QuicConnectionStats) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + dynamic raw, + ); + + @protected + (QuicConnection, QuicPeerTransportParams) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + dynamic raw, + ); + + @protected + (QuicConnection, SocketAddress) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + dynamic raw, + ); + + @protected + (QuicConnection, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + dynamic raw, + ); + + @protected + (QuicConnection, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + dynamic raw, + ); + + @protected + (QuicEndpoint, QuicConnection) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + dynamic raw, + ); + + @protected + (QuicRecvStream, Uint8List) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (QuicRecvStream, Uint8List?) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (QuicSendStream, BigInt) + dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + dynamic raw, + ); + + @protected + SocketAddress dco_decode_socket_address(dynamic raw); + + @protected + int dco_decode_u_16(dynamic raw); + + @protected + int dco_decode_u_32(dynamic raw); + + @protected + BigInt dco_decode_u_64(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + QuicClient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + SseDeserializer deserializer, + ); + + @protected + QuicConnection + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ); + + @protected + QuicEndpoint + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + SseDeserializer deserializer, + ); + + @protected + QuicEndpointConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + SseDeserializer deserializer, + ); + + @protected + QuicRecvStream + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ); + + @protected + QuicSendStream + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ); + + @protected + QuicServerConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + SseDeserializer deserializer, + ); + + @protected + QuicTransportConfig + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + SseDeserializer deserializer, + ); + + @protected + QuicClient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + SseDeserializer deserializer, + ); + + @protected + QuicConnection + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + SseDeserializer deserializer, + ); + + @protected + QuicEndpoint + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + SseDeserializer deserializer, + ); + + @protected + QuicEndpointConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + SseDeserializer deserializer, + ); + + @protected + QuicRecvStream + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + SseDeserializer deserializer, + ); + + @protected + QuicSendStream + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + SseDeserializer deserializer, + ); + + @protected + QuicServerConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + SseDeserializer deserializer, + ); + + @protected + QuicTransportConfig + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + QuicClientConfig sse_decode_box_autoadd_quic_client_config( + SseDeserializer deserializer, + ); + + @protected + QuicReadException sse_decode_box_autoadd_quic_read_exception( + SseDeserializer deserializer, + ); + + @protected + BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); + + @protected + List sse_decode_list_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); + + @protected + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + QuicClientConfig sse_decode_quic_client_config(SseDeserializer deserializer); + + @protected + QuicConnectionStats sse_decode_quic_connection_stats( + SseDeserializer deserializer, + ); + + @protected + QuicDatagramException sse_decode_quic_datagram_exception( + SseDeserializer deserializer, + ); + + @protected + QuicError sse_decode_quic_error(SseDeserializer deserializer); + + @protected + QuicFrameStats sse_decode_quic_frame_stats(SseDeserializer deserializer); + + @protected + QuicPathStats sse_decode_quic_path_stats(SseDeserializer deserializer); + + @protected + QuicPeerTransportParams sse_decode_quic_peer_transport_params( + SseDeserializer deserializer, + ); + + @protected + QuicReadException sse_decode_quic_read_exception( + SseDeserializer deserializer, + ); + + @protected + QuicReadToEndException sse_decode_quic_read_to_end_exception( + SseDeserializer deserializer, + ); + + @protected + QuicUdpStats sse_decode_quic_udp_stats(SseDeserializer deserializer); + + @protected + QuicWriteException sse_decode_quic_write_exception( + SseDeserializer deserializer, + ); + + @protected + (QuicClient, QuicClientConfig) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + SseDeserializer deserializer, + ); + + @protected + (QuicClient, String) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicSendStream) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicSendStream, QuicRecvStream) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, BigInt?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, Uint8List?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, String?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicConnectionStats) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, QuicPeerTransportParams) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, SocketAddress) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + SseDeserializer deserializer, + ); + + @protected + (QuicConnection, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + SseDeserializer deserializer, + ); + + @protected + (QuicEndpoint, QuicConnection) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + SseDeserializer deserializer, + ); + + @protected + (QuicRecvStream, Uint8List) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (QuicRecvStream, Uint8List?) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (QuicSendStream, BigInt) + sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + SseDeserializer deserializer, + ); + + @protected + SocketAddress sse_decode_socket_address(SseDeserializer deserializer); + + @protected + int sse_decode_u_16(SseDeserializer deserializer); + + @protected + int sse_decode_u_32(SseDeserializer deserializer); + + @protected + BigInt sse_decode_u_64(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + QuicClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + QuicEndpoint self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + QuicEndpointConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + QuicServerConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + QuicTransportConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + QuicClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + QuicConnection self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + QuicEndpoint self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + QuicEndpointConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + QuicRecvStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + QuicSendStream self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + QuicServerConfig self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + QuicTransportConfig self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_quic_client_config( + QuicClientConfig self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_quic_read_exception( + QuicReadException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_list_list_prim_u_8_strict( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); + + @protected + void sse_encode_opt_list_prim_u_8_strict( + Uint8List? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_client_config( + QuicClientConfig self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_connection_stats( + QuicConnectionStats self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_datagram_exception( + QuicDatagramException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_error(QuicError self, SseSerializer serializer); + + @protected + void sse_encode_quic_frame_stats( + QuicFrameStats self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_path_stats(QuicPathStats self, SseSerializer serializer); + + @protected + void sse_encode_quic_peer_transport_params( + QuicPeerTransportParams self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_read_exception( + QuicReadException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_read_to_end_exception( + QuicReadToEndException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_quic_udp_stats(QuicUdpStats self, SseSerializer serializer); + + @protected + void sse_encode_quic_write_exception( + QuicWriteException self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_quic_client_config( + (QuicClient, QuicClientConfig) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_client_string( + (QuicClient, String) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream( + (QuicConnection, QuicSendStream) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream( + (QuicConnection, QuicSendStream, QuicRecvStream) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_box_autoadd_usize( + (QuicConnection, BigInt?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_list_prim_u_8_strict( + (QuicConnection, Uint8List?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_opt_string( + (QuicConnection, String?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_connection_stats( + (QuicConnection, QuicConnectionStats) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_quic_peer_transport_params( + (QuicConnection, QuicPeerTransportParams) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_socket_address( + (QuicConnection, SocketAddress) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_u_64( + (QuicConnection, BigInt) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection_usize( + (QuicConnection, BigInt) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_endpoint_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_connection( + (QuicEndpoint, QuicConnection) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_list_prim_u_8_strict( + (QuicRecvStream, Uint8List) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_recv_stream_opt_list_prim_u_8_strict( + (QuicRecvStream, Uint8List?) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_quic_send_stream_usize( + (QuicSendStream, BigInt) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_socket_address(SocketAddress self, SseSerializer serializer); + + @protected + void sse_encode_u_16(int self, SseSerializer serializer); + + @protected + void sse_encode_u_32(int self, SseSerializer serializer); + + @protected + void sse_encode_u_64(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + RustLibWire.fromExternalLibrary(ExternalLibrary lib); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr, + ); +} + +@JS('wasm_bindgen') +external RustLibWasmModule get wasmModule; + +@JS() +@anonymous +extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + int ptr, + ); +} diff --git a/vendor/flutter_quic/lib/src/rust/models/types.dart b/vendor/flutter_quic/lib/src/rust/models/types.dart new file mode 100644 index 0000000..5da9a63 --- /dev/null +++ b/vendor/flutter_quic/lib/src/rust/models/types.dart @@ -0,0 +1,26 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Network address information +class SocketAddress { + final String ip; + final int port; + + const SocketAddress({required this.ip, required this.port}); + + @override + int get hashCode => ip.hashCode ^ port.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SocketAddress && + runtimeType == other.runtimeType && + ip == other.ip && + port == other.port; +} diff --git a/vendor/flutter_quic/linux/CMakeLists.txt b/vendor/flutter_quic/linux/CMakeLists.txt new file mode 100644 index 0000000..648f04b --- /dev/null +++ b/vendor/flutter_quic/linux/CMakeLists.txt @@ -0,0 +1,19 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "flutter_quic") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../rust flutter_quic "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(flutter_quic_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/vendor/flutter_quic/macos/Classes/dummy_file.c b/vendor/flutter_quic/macos/Classes/dummy_file.c new file mode 100644 index 0000000..e06dab9 --- /dev/null +++ b/vendor/flutter_quic/macos/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/vendor/flutter_quic/macos/flutter_quic.podspec b/vendor/flutter_quic/macos/flutter_quic.podspec new file mode 100644 index 0000000..3fdfcba --- /dev/null +++ b/vendor/flutter_quic/macos/flutter_quic.podspec @@ -0,0 +1,44 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_quic.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_quic' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust flutter_quic', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/libflutter_quic.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libflutter_quic.a', + } +end \ No newline at end of file diff --git a/vendor/flutter_quic/pubspec.yaml b/vendor/flutter_quic/pubspec.yaml new file mode 100644 index 0000000..1d40480 --- /dev/null +++ b/vendor/flutter_quic/pubspec.yaml @@ -0,0 +1,98 @@ +name: flutter_quic +description: "Next-generation networking for Flutter with QUIC protocol. Faster connections, better mobile performance, and HTTP/3 support built on the battle-tested Quinn library." +version: 1.0.0 +homepage: https://github.com/ShankarKakumani/flutter_quic +repository: https://github.com/ShankarKakumani/flutter_quic +issue_tracker: https://github.com/ShankarKakumani/flutter_quic/issues +documentation: https://github.com/ShankarKakumani/flutter_quic#readme + +environment: + sdk: '>=3.8.1 <4.0.0' + flutter: '>=3.3.0' + +topics: + - networking + - http3 + - quic + - performance + - networking-protocol + + + +dependencies: + flutter: + sdk: flutter + flutter_rust_bridge: 2.11.1 + plugin_platform_interface: ^2.0.2 + freezed_annotation: ^3.0.0 + +dev_dependencies: + ffi: ^2.1.3 + ffigen: ^13.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + freezed: ^3.1.0 + json_annotation: ^4.9.0 + build_runner: ^2.4.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + # + # Please refer to README.md for a detailed explanation. + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/vendor/flutter_quic/rust/Cargo.lock b/vendor/flutter_quic/rust/Cargo.lock new file mode 100644 index 0000000..e3105b4 --- /dev/null +++ b/vendor/flutter_quic/rust/Cargo.lock @@ -0,0 +1,2140 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" +dependencies = [ + "anyhow", + "atomic", + "backtrace", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand", + "siphasher", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flutter_quic" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "flutter_rust_bridge", + "futures", + "quinn", + "quinn-proto", + "rustls", + "rustls-pki-types", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "flutter_rust_bridge" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725" +dependencies = [ + "allo-isolate", + "android_logger", + "anyhow", + "build-target", + "bytemuck", + "byteorder", + "console_error_panic_hook", + "dart-sys", + "delegate-attr", + "flutter_rust_bridge_macros", + "futures", + "js-sys", + "lazy_static", + "log", + "oslog", + "portable-atomic", + "threadpool", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qlog" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f15b83c59e6b945f2261c95a1dd9faf239187f32ff0a96af1d1d28c4557f919" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "smallvec", +] + +[[package]] +name = "quinn" +version = "0.11.9" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "qlog", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "serde_core", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/flutter_quic/rust/Cargo.toml b/vendor/flutter_quic/rust/Cargo.toml new file mode 100644 index 0000000..9f56dfe --- /dev/null +++ b/vendor/flutter_quic/rust/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "flutter_quic" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +flutter_rust_bridge = "=2.11.1" +quinn = { version = "0.11.8", features = ["qlog"] } +quinn-proto = "0.11.12" +tokio = { version = "1.0", features = ["full"] } +rustls = { version = "0.23.5", default-features = false, features = ["ring"] } +rustls-pki-types = "1.0" +thiserror = "2.0" +bytes = "1.8" +futures = "0.3" +tracing = "0.1" +chrono = "0.4" +url = "2.4" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } + +# Local vendored forks of quinn and quinn-proto 0.11.x, patched to expose the +# peer's advertised `initial_max_streams_bidi` (and friends) via new public +# accessors on `Connection`. Upstream quinn 0.11 does not expose peer transport +# parameters, which makes diagnosing bidi-stream credit starvation (an +# `open_bi()` that blocks forever because the peer advertised a small +# `initial_max_streams_bidi` and never sends `MAX_STREAMS`) essentially +# impossible from the application side. See: +# vendor/quinn-proto/src/connection/mod.rs -> peer_params_initial_max_streams_bidi +# vendor/quinn/src/connection.rs -> peer_params_initial_max_streams_bidi +[patch.crates-io] +quinn = { path = "vendor/quinn" } +quinn-proto = { path = "vendor/quinn-proto" } diff --git a/vendor/flutter_quic/rust/src/api/bridge.rs b/vendor/flutter_quic/rust/src/api/bridge.rs new file mode 100644 index 0000000..93a2b9e --- /dev/null +++ b/vendor/flutter_quic/rust/src/api/bridge.rs @@ -0,0 +1,420 @@ +#[flutter_rust_bridge::frb(init)] +pub fn init_app() { + // Default utilities - feel free to customize + flutter_rust_bridge::setup_default_user_utils(); +} + +// Core API exposure functions to ensure flutter_rust_bridge discovers our types +use crate::core::{QuicEndpoint, QuicConnection, QuicSendStream, QuicRecvStream}; +use crate::core::{QuicConnectionStats, QuicPathStats, QuicFrameStats, QuicUdpStats}; +use crate::core::connection::QuicPeerTransportParams; +use crate::core::{QuicServerConfig, QuicTransportConfig, QuicEndpointConfig}; +use crate::convenience::{QuicClient, QuicClientConfig}; +use crate::errors::{QuicError, QuicWriteException, QuicReadException, QuicReadToEndException, QuicDatagramException}; +use std::net::SocketAddr; + + + +/// Create a new QUIC client endpoint +pub fn create_client_endpoint() -> Result { + let rt = tokio::runtime::Runtime::new() + .map_err(|e| QuicError::Endpoint(format!("Failed to create runtime: {:?}", e)))?; + + rt.block_on(async { + QuicEndpoint::client() + }) +} + +/// Create a new QUIC server endpoint +pub fn create_server_endpoint(config: QuicServerConfig, addr: String) -> Result { + QuicEndpoint::server(config, addr) +} + +/// Write data to a QUIC send stream +/// This exposes the QuicSendStream.write() method to flutter_rust_bridge +pub async fn send_stream_write( + mut stream: QuicSendStream, + data: Vec, +) -> Result<(QuicSendStream, usize), QuicWriteException> { + let bytes_written = stream.write(data).await?; + Ok((stream, bytes_written)) +} + +/// Write all data to a QUIC send stream +/// This exposes the QuicSendStream.write_all() method to flutter_rust_bridge +pub async fn send_stream_write_all( + mut stream: QuicSendStream, + data: Vec, +) -> Result { + stream.write_all(data).await?; + Ok(stream) +} + +/// Finish a QUIC send stream +/// This exposes the QuicSendStream.finish() method to flutter_rust_bridge +pub fn send_stream_finish( + mut stream: QuicSendStream, +) -> Result { + stream.finish()?; + Ok(stream) +} + +/// Read data from a QUIC recv stream +/// This exposes the QuicRecvStream.read() method to flutter_rust_bridge +pub async fn recv_stream_read( + mut stream: QuicRecvStream, + max_length: usize, +) -> Result<(QuicRecvStream, Option>), QuicReadException> { + let data = stream.read(max_length).await?; + Ok((stream, data)) +} + +/// Read all remaining data from a QUIC recv stream +/// This exposes the QuicRecvStream.read_to_end() method to flutter_rust_bridge +pub async fn recv_stream_read_to_end( + mut stream: QuicRecvStream, + max_length: usize, +) -> Result<(QuicRecvStream, Vec), QuicReadToEndException> { + let data = stream.read_to_end(max_length).await?; + Ok((stream, data)) +} + +/// Open a bidirectional stream on a QUIC connection +/// This exposes the QuicConnection.open_bi() method to flutter_rust_bridge +pub async fn connection_open_bi( + connection: QuicConnection, +) -> Result<(QuicConnection, QuicSendStream, QuicRecvStream), QuicError> { + let (send_stream, recv_stream) = connection.open_bi().await?; + Ok((connection, send_stream, recv_stream)) +} + +/// Accept the next server-initiated bidirectional stream on a QUIC connection. +/// +/// Blocks until the remote peer opens a new bidirectional stream or the +/// connection is closed. Returns `None` (as a missing tuple) when the +/// connection has been terminated. +/// +/// Unlike `connection_open_bi`, this function takes a shared reference so it +/// does NOT consume the `QuicConnection` arc. This allows `accept_bi` to block +/// waiting for a server-initiated stream concurrently with `open_bi` calls +/// without holding the Auto_Owned ownership lock. +/// +/// This exposes the QuicConnection.accept_bi() method to flutter_rust_bridge. +pub async fn connection_accept_bi( + connection: &QuicConnection, +) -> Result<(Option, Option), QuicError> { + match connection.accept_bi().await { + Some((send_stream, recv_stream)) => Ok((Some(send_stream), Some(recv_stream))), + None => Ok((None, None)), + } +} + +/// Open a unidirectional stream on a QUIC connection +/// This exposes the QuicConnection.open_uni() method to flutter_rust_bridge +pub async fn connection_open_uni( + connection: QuicConnection, +) -> Result<(QuicConnection, QuicSendStream), QuicError> { + let send_stream = connection.open_uni().await?; + Ok((connection, send_stream)) +} + +/// Connect to a server using a QUIC endpoint. +/// +/// If `qlog_path` is `Some`, Quinn will write a full QUIC event trace (in +/// qlog JSON-SEQ format) to that file for the lifetime of the connection. +/// The resulting file can be analysed offline with tools such as +/// [qvis](https://qvis.quictools.info/) or Wireshark's qlog importer. +/// +/// This exposes the QuicEndpoint.connect() method to flutter_rust_bridge. +pub async fn endpoint_connect( + _endpoint: QuicEndpoint, + addr: String, + server_name: String, + qlog_path: Option, +) -> Result<(QuicEndpoint, QuicConnection), QuicError> { + // Build the endpoint inside the async executor context. + let remote_addr: SocketAddr = addr.parse() + .map_err(|e| QuicError::Connection(format!("Invalid address: {:?}", e)))?; + let endpoint = QuicEndpoint::client_for_remote_with_qlog(remote_addr, qlog_path)?; + let connection = endpoint.connect(addr, server_name).await?; + Ok((endpoint, connection)) +} + +/// Send a datagram on a QUIC connection +/// This exposes the QuicConnection.send_datagram() method to flutter_rust_bridge +pub fn connection_send_datagram( + connection: QuicConnection, + data: Vec, +) -> Result { + connection.send_datagram(data)?; + Ok(connection) +} + +/// Send a datagram with backpressure on a QUIC connection +/// This exposes the QuicConnection.send_datagram_wait() method to flutter_rust_bridge +pub async fn connection_send_datagram_wait( + connection: QuicConnection, + data: Vec, +) -> Result { + connection.send_datagram_wait(data).await?; + Ok(connection) +} + +/// Read a datagram from a QUIC connection +/// This exposes the QuicConnection.read_datagram() method to flutter_rust_bridge +pub async fn connection_read_datagram( + connection: QuicConnection, +) -> (QuicConnection, Option>) { + let data = connection.read_datagram().await; + (connection, data) +} + +/// Get datagram send buffer space +/// This exposes the QuicConnection.datagram_send_buffer_space() method to flutter_rust_bridge +pub fn connection_datagram_send_buffer_space( + connection: QuicConnection, +) -> (QuicConnection, usize) { + let space = connection.datagram_send_buffer_space(); + (connection, space) +} + +/// Get maximum datagram size +/// This exposes the QuicConnection.max_datagram_size() method to flutter_rust_bridge +pub fn connection_max_datagram_size( + connection: QuicConnection, +) -> (QuicConnection, Option) { + let max_size = connection.max_datagram_size(); + (connection, max_size) +} + +// Connection Info bridge functions + +/// Get the remote address of a QUIC connection +/// This exposes the QuicConnection.remote_address() method to flutter_rust_bridge +pub fn connection_remote_address( + connection: QuicConnection, +) -> (QuicConnection, crate::models::types::SocketAddress) { + let addr = connection.remote_address(); + let socket_addr = crate::models::types::SocketAddress { + ip: addr.ip().to_string(), + port: addr.port(), + }; + (connection, socket_addr) +} + +/// Get the local IP address of a QUIC connection +/// This exposes the QuicConnection.local_ip() method to flutter_rust_bridge +pub fn connection_local_ip( + connection: QuicConnection, +) -> (QuicConnection, Option) { + let ip = connection.local_ip(); + (connection, ip.map(|ip| ip.to_string())) +} + +/// Get the RTT of a QUIC connection in milliseconds +/// This exposes the QuicConnection.rtt() method to flutter_rust_bridge +pub fn connection_rtt_millis( + connection: QuicConnection, +) -> (QuicConnection, u64) { + let rtt = connection.rtt(); + (connection, rtt.as_millis() as u64) +} + +/// Get the stable ID of a QUIC connection +/// This exposes the QuicConnection.stable_id() method to flutter_rust_bridge +pub fn connection_stable_id( + connection: QuicConnection, +) -> (QuicConnection, usize) { + let id = connection.stable_id(); + (connection, id) +} + +/// Get the close reason of a QUIC connection +/// This exposes the QuicConnection.close_reason() method to flutter_rust_bridge +pub fn connection_close_reason( + connection: QuicConnection, +) -> (QuicConnection, Option) { + let reason = connection.close_reason(); + (connection, reason) +} + +/// Get the statistics of a QUIC connection +/// This exposes the QuicConnection.stats() method to flutter_rust_bridge +pub fn connection_stats( + connection: QuicConnection, +) -> (QuicConnection, QuicConnectionStats) { + let stats = connection.stats(); + (connection, stats) +} + +/// Get the peer's advertised QUIC transport parameters relevant to stream credits. +/// +/// Exposes [`QuicConnection::peer_transport_params`] across the FFI. Useful for +/// diagnosing `connection_open_bi` hangs caused by the peer advertising a small +/// `initial_max_streams_bidi` and never raising it with `MAX_STREAMS`. +pub fn connection_peer_transport_params( + connection: QuicConnection, +) -> (QuicConnection, QuicPeerTransportParams) { + let params = connection.peer_transport_params(); + (connection, params) +} + +// Configuration builder functions + +/// Create a new server config with single certificate +pub fn server_config_with_single_cert( + cert_chain: Vec>, + key: Vec, +) -> Result { + QuicServerConfig::with_single_cert(cert_chain, key) +} + +/// Create a new transport config +pub fn transport_config_new() -> QuicTransportConfig { + QuicTransportConfig::new() +} + +/// Create a new endpoint config +pub fn endpoint_config_new() -> QuicEndpointConfig { + QuicEndpointConfig::new() +} + +// Type exposure functions to ensure flutter_rust_bridge discovers our types +pub fn _expose_types_for_frb_generation() { + let _endpoint: Option = None; + let _connection: Option = None; + let _send_stream: Option = None; + let _recv_stream: Option = None; + let _error: Option = None; + let _write_exception: Option = None; + let _read_exception: Option = None; + let _read_to_end_exception: Option = None; + let _datagram_exception: Option = None; + let _connection_stats: Option = None; + let _path_stats: Option = None; + let _frame_stats: Option = None; + let _udp_stats: Option = None; + let _peer_params: Option = None; + let _server_config: Option = None; + let _transport_config: Option = None; + let _endpoint_config: Option = None; +} + +// Legacy expose functions for backwards compatibility with generated code +pub fn _expose_connection_type(connection: QuicConnection) -> QuicConnection { + connection +} + +pub fn _expose_send_stream_type(stream: QuicSendStream) -> QuicSendStream { + stream +} + +pub fn _expose_recv_stream_type(stream: QuicRecvStream) -> QuicRecvStream { + stream +} + +// Convenience API exposure functions + +/// Create a new QuicClient with default configuration +pub fn quic_client_create() -> Result { + let rt = tokio::runtime::Runtime::new() + .map_err(|e| QuicError::Endpoint(format!("Failed to create runtime: {:?}", e)))?; + + rt.block_on(async { + QuicClient::create() + }) +} + +/// Create a new QuicClient with custom configuration +pub fn quic_client_create_with_config(config: QuicClientConfig) -> Result { + let rt = tokio::runtime::Runtime::new() + .map_err(|e| QuicError::Endpoint(format!("Failed to create runtime: {:?}", e)))?; + + rt.block_on(async { + QuicClient::create_with_config(config) + }) +} + +/// Send data using QuicClient and return response +pub async fn quic_client_send( + client: QuicClient, + url: String, + data: String, +) -> Result<(QuicClient, String), QuicError> { + let response = client.send(url, data).await?; + Ok((client, response)) +} + +/// Send data with timeout using QuicClient +pub async fn quic_client_send_with_timeout( + client: QuicClient, + url: String, + data: String, +) -> Result<(QuicClient, String), QuicError> { + let response = client.send_with_timeout(url, data).await?; + Ok((client, response)) +} + +/// Send a GET request using QuicClient +pub async fn quic_client_get( + client: QuicClient, + url: String, +) -> Result<(QuicClient, String), QuicError> { + let response = client.get(url).await?; + Ok((client, response)) +} + +/// Send a POST request using QuicClient +pub async fn quic_client_post( + client: QuicClient, + url: String, + data: String, +) -> Result<(QuicClient, String), QuicError> { + let response = client.post(url, data).await?; + Ok((client, response)) +} + +/// Send a GET request with timeout using QuicClient +pub async fn quic_client_get_with_timeout( + client: QuicClient, + url: String, +) -> Result<(QuicClient, String), QuicError> { + let response = client.get_with_timeout(url).await?; + Ok((client, response)) +} + +/// Send a POST request with timeout using QuicClient +pub async fn quic_client_post_with_timeout( + client: QuicClient, + url: String, + data: String, +) -> Result<(QuicClient, String), QuicError> { + let response = client.post_with_timeout(url, data).await?; + Ok((client, response)) +} + +/// Get QuicClient configuration +pub fn quic_client_config(client: QuicClient) -> (QuicClient, QuicClientConfig) { + let config = client.config(); + (client, config) +} + +/// Clear QuicClient connection pool +pub fn quic_client_clear_pool(client: QuicClient) -> QuicClient { + client.clear_pool(); + client +} + +/// Create a new QuicClientConfig with default values +pub fn quic_client_config_new() -> QuicClientConfig { + QuicClientConfig::default() +} + +/// Expose QuicClient and QuicClientConfig types for flutter_rust_bridge +pub fn _expose_quic_client_type(client: QuicClient) -> QuicClient { + client +} + +pub fn _expose_quic_client_config_type(config: QuicClientConfig) -> QuicClientConfig { + config +} diff --git a/vendor/flutter_quic/rust/src/api/mod.rs b/vendor/flutter_quic/rust/src/api/mod.rs new file mode 100644 index 0000000..d437780 --- /dev/null +++ b/vendor/flutter_quic/rust/src/api/mod.rs @@ -0,0 +1,7 @@ +pub mod bridge; + +// Re-export core and convenience APIs for flutter_rust_bridge +pub use crate::core; +pub use crate::convenience; +pub use crate::models; +pub use crate::errors; diff --git a/vendor/flutter_quic/rust/src/convenience/client.rs b/vendor/flutter_quic/rust/src/convenience/client.rs new file mode 100644 index 0000000..aa9d59e --- /dev/null +++ b/vendor/flutter_quic/rust/src/convenience/client.rs @@ -0,0 +1,392 @@ +//! Convenience Client API - Simple QUIC client interface + +use flutter_rust_bridge::frb; +use crate::core::{QuicEndpoint, QuicConnection}; +use crate::errors::QuicError; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use url::Url; + +/// Configuration for QuicClient +#[derive(Debug, Clone)] +pub struct QuicClientConfig { + /// Maximum number of connections per host + pub max_connections_per_host: usize, + /// Connection timeout in milliseconds + pub connect_timeout_ms: u64, + /// Request timeout in milliseconds + pub request_timeout_ms: u64, + /// Number of retry attempts for failed requests + pub retry_attempts: u32, + /// Retry delay in milliseconds + pub retry_delay_ms: u64, + /// Keep-alive timeout for connections in milliseconds + pub keep_alive_timeout_ms: u64, +} + +impl Default for QuicClientConfig { + fn default() -> Self { + Self { + max_connections_per_host: 10, + connect_timeout_ms: 5000, + request_timeout_ms: 30000, + retry_attempts: 3, + retry_delay_ms: 1000, + keep_alive_timeout_ms: 60000, + } + } +} + +/// Entry in the connection pool +#[derive(Debug)] +struct PooledConnection { + connection: QuicConnection, + last_used: Instant, + host: String, +} + +/// High-level QUIC client with Dio-style interface +/// +/// QuicClient provides a simple HTTP-like interface built on top of the Core API, +/// with automatic connection pooling, retries, and simplified configuration. +/// +/// # Example +/// ```dart +/// final client = QuicClient.create(); +/// final response = await client.send('https://example.com/api/data', data); +/// ``` +#[frb(opaque)] +pub struct QuicClient { + endpoint: QuicEndpoint, + config: QuicClientConfig, + // Connection pool keyed by host:port + pool: Arc>>>, +} + +impl QuicClient { + /// Create a new QuicClient with default configuration + /// + /// Creates a client endpoint and initializes the connection pool. + /// Uses insecure configuration suitable for testing. + pub fn create() -> Result { + let endpoint = QuicEndpoint::client()?; + let config = QuicClientConfig::default(); + let pool = Arc::new(Mutex::new(HashMap::new())); + + Ok(Self { + endpoint, + config, + pool, + }) + } + + /// Create a new QuicClient with custom configuration + /// + /// # Arguments + /// * `config` - Custom configuration for the client + pub fn create_with_config(config: QuicClientConfig) -> Result { + let endpoint = QuicEndpoint::client()?; + let pool = Arc::new(Mutex::new(HashMap::new())); + + Ok(Self { + endpoint, + config, + pool, + }) + } + + /// Get or create a connection to the specified host + /// + /// This method implements connection pooling - it will reuse existing + /// connections when possible or create new ones when needed. + /// + /// # Arguments + /// * `url` - Target URL to connect to + async fn get_connection(&self, url: &str) -> Result { + let parsed_url = Url::parse(url) + .map_err(|e| QuicError::Connection(format!("Invalid URL: {:?}", e)))?; + + let host = parsed_url.host_str() + .ok_or_else(|| QuicError::Connection("URL must contain a host".to_string()))?; + + let port = parsed_url.port().unwrap_or(443); + let host_key = format!("{}:{}", host, port); + let addr = format!("{}:{}", host, port); + + // Try to get an existing connection from the pool + { + let mut pool = self.pool.lock().unwrap(); + if let Some(connections) = pool.get_mut(&host_key) { + // Remove expired connections + let now = Instant::now(); + let keep_alive_duration = Duration::from_millis(self.config.keep_alive_timeout_ms); + connections.retain(|conn| now.duration_since(conn.last_used) < keep_alive_duration); + + // Try to reuse an existing connection + if let Some(mut pooled_conn) = connections.pop() { + pooled_conn.last_used = now; + connections.push(pooled_conn); + return Ok(connections.last().unwrap().connection.clone()); + } + } + } + + // Create a new connection + let connection = self.endpoint.connect(addr, host.to_string()).await?; + + // Add to pool + { + let mut pool = self.pool.lock().unwrap(); + let connections = pool.entry(host_key.clone()).or_insert_with(Vec::new); + + // Limit connections per host + if connections.len() >= self.config.max_connections_per_host { + connections.remove(0); // Remove oldest connection + } + + connections.push(PooledConnection { + connection: connection.clone(), + last_used: Instant::now(), + host: host_key, + }); + } + + Ok(connection) + } + + /// Get the current configuration + pub fn config(&self) -> QuicClientConfig { + self.config.clone() + } + + /// Update the client configuration + pub fn set_config(&mut self, config: QuicClientConfig) { + self.config = config; + } + + /// Get connection pool statistics + /// + /// Returns information about the current state of the connection pool. + pub fn pool_stats(&self) -> HashMap { + let pool = self.pool.lock().unwrap(); + pool.iter() + .map(|(host, connections)| (host.clone(), connections.len())) + .collect() + } + + /// Clear all connections from the pool + /// + /// Forces all future requests to create new connections. + pub fn clear_pool(&self) { + let mut pool = self.pool.lock().unwrap(); + pool.clear(); + } + + /// Send data to a URL and return the response + /// + /// This is the core method providing a Dio-style interface for QUIC requests. + /// It handles connection pooling, retries, and provides a simple request/response pattern. + /// + /// # Arguments + /// * `url` - Target URL (e.g., "https://example.com/api/data") + /// * `data` - Data to send (will be sent as bytes over QUIC stream) + /// + /// # Returns + /// * `Ok(String)` - Response data as UTF-8 string + /// * `Err(QuicError)` - Connection, stream, or network errors + /// + /// # Example + /// ```dart + /// final client = QuicClient.create(); + /// final response = await client.send('https://api.example.com/data', 'Hello QUIC!'); + /// print('Response: $response'); + /// ``` + pub async fn send(&self, url: String, data: String) -> Result { + let mut last_error = QuicError::Connection("No attempts made".to_string()); + + // Retry logic with configurable attempts + for attempt in 0..=self.config.retry_attempts { + match self.send_once(&url, &data).await { + Ok(response) => return Ok(response), + Err(error) => { + last_error = error; + + // Don't retry on final attempt + if attempt < self.config.retry_attempts { + // Check if error is retryable + if self.is_retryable_error(&last_error) { + // Wait before retry + let delay = Duration::from_millis( + self.config.retry_delay_ms * (attempt as u64 + 1) + ); + tokio::time::sleep(delay).await; + continue; + } else { + // Non-retryable error, fail immediately + break; + } + } + } + } + } + + Err(last_error) + } + + /// Perform a single send attempt without retries + async fn send_once(&self, url: &str, data: &str) -> Result { + // Get or create connection using our pooling logic + let connection = self.get_connection(url).await?; + + // Open a bidirectional stream for request/response + let (mut send_stream, mut recv_stream) = connection.open_bi().await?; + + // Send the request data + let request_bytes = data.as_bytes().to_vec(); + send_stream.write_all(request_bytes).await + .map_err(|e| QuicError::Stream(format!("Failed to send data: {:?}", e)))?; + + // Finish sending (signals end of request) + send_stream.finish() + .map_err(|e| QuicError::Stream(format!("Failed to finish send stream: {:?}", e)))?; + + // Read the response with a reasonable size limit (1MB) + let response_bytes = recv_stream.read_to_end(1024 * 1024).await + .map_err(|e| QuicError::Stream(format!("Failed to read response: {:?}", e)))?; + + // Convert response to UTF-8 string + let response = String::from_utf8(response_bytes) + .map_err(|e| QuicError::Stream(format!("Response is not valid UTF-8: {:?}", e)))?; + + Ok(response) + } + + /// Check if an error is retryable + /// + /// Determines whether a failed request should be retried based on the error type. + /// Network timeouts and connection issues are retryable, but authentication + /// and protocol errors are not. + fn is_retryable_error(&self, error: &QuicError) -> bool { + match error { + // Retryable connection errors + QuicError::Connection(msg) => { + // Check for timeout or network-related errors + msg.contains("timeout") || + msg.contains("refused") || + msg.contains("unreachable") || + msg.contains("Failed to establish connection") + }, + + // Retryable network errors + QuicError::Network(_) => true, + + // Retryable stream errors (connection might have been lost) + QuicError::Stream(msg) => { + msg.contains("ConnectionLost") || + msg.contains("connection lost") || + msg.contains("Failed to send data") + }, + + // Non-retryable errors + QuicError::Config(_) => false, + QuicError::Tls(_) => false, + QuicError::Endpoint(_) => false, + QuicError::Write(_) => false, + } + } + + /// Send data with timeout + /// + /// Wraps the send operation with a configurable timeout to prevent hanging requests. + pub async fn send_with_timeout(&self, url: String, data: String) -> Result { + let timeout_duration = Duration::from_millis(self.config.request_timeout_ms); + + match tokio::time::timeout(timeout_duration, self.send(url, data)).await { + Ok(result) => result, + Err(_) => Err(QuicError::Network("Request timed out".to_string())), + } + } + + /// Send a GET request to the specified URL + /// + /// This provides an HTTP-like interface for simple GET requests without data. + /// Internally uses the send() method with empty data. + /// + /// # Arguments + /// * `url` - Target URL (e.g., "https://api.example.com/users") + /// + /// # Returns + /// * `Ok(String)` - Response data as UTF-8 string + /// * `Err(QuicError)` - Connection, stream, or network errors + /// + /// # Example + /// ```dart + /// final client = QuicClient.create(); + /// final users = await client.get('https://api.example.com/users'); + /// print('Users: $users'); + /// ``` + pub async fn get(&self, url: String) -> Result { + // GET requests typically don't send data, so we use empty string + self.send(url, String::new()).await + } + + /// Send a POST request with data to the specified URL + /// + /// This provides an HTTP-like interface for POST requests with data payload. + /// Internally uses the send() method. + /// + /// # Arguments + /// * `url` - Target URL (e.g., "https://api.example.com/users") + /// * `data` - Data to send in the request body (JSON, form data, etc.) + /// + /// # Returns + /// * `Ok(String)` - Response data as UTF-8 string + /// * `Err(QuicError)` - Connection, stream, or network errors + /// + /// # Example + /// ```dart + /// final client = QuicClient.create(); + /// final newUser = await client.post( + /// 'https://api.example.com/users', + /// '{"name": "John", "email": "john@example.com"}' + /// ); + /// print('Created user: $newUser'); + /// ``` + pub async fn post(&self, url: String, data: String) -> Result { + // POST requests send data, so we pass it directly to send() + self.send(url, data).await + } + + /// Send a GET request with timeout + /// + /// Combines the convenience of get() with timeout protection. + pub async fn get_with_timeout(&self, url: String) -> Result { + let timeout_duration = Duration::from_millis(self.config.request_timeout_ms); + + match tokio::time::timeout(timeout_duration, self.get(url)).await { + Ok(result) => result, + Err(_) => Err(QuicError::Network("GET request timed out".to_string())), + } + } + + /// Send a POST request with timeout + /// + /// Combines the convenience of post() with timeout protection. + pub async fn post_with_timeout(&self, url: String, data: String) -> Result { + let timeout_duration = Duration::from_millis(self.config.request_timeout_ms); + + match tokio::time::timeout(timeout_duration, self.post(url, data)).await { + Ok(result) => result, + Err(_) => Err(QuicError::Network("POST request timed out".to_string())), + } + } +} + +// Quinn Connection already implements Clone, so we can derive it for QuicConnection +impl Clone for QuicConnection { + fn clone(&self) -> Self { + // Quinn connections can be safely cloned - they represent handles to the same connection + QuicConnection::new(self.inner().clone()) + } +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/convenience/mod.rs b/vendor/flutter_quic/rust/src/convenience/mod.rs new file mode 100644 index 0000000..872ba30 --- /dev/null +++ b/vendor/flutter_quic/rust/src/convenience/mod.rs @@ -0,0 +1,10 @@ +//! Convenience API - Simple, high-level interface +//! +//! This module provides an easy-to-use API built on top of the Core API, +//! with features like connection pooling, automatic retries, and simplified configuration. + +pub mod client; +pub mod server; +pub mod pool; + +pub use client::{QuicClient, QuicClientConfig}; \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/convenience/pool.rs b/vendor/flutter_quic/rust/src/convenience/pool.rs new file mode 100644 index 0000000..727d7ea --- /dev/null +++ b/vendor/flutter_quic/rust/src/convenience/pool.rs @@ -0,0 +1,8 @@ +//! Connection Pool API - Managed QUIC connections + +use flutter_rust_bridge::frb; + +#[frb(opaque)] +pub struct QuicConnectionPool { + // Connection pool implementation +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/convenience/server.rs b/vendor/flutter_quic/rust/src/convenience/server.rs new file mode 100644 index 0000000..3b04315 --- /dev/null +++ b/vendor/flutter_quic/rust/src/convenience/server.rs @@ -0,0 +1,8 @@ +//! Convenience Server API - Simple QUIC server interface + +use flutter_rust_bridge::frb; + +#[frb(opaque)] +pub struct SimpleQuicServer { + // High-level server implementation +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/core/config.rs b/vendor/flutter_quic/rust/src/core/config.rs new file mode 100644 index 0000000..8a39037 --- /dev/null +++ b/vendor/flutter_quic/rust/src/core/config.rs @@ -0,0 +1,268 @@ +//! Configuration builders for QUIC endpoints and connections + +use flutter_rust_bridge::frb; +use std::sync::Arc; +use std::time::Duration; + +/// QUIC Server Configuration +#[frb(opaque)] +pub struct QuicServerConfig { + inner: quinn::ServerConfig, +} + +impl QuicServerConfig { + /// Create a new server config with a single certificate chain and key + pub fn with_single_cert( + cert_chain: Vec>, + key: Vec, + ) -> Result { + use rustls_pki_types::{CertificateDer, PrivateKeyDer}; + + // Convert Vec> to Vec + let cert_chain: Vec = cert_chain + .into_iter() + .map(|cert| CertificateDer::from(cert)) + .collect(); + + // Convert key bytes to PrivateKeyDer + let private_key = PrivateKeyDer::try_from(key) + .map_err(|e| format!("Invalid private key: {:?}", e))?; + + let server_config = quinn::ServerConfig::with_single_cert(cert_chain, private_key) + .map_err(|e| format!("Failed to create server config: {:?}", e))?; + + Ok(Self { inner: server_config }) + } + + /// Create a new server config with a crypto provider and certificate + pub fn with_crypto( + cert_chain: Vec>, + key: Vec, + alpn_protocols: Vec>, + ) -> Result { + use rustls_pki_types::{CertificateDer, PrivateKeyDer}; + use rustls::ServerConfig as RustlsServerConfig; + + // Convert certificate chain + let cert_chain: Vec = cert_chain + .into_iter() + .map(|cert| CertificateDer::from(cert)) + .collect(); + + // Convert private key + let private_key = PrivateKeyDer::try_from(key) + .map_err(|e| format!("Invalid private key: {:?}", e))?; + + // Create rustls config + let mut crypto_config = RustlsServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, private_key) + .map_err(|e| format!("Failed to create TLS config: {:?}", e))?; + + crypto_config.alpn_protocols = alpn_protocols; + + let server_config = quinn::ServerConfig::with_crypto(Arc::new( + quinn::crypto::rustls::QuicServerConfig::try_from(crypto_config) + .map_err(|e| format!("Failed to create QUIC server config: {:?}", e))? + )); + + Ok(Self { inner: server_config }) + } + + /// Get the inner Quinn ServerConfig + pub(crate) fn inner(&self) -> &quinn::ServerConfig { + &self.inner + } + + /// Convert to Quinn ServerConfig + pub fn into_inner(self) -> quinn::ServerConfig { + self.inner + } +} + +/// QUIC Transport Configuration +#[frb(opaque)] +pub struct QuicTransportConfig { + inner: quinn::TransportConfig, +} + +impl QuicTransportConfig { + /// Create a new transport config with default settings + pub fn new() -> Self { + Self { + inner: quinn::TransportConfig::default(), + } + } + + /// Set the maximum number of concurrent bidirectional streams + pub fn max_concurrent_bidi_streams(&mut self, count: u32) -> Result<(), String> { + self.inner + .max_concurrent_bidi_streams(quinn::VarInt::from_u32(count)); + Ok(()) + } + + /// Set the maximum number of concurrent unidirectional streams + pub fn max_concurrent_uni_streams(&mut self, count: u32) -> Result<(), String> { + self.inner + .max_concurrent_uni_streams(quinn::VarInt::from_u32(count)); + Ok(()) + } + + /// Set the maximum idle timeout + pub fn max_idle_timeout(&mut self, timeout_ms: Option) -> Result<(), String> { + let timeout = timeout_ms.map(|ms| { + quinn::IdleTimeout::try_from(Duration::from_millis(ms)) + }).transpose() + .map_err(|e| format!("Invalid idle timeout: {:?}", e))?; + + self.inner + .max_idle_timeout(timeout); + Ok(()) + } + + /// Set the stream receive window size + pub fn stream_receive_window(&mut self, size: u32) -> Result<(), String> { + self.inner + .stream_receive_window(quinn::VarInt::from_u32(size)); + Ok(()) + } + + /// Set the connection receive window size + pub fn receive_window(&mut self, size: u32) -> Result<(), String> { + self.inner + .receive_window(quinn::VarInt::from_u32(size)); + Ok(()) + } + + /// Set the send window size + pub fn send_window(&mut self, size: u64) -> &mut Self { + self.inner.send_window(size); + self + } + + /// Set the initial RTT estimate + pub fn initial_rtt(&mut self, rtt_ms: u64) -> &mut Self { + self.inner.initial_rtt(Duration::from_millis(rtt_ms)); + self + } + + /// Set the keep-alive interval + pub fn keep_alive_interval(&mut self, interval_ms: Option) -> &mut Self { + let interval = interval_ms.map(Duration::from_millis); + self.inner.keep_alive_interval(interval); + self + } + + /// Enable or disable the spin bit + pub fn allow_spin(&mut self, allow: bool) -> &mut Self { + self.inner.allow_spin(allow); + self + } + + /// Set the datagram receive buffer size + pub fn datagram_receive_buffer_size(&mut self, size: Option) -> &mut Self { + self.inner.datagram_receive_buffer_size(size); + self + } + + /// Set the datagram send buffer size + pub fn datagram_send_buffer_size(&mut self, size: usize) -> &mut Self { + self.inner.datagram_send_buffer_size(size); + self + } + + /// Get the inner Quinn TransportConfig + pub(crate) fn inner(&self) -> &quinn::TransportConfig { + &self.inner + } + + /// Convert to Quinn TransportConfig + pub fn into_inner(self) -> quinn::TransportConfig { + self.inner + } +} + +impl Default for QuicTransportConfig { + fn default() -> Self { + Self::new() + } +} + +/// QUIC Endpoint Configuration +#[frb(opaque)] +pub struct QuicEndpointConfig { + inner: quinn::EndpointConfig, +} + +impl QuicEndpointConfig { + /// Create a new endpoint config with default settings + pub fn new() -> Self { + Self { + inner: quinn::EndpointConfig::default(), + } + } + + /// Set the maximum UDP payload size + pub fn max_udp_payload_size(&mut self, size: u16) -> Result<(), String> { + self.inner + .max_udp_payload_size(size) + .map_err(|e| format!("Invalid UDP payload size: {:?}", e))?; + Ok(()) + } + + /// Get the current maximum UDP payload size + pub fn get_max_udp_payload_size(&self) -> u64 { + self.inner.get_max_udp_payload_size() + } + + /// Set supported QUIC versions + pub fn supported_versions(&mut self, versions: Vec) -> &mut Self { + self.inner.supported_versions(versions); + self + } + + /// Enable or disable QUIC bit greasing + pub fn grease_quic_bit(&mut self, enabled: bool) -> &mut Self { + self.inner.grease_quic_bit(enabled); + self + } + + /// Set the minimum reset interval + pub fn min_reset_interval(&mut self, interval_ms: u64) -> &mut Self { + self.inner.min_reset_interval(Duration::from_millis(interval_ms)); + self + } + + /// Set the RNG seed for deterministic behavior (testing only) + pub fn rng_seed(&mut self, seed: Option>) -> Result<(), String> { + let seed_array = if let Some(seed_vec) = seed { + if seed_vec.len() != 32 { + return Err("RNG seed must be exactly 32 bytes".to_string()); + } + let mut array = [0u8; 32]; + array.copy_from_slice(&seed_vec); + Some(array) + } else { + None + }; + + self.inner.rng_seed(seed_array); + Ok(()) + } + + /// Get the inner Quinn EndpointConfig + pub(crate) fn inner(&self) -> &quinn::EndpointConfig { + &self.inner + } + + /// Convert to Quinn EndpointConfig + pub fn into_inner(self) -> quinn::EndpointConfig { + self.inner + } +} + +impl Default for QuicEndpointConfig { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/core/connection.rs b/vendor/flutter_quic/rust/src/core/connection.rs new file mode 100644 index 0000000..a431e60 --- /dev/null +++ b/vendor/flutter_quic/rust/src/core/connection.rs @@ -0,0 +1,289 @@ +//! Core Connection API - Direct Quinn connection wrapper + +use flutter_rust_bridge::frb; +use crate::core::stream::{QuicSendStream, QuicRecvStream}; +use crate::errors::{QuicError, QuicDatagramException}; +use std::net::{SocketAddr, IpAddr}; +use std::time::Duration; + +#[derive(Debug)] +#[frb(opaque)] +pub struct QuicConnection { + inner: quinn::Connection, +} + +impl QuicConnection { + /// Create a new QuicConnection wrapping a Quinn connection + pub fn new(connection: quinn::Connection) -> Self { + Self { inner: connection } + } + + /// Open a bidirectional stream + pub async fn open_bi(&self) -> Result<(QuicSendStream, QuicRecvStream), QuicError> { + let (send_stream, recv_stream) = self.inner + .open_bi() + .await + .map_err(|e| QuicError::Connection(format!("Failed to open bidirectional stream: {:?}", e)))?; + + Ok((QuicSendStream::new(send_stream), QuicRecvStream::new(recv_stream))) + } + + /// Accept the next server-initiated bidirectional stream. + /// + /// Blocks until the remote peer opens a new bidirectional stream or the + /// connection is closed. Returns `None` when the connection has been + /// terminated (mirrors Quinn's `accept_bi()` returning + /// `ConnectionError::LocallyClosed` / `ConnectionError::Reset`). + pub async fn accept_bi(&self) -> Option<(QuicSendStream, QuicRecvStream)> { + match self.inner.accept_bi().await { + Ok((send_stream, recv_stream)) => { + Some((QuicSendStream::new(send_stream), QuicRecvStream::new(recv_stream))) + } + Err(_) => None, + } + } + + /// Open a unidirectional stream for sending + pub async fn open_uni(&self) -> Result { + let send_stream = self.inner + .open_uni() + .await + .map_err(|e| QuicError::Connection(format!("Failed to open unidirectional stream: {:?}", e)))?; + + Ok(QuicSendStream::new(send_stream)) + } + + // Datagram operations + + /// Send an unreliable datagram + pub fn send_datagram(&self, data: Vec) -> Result<(), QuicDatagramException> { + match self.inner.send_datagram(data.into()) { + Ok(()) => Ok(()), + Err(error) => { + // If the error is TooLarge, try to get the actual max size + if let quinn::SendDatagramError::TooLarge = error { + let max_size = self.inner.max_datagram_size().unwrap_or(0); + Err(QuicDatagramException::TooLarge { max_size }) + } else { + Err(QuicDatagramException::from(error)) + } + } + } + } + + /// Send a datagram, waiting for buffer space + pub async fn send_datagram_wait(&self, data: Vec) -> Result<(), QuicDatagramException> { + self.inner + .send_datagram_wait(data.into()) + .await + .map_err(QuicDatagramException::from) + } + + /// Read a datagram from the connection + pub async fn read_datagram(&self) -> Option> { + match self.inner.read_datagram().await { + Ok(datagram) => Some(datagram.to_vec()), + Err(_) => None, + } + } + + /// Get the space available in the datagram send buffer + pub fn datagram_send_buffer_space(&self) -> usize { + self.inner.datagram_send_buffer_space() + } + + /// Get the maximum datagram size + pub fn max_datagram_size(&self) -> Option { + self.inner.max_datagram_size() + } + + // Connection Info operations + + /// Get the remote address of the peer + pub fn remote_address(&self) -> SocketAddr { + self.inner.remote_address() + } + + /// Get the local IP address used for this connection + pub fn local_ip(&self) -> Option { + self.inner.local_ip() + } + + /// Get the current round-trip time estimate + pub fn rtt(&self) -> Duration { + self.inner.rtt() + } + + /// Get a stable identifier for this connection + pub fn stable_id(&self) -> usize { + self.inner.stable_id() + } + + /// Get the reason the connection was closed, if any + pub fn close_reason(&self) -> Option { + self.inner.close_reason().map(|error| format!("{:?}", error)) + } + + /// Get connection statistics + pub fn stats(&self) -> QuicConnectionStats { + let quinn_stats = self.inner.stats(); + QuicConnectionStats::from(quinn_stats) + } + + /// Get the peer's advertised transport parameters relevant to stream credits. + /// + /// Exposes fields of the remote peer's QUIC transport parameters that matter + /// for diagnosing `open_bi()` credit starvation, which upstream quinn 0.11 + /// does not surface publicly. Requires the patched vendored quinn/quinn-proto + /// in `vendor/flutter_quic/rust/vendor/`. + pub fn peer_transport_params(&self) -> QuicPeerTransportParams { + QuicPeerTransportParams { + initial_max_streams_bidi: self.inner.peer_params_initial_max_streams_bidi(), + initial_max_streams_uni: self.inner.peer_params_initial_max_streams_uni(), + initial_max_data: self.inner.peer_params_initial_max_data(), + } + } + + /// Get a reference to the inner Quinn connection + pub(crate) fn inner(&self) -> &quinn::Connection { + &self.inner + } +} + +/// Connection statistics from Quinn +#[derive(Debug, Clone)] +pub struct QuicConnectionStats { + pub path: QuicPathStats, + pub frame_tx: QuicFrameStats, + pub frame_rx: QuicFrameStats, + pub udp_tx: QuicUdpStats, + pub udp_rx: QuicUdpStats, +} + +/// Path-specific statistics +#[derive(Debug, Clone)] +pub struct QuicPathStats { + pub rtt_millis: u64, + pub cwnd: u64, + pub lost_packets: u64, + pub lost_bytes: u64, + pub sent_packets: u64, + pub congestion_events: u64, +} + +/// Frame transmission/reception statistics +#[derive(Debug, Clone)] +pub struct QuicFrameStats { + pub acks: u64, + pub crypto: u64, + pub connection_close: u64, + pub data_blocked: u64, + pub datagram: u64, + pub handshake_done: u64, + pub max_data: u64, + pub max_stream_data: u64, + pub max_streams_bidi: u64, + pub max_streams_uni: u64, + pub new_connection_id: u64, + pub new_token: u64, + pub path_challenge: u64, + pub path_response: u64, + pub ping: u64, + pub reset_stream: u64, + pub retire_connection_id: u64, + pub stream: u64, + pub stream_data_blocked: u64, + pub streams_blocked_bidi: u64, + pub streams_blocked_uni: u64, + pub stop_sending: u64, +} + +/// Peer-advertised QUIC transport parameters relevant to stream/data credits. +/// +/// These come from the remote peer's `transport_parameters` TLS extension and +/// represent the *initial* limits the peer granted us at handshake time. They +/// may be raised at runtime by `MAX_STREAMS` / `MAX_DATA` frames, which are +/// reflected in [`QuicFrameStats`] (`max_streams_bidi`, `max_streams_uni`, +/// `max_data`), not here. +#[derive(Debug, Clone)] +pub struct QuicPeerTransportParams { + /// Peer's `initial_max_streams_bidi`: the total number of client-initiated + /// bidirectional streams we may open before needing a `MAX_STREAMS` update. + pub initial_max_streams_bidi: u64, + /// Peer's `initial_max_streams_uni`. + pub initial_max_streams_uni: u64, + /// Peer's `initial_max_data` (connection-level flow-control window, bytes). + pub initial_max_data: u64, +} + +/// UDP-level statistics +#[derive(Debug, Clone)] +pub struct QuicUdpStats { + pub datagrams: u64, + pub bytes: u64, + pub ios: u64, +} + +impl From for QuicConnectionStats { + fn from(stats: quinn::ConnectionStats) -> Self { + Self { + path: QuicPathStats::from(stats.path), + frame_tx: QuicFrameStats::from(stats.frame_tx), + frame_rx: QuicFrameStats::from(stats.frame_rx), + udp_tx: QuicUdpStats::from(stats.udp_tx), + udp_rx: QuicUdpStats::from(stats.udp_rx), + } + } +} + +impl From for QuicPathStats { + fn from(stats: quinn::PathStats) -> Self { + Self { + rtt_millis: stats.rtt.as_millis() as u64, + cwnd: stats.cwnd, + lost_packets: stats.lost_packets, + lost_bytes: stats.lost_bytes, + sent_packets: stats.sent_packets, + congestion_events: stats.congestion_events, + } + } +} + +impl From for QuicFrameStats { + fn from(stats: quinn::FrameStats) -> Self { + Self { + acks: stats.acks, + crypto: stats.crypto, + connection_close: stats.connection_close, + data_blocked: stats.data_blocked, + datagram: stats.datagram, + handshake_done: stats.handshake_done as u64, // Convert u8 to u64 + max_data: stats.max_data, + max_stream_data: stats.max_stream_data, + max_streams_bidi: stats.max_streams_bidi, + max_streams_uni: stats.max_streams_uni, + new_connection_id: stats.new_connection_id, + new_token: stats.new_token, + path_challenge: stats.path_challenge, + path_response: stats.path_response, + ping: stats.ping, + reset_stream: stats.reset_stream, + retire_connection_id: stats.retire_connection_id, + stream: stats.stream, + stream_data_blocked: stats.stream_data_blocked, + streams_blocked_bidi: stats.streams_blocked_bidi, + streams_blocked_uni: stats.streams_blocked_uni, + stop_sending: stats.stop_sending, + } + } +} + +impl From for QuicUdpStats { + fn from(stats: quinn::UdpStats) -> Self { + Self { + datagrams: stats.datagrams, + bytes: stats.bytes, + ios: stats.ios, // Use ios instead of transmits + } + } +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/core/endpoint.rs b/vendor/flutter_quic/rust/src/core/endpoint.rs new file mode 100644 index 0000000..3834503 --- /dev/null +++ b/vendor/flutter_quic/rust/src/core/endpoint.rs @@ -0,0 +1,205 @@ +//! Core Endpoint API - Direct Quinn endpoint wrapper + +use flutter_rust_bridge::frb; +use crate::core::connection::QuicConnection; +use crate::errors::QuicError; +use std::net::{SocketAddr, Ipv4Addr, Ipv6Addr}; +use std::sync::Arc; +use std::time::Duration; + +#[frb(opaque)] +pub struct QuicEndpoint { + inner: quinn::Endpoint, +} + +impl QuicEndpoint { + /// Create a new server endpoint with the given configuration + pub fn server(config: crate::core::config::QuicServerConfig, addr: String) -> Result { + let rt = tokio::runtime::Runtime::new() + .map_err(|e| QuicError::Endpoint(format!("Failed to create runtime: {:?}", e)))?; + + rt.block_on(async { + let addr: SocketAddr = addr.parse() + .map_err(|e| QuicError::Config(format!("Invalid address: {:?}", e)))?; + + let endpoint = quinn::Endpoint::server(config.into_inner(), addr) + .map_err(|e| QuicError::Endpoint(format!("Failed to create server endpoint: {:?}", e)))?; + + Ok(Self { inner: endpoint }) + }) + } + + /// Create a new client endpoint with insecure configuration (for testing) + pub fn client() -> Result { + let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); + Self::client_with_bind_addr(bind_addr, None) + } + + /// Create a client endpoint suitable for the target remote address family. + pub fn client_for_remote(remote_addr: SocketAddr) -> Result { + Self::client_for_remote_with_qlog(remote_addr, None) + } + + /// Create a client endpoint suitable for the target remote address family, + /// optionally writing a qlog trace to `qlog_path`. + pub fn client_for_remote_with_qlog( + remote_addr: SocketAddr, + qlog_path: Option, + ) -> Result { + let bind_addr = if remote_addr.is_ipv6() { + SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0) + } else { + SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0) + }; + Self::client_with_bind_addr(bind_addr, qlog_path) + } + + fn client_with_bind_addr(bind_addr: SocketAddr, qlog_path: Option) -> Result { + // Ensure crypto provider is installed + if rustls::crypto::CryptoProvider::get_default().is_none() { + rustls::crypto::ring::default_provider() + .install_default() + .map_err(|_| QuicError::Config("Failed to install default crypto provider".to_string()))?; + } + + // Create insecure client config + let crypto = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new()) + .with_no_client_auth(); + let mut crypto = crypto; + crypto.alpn_protocols = vec![b"xmpp-client".to_vec()]; + + let mut config = quinn::ClientConfig::new(Arc::new( + quinn::crypto::rustls::QuicClientConfig::try_from(crypto) + .map_err(|e| QuicError::Config(format!("Failed to create QUIC client config: {:?}", e)))? + )); + + // Configure transport parameters for better performance + let mut transport = quinn::TransportConfig::default(); + // Per XEP-0467, the QUIC connection migration timeout SHOULD be set to + // the maximum 600 seconds, so we advertise that as our max_idle_timeout. + // The negotiated value will be the minimum of ours and the peer's. + let idle_timeout = quinn::IdleTimeout::try_from(Duration::from_secs(600)) + .map_err(|e| QuicError::Config(format!("Invalid idle timeout: {:?}", e)))?; + transport.max_idle_timeout(Some(idle_timeout)); + // Send a QUIC PING every 240 seconds to keep the connection alive. + // The negotiated idle timeout is min(ours=600s, server's=300s) = 300s, + // so 240s gives a comfortable margin without being chatty. In practice + // the XMPP-level pings (30s foreground / 5min background) will fire + // more often, but this ensures the QUIC transport layer itself never + // idles out even when the XMPP layer is quiet. + transport.keep_alive_interval(Some(Duration::from_secs(240))); + transport.max_concurrent_bidi_streams(25u32.into()); + transport.max_concurrent_uni_streams(25u32.into()); + + // If a qlog path was provided, open the file and attach a qlog stream + // so Quinn writes a full QUIC trace (transport events, stream opens, + // flow-control frames, etc.) to that file for offline analysis with + // tools like qvis (https://qvis.quictools.info/). + if let Some(ref path) = qlog_path { + match std::fs::File::create(path) { + Ok(file) => { + let mut qlog_config = quinn_proto::QlogConfig::default(); + qlog_config.writer(Box::new(file)); + qlog_config.title(Some("Wimsy QUIC trace".to_string())); + qlog_config.description(Some(format!("QUIC connection to {}", path))); + if let Some(stream) = qlog_config.into_stream() { + transport.qlog_stream(Some(stream)); + } + } + Err(e) => { + tracing::warn!("qlog: failed to create file {:?}: {}", path, e); + } + } + } + + config.transport_config(Arc::new(transport)); + + // Create endpoint with default socket + let mut endpoint = quinn::Endpoint::client(bind_addr) + .map_err(|e| QuicError::Endpoint(format!("Failed to create client endpoint: {:?}", e)))?; + + endpoint.set_default_client_config(config); + + Ok(Self { inner: endpoint }) + } + + /// Connect to a server + pub async fn connect(&self, addr: String, server_name: String) -> Result { + let addr: SocketAddr = addr.parse() + .map_err(|e| QuicError::Connection(format!("Invalid address: {:?}", e)))?; + + let connecting = self.inner.connect(addr, &server_name) + .map_err(|e| QuicError::Connection(format!("Failed to initiate connection: {:?}", e)))?; + + let connection = connecting.await + .map_err(|e| QuicError::Connection(format!("Failed to establish connection: {:?}", e)))?; + + Ok(QuicConnection::new(connection)) + } + + /// Get a reference to the inner Quinn endpoint + pub(crate) fn inner(&self) -> &quinn::Endpoint { + &self.inner + } +} + +/// Skip server certificate verification for testing purposes +#[derive(Debug)] +struct SkipServerVerification; + +impl SkipServerVerification { + fn new() -> Arc { + Arc::new(Self) + } +} + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA1, + rustls::SignatureScheme::ECDSA_SHA1_Legacy, + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::ECDSA_NISTP521_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::ED448, + ] + } +} diff --git a/vendor/flutter_quic/rust/src/core/mod.rs b/vendor/flutter_quic/rust/src/core/mod.rs new file mode 100644 index 0000000..a8b7d67 --- /dev/null +++ b/vendor/flutter_quic/rust/src/core/mod.rs @@ -0,0 +1,14 @@ +//! Core API - Direct 1:1 mapping to Quinn library +//! +//! This module provides complete access to Quinn's functionality with minimal abstraction. +//! Every public Quinn method is exposed here for maximum flexibility and power. + +pub mod endpoint; +pub mod connection; +pub mod stream; +pub mod config; + +pub use endpoint::QuicEndpoint; +pub use connection::{QuicConnection, QuicConnectionStats, QuicPathStats, QuicFrameStats, QuicUdpStats, QuicPeerTransportParams}; +pub use stream::{QuicSendStream, QuicRecvStream}; +pub use config::{QuicServerConfig, QuicTransportConfig, QuicEndpointConfig}; \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/core/stream.rs b/vendor/flutter_quic/rust/src/core/stream.rs new file mode 100644 index 0000000..09cefd7 --- /dev/null +++ b/vendor/flutter_quic/rust/src/core/stream.rs @@ -0,0 +1,172 @@ +//! Core Stream API - Direct Quinn stream wrapper + +use flutter_rust_bridge::frb; +use crate::errors::{QuicWriteException, QuicReadException, QuicReadToEndException}; + +#[frb(opaque)] +pub struct QuicSendStream { + inner: quinn::SendStream, +} + +impl QuicSendStream { + /// Create a new QuicSendStream wrapping a Quinn send stream + pub fn new(stream: quinn::SendStream) -> Self { + Self { inner: stream } + } + + /// Write data to the stream + /// + /// Returns the number of bytes written. + /// + /// # Arguments + /// + /// * `data` - The bytes to write to the stream + /// + /// # Returns + /// + /// The number of bytes actually written. This may be less than the length + /// of `data` if the stream's send buffer is full. + /// + /// # Errors + /// + /// Returns a `QuicWriteException` if the stream has been stopped by the peer + /// or the connection has been lost. + pub async fn write(&mut self, data: Vec) -> Result { + match self.inner.write(&data).await { + Ok(bytes_written) => Ok(bytes_written), + Err(write_error) => Err(QuicWriteException::from(write_error)), + } + } + + /// Write all data to the stream + /// + /// Ensures that all data is written to the stream or an error is returned. + /// Unlike `write()`, this method will continue writing until all data is sent + /// or an error occurs. + /// + /// # Arguments + /// + /// * `data` - The bytes to write to the stream + /// + /// # Errors + /// + /// Returns a `QuicWriteException` if the stream has been stopped by the peer + /// or the connection has been lost. + pub async fn write_all(&mut self, data: Vec) -> Result<(), QuicWriteException> { + match self.inner.write_all(&data).await { + Ok(()) => Ok(()), + Err(write_error) => Err(QuicWriteException::from(write_error)), + } + } + + /// Finish the stream + /// + /// Signals that no more data will be sent on this stream. This will cause + /// the receiving end to receive an end-of-stream signal after all + /// previously written data has been delivered. + /// + /// # Errors + /// + /// Returns a `QuicWriteException` if the stream has already been finished + /// or the connection has been lost. + pub fn finish(&mut self) -> Result<(), QuicWriteException> { + match self.inner.finish() { + Ok(()) => Ok(()), + Err(write_error) => Err(QuicWriteException::from(write_error)), + } + } + + /// Get a reference to the inner Quinn send stream + pub(crate) fn inner(&self) -> &quinn::SendStream { + &self.inner + } + + /// Get a mutable reference to the inner Quinn send stream + pub(crate) fn inner_mut(&mut self) -> &mut quinn::SendStream { + &mut self.inner + } +} + +#[frb(opaque)] +pub struct QuicRecvStream { + inner: quinn::RecvStream, +} + +impl QuicRecvStream { + /// Create a new QuicRecvStream wrapping a Quinn receive stream + pub fn new(stream: quinn::RecvStream) -> Self { + Self { inner: stream } + } + + /// Read data from the stream + /// + /// Attempts to read data from the stream into a buffer. + /// + /// # Arguments + /// + /// * `max_length` - Maximum number of bytes to read + /// + /// # Returns + /// + /// Returns `Some(Vec)` with the data read, or `None` if the stream + /// has been finished by the peer. The returned data may be less than + /// `max_length` bytes. + /// + /// # Errors + /// + /// Returns a `QuicReadException` if the stream has been reset by the peer, + /// the connection has been lost, or other read errors occur. + pub async fn read(&mut self, max_length: usize) -> Result>, QuicReadException> { + let mut buf = vec![0u8; max_length]; + match self.inner.read(&mut buf).await { + Ok(Some(bytes_read)) => { + buf.truncate(bytes_read); + Ok(Some(buf)) + } + Ok(None) => Ok(None), // Stream finished + Err(read_error) => Err(QuicReadException::from(read_error)), + } + } + + /// Read all remaining data from the stream + /// + /// Reads data until the stream is finished or the size limit is reached. + /// This method uses unordered reads internally for efficiency and is not + /// cancel-safe. + /// + /// # Arguments + /// + /// * `size_limit` - Maximum number of bytes to read to prevent memory exhaustion + /// + /// # Returns + /// + /// Returns all remaining data from the stream as a Vec. + /// + /// # Errors + /// + /// Returns a `QuicReadToEndException::TooLong` if the stream data exceeds + /// the size limit, or `QuicReadToEndException::Read` for other read errors. + pub async fn read_to_end(&mut self, size_limit: usize) -> Result, QuicReadToEndException> { + match self.inner.read_to_end(size_limit).await { + Ok(data) => Ok(data), + Err(read_to_end_error) => Err(QuicReadToEndException::from(read_to_end_error)), + } + } + + /// Get a reference to the inner Quinn receive stream + pub(crate) fn inner(&self) -> &quinn::RecvStream { + &self.inner + } + + /// Get a mutable reference to the inner Quinn receive stream + pub(crate) fn inner_mut(&mut self) -> &mut quinn::RecvStream { + &mut self.inner + } +} + +// Legacy QuicStream for compatibility +#[frb(opaque)] +pub struct QuicStream { + // This is kept for backward compatibility but not used in new API + _unused: (), +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/errors/mod.rs b/vendor/flutter_quic/rust/src/errors/mod.rs new file mode 100644 index 0000000..719c6eb --- /dev/null +++ b/vendor/flutter_quic/rust/src/errors/mod.rs @@ -0,0 +1,149 @@ +//! Error handling for Flutter QUIC +//! +//! Specific error types mapped from Quinn errors to provide +//! clear error handling in Dart applications. + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum QuicError { + #[error("Connection error: {0}")] + Connection(String), + + #[error("Endpoint error: {0}")] + Endpoint(String), + + #[error("Stream error: {0}")] + Stream(String), + + #[error("TLS error: {0}")] + Tls(String), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("Network error: {0}")] + Network(String), + + #[error("Write error: {0}")] + Write(String), +} + +/// Write operation errors when sending data on a QUIC stream +#[derive(Error, Debug)] +pub enum QuicWriteException { + #[error("Stream was stopped by peer with error code {0}")] + Stopped(u64), + + #[error("Connection was lost: {0}")] + ConnectionLost(String), +} + +impl From for QuicWriteException { + fn from(error: quinn::WriteError) -> Self { + match error { + quinn::WriteError::Stopped(code) => QuicWriteException::Stopped(code.into()), + quinn::WriteError::ConnectionLost(conn_err) => { + QuicWriteException::ConnectionLost(format!("{:?}", conn_err)) + } + quinn::WriteError::ClosedStream => { + QuicWriteException::ConnectionLost("Stream was closed".to_string()) + } + quinn::WriteError::ZeroRttRejected => { + QuicWriteException::ConnectionLost("0-RTT was rejected".to_string()) + } + } + } +} + +impl From for QuicWriteException { + fn from(_error: quinn::ClosedStream) -> Self { + QuicWriteException::ConnectionLost("Stream was already closed".to_string()) + } +} + +/// Read operation errors when receiving data from a QUIC stream +#[derive(Error, Debug)] +pub enum QuicReadException { + #[error("Stream was reset by peer with error code {0}")] + Reset(u64), + + #[error("Connection was lost: {0}")] + ConnectionLost(String), + + #[error("0-RTT was rejected")] + ZeroRttRejected, + + #[error("Stream was closed")] + ClosedStream, + + #[error("Illegal ordered read")] + IllegalOrderedRead, +} + +impl From for QuicReadException { + fn from(error: quinn::ReadError) -> Self { + match error { + quinn::ReadError::Reset(code) => QuicReadException::Reset(code.into()), + quinn::ReadError::ConnectionLost(conn_err) => { + QuicReadException::ConnectionLost(format!("{:?}", conn_err)) + } + quinn::ReadError::ZeroRttRejected => QuicReadException::ZeroRttRejected, + quinn::ReadError::ClosedStream => QuicReadException::ClosedStream, + quinn::ReadError::IllegalOrderedRead => QuicReadException::IllegalOrderedRead, + } + } +} + +/// Read-to-end operation errors when reading all remaining data from a QUIC stream +#[derive(Error, Debug)] +pub enum QuicReadToEndException { + #[error("Read error occurred: {0}")] + Read(#[from] QuicReadException), + + #[error("Stream data exceeds size limit")] + TooLong, +} + +impl From for QuicReadToEndException { + fn from(error: quinn::ReadToEndError) -> Self { + match error { + quinn::ReadToEndError::Read(read_error) => { + QuicReadToEndException::Read(QuicReadException::from(read_error)) + } + quinn::ReadToEndError::TooLong => QuicReadToEndException::TooLong, + } + } +} + +/// Datagram operation errors when sending unreliable datagrams +#[derive(Error, Debug)] +pub enum QuicDatagramException { + #[error("Datagrams are not supported by the peer")] + UnsupportedByPeer, + + #[error("Datagram is too large (max size: {max_size})")] + TooLarge { max_size: usize }, + + #[error("Connection was lost: {0}")] + ConnectionLost(String), +} + +impl From for QuicDatagramException { + fn from(error: quinn::SendDatagramError) -> Self { + match error { + quinn::SendDatagramError::UnsupportedByPeer => QuicDatagramException::UnsupportedByPeer, + quinn::SendDatagramError::Disabled => QuicDatagramException::UnsupportedByPeer, + quinn::SendDatagramError::TooLarge => { + // We don't have access to the actual max size from Quinn error, + // so we'll set a reasonable default that the caller can override + QuicDatagramException::TooLarge { max_size: 0 } + } + quinn::SendDatagramError::ConnectionLost(conn_err) => { + QuicDatagramException::ConnectionLost(format!("{:?}", conn_err)) + } + } + } +} + + \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/frb_generated.rs b/vendor/flutter_quic/rust/src/frb_generated.rs new file mode 100644 index 0000000..3cbf129 --- /dev/null +++ b/vendor/flutter_quic/rust/src/frb_generated.rs @@ -0,0 +1,3996 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::needless_borrow +)] + +// Section: imports + +use crate::convenience::client::*; +use crate::core::config::*; +use crate::core::connection::*; +use crate::core::endpoint::*; +use crate::core::stream::*; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 421219948; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__api__bridge___expose_connection_type_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "_expose_connection_type", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::_expose_connection_type(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge___expose_quic_client_config_type_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "_expose_quic_client_config_type", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_config = + ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::_expose_quic_client_config_type(api_config), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge___expose_quic_client_type_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "_expose_quic_client_type", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::_expose_quic_client_type(api_client), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge___expose_recv_stream_type_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "_expose_recv_stream_type", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::_expose_recv_stream_type(api_stream), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge___expose_send_stream_type_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "_expose_send_stream_type", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::_expose_send_stream_type(api_stream), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge___expose_types_for_frb_generation_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "_expose_types_for_frb_generation", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::bridge::_expose_types_for_frb_generation(); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_accept_bi_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_accept_bi", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let mut api_connection_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_connection, + 0, + false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_connection_guard = + Some(api_connection.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_connection_guard = api_connection_guard.unwrap(); + let output_ok = + crate::api::bridge::connection_accept_bi(&*api_connection_guard) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__connection_close_reason_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_close_reason", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_close_reason(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_datagram_send_buffer_space_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_datagram_send_buffer_space", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_datagram_send_buffer_space(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_local_ip_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_local_ip", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::bridge::connection_local_ip( + api_connection, + ))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_max_datagram_size_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_max_datagram_size", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_max_datagram_size(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_open_bi_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_open_bi", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = + crate::api::bridge::connection_open_bi(api_connection).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__connection_open_uni_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_open_uni", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = + crate::api::bridge::connection_open_uni(api_connection).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__connection_peer_transport_params_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_peer_transport_params", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_peer_transport_params(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_read_datagram_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_read_datagram", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_read_datagram(api_connection).await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__connection_remote_address_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_remote_address", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_remote_address(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_rtt_millis_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_rtt_millis", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::connection_rtt_millis(api_connection), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_send_datagram_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_send_datagram", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::errors::QuicDatagramException>((move || { + let output_ok = + crate::api::bridge::connection_send_datagram(api_connection, api_data)?; + Ok(output_ok) + })( + )) + } + }, + ) +} +fn wire__crate__api__bridge__connection_send_datagram_wait_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_send_datagram_wait", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicDatagramException>( + (move || async move { + let output_ok = crate::api::bridge::connection_send_datagram_wait( + api_connection, + api_data, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__connection_stable_id_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_stable_id", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::bridge::connection_stable_id( + api_connection, + ))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__connection_stats_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "connection_stats", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_connection = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::bridge::connection_stats(api_connection))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__create_client_endpoint_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_client_endpoint", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::errors::QuicError>((move || { + let output_ok = crate::api::bridge::create_client_endpoint()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__create_server_endpoint_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_server_endpoint", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_config = ::sse_decode(&mut deserializer); + let api_addr = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::errors::QuicError>((move || { + let output_ok = + crate::api::bridge::create_server_endpoint(api_config, api_addr)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__endpoint_config_new_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "endpoint_config_new", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::bridge::endpoint_config_new())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__endpoint_connect_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "endpoint_connect", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api__endpoint = ::sse_decode(&mut deserializer); + let api_addr = ::sse_decode(&mut deserializer); + let api_server_name = ::sse_decode(&mut deserializer); + let api_qlog_path = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = crate::api::bridge::endpoint_connect( + api__endpoint, + api_addr, + api_server_name, + api_qlog_path, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__init_app_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "init_app", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::bridge::init_app(); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_clear_pool_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_clear_pool", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::bridge::quic_client_clear_pool(api_client), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_config_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_config", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::bridge::quic_client_config(api_client))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_config_new_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_config_new", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::bridge::quic_client_config_new())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_create_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_create", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::errors::QuicError>((move || { + let output_ok = crate::api::bridge::quic_client_create()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_create_with_config_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_create_with_config", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_config = + ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::errors::QuicError>((move || { + let output_ok = crate::api::bridge::quic_client_create_with_config(api_config)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_get_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_get", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + let api_url = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = + crate::api::bridge::quic_client_get(api_client, api_url).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_get_with_timeout_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_get_with_timeout", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + let api_url = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = + crate::api::bridge::quic_client_get_with_timeout(api_client, api_url) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_post_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_post", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + let api_url = ::sse_decode(&mut deserializer); + let api_data = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = + crate::api::bridge::quic_client_post(api_client, api_url, api_data) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_post_with_timeout_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_post_with_timeout", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + let api_url = ::sse_decode(&mut deserializer); + let api_data = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = crate::api::bridge::quic_client_post_with_timeout( + api_client, api_url, api_data, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_send_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_send", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + let api_url = ::sse_decode(&mut deserializer); + let api_data = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = + crate::api::bridge::quic_client_send(api_client, api_url, api_data) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__quic_client_send_with_timeout_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "quic_client_send_with_timeout", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_client = ::sse_decode(&mut deserializer); + let api_url = ::sse_decode(&mut deserializer); + let api_data = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicError>( + (move || async move { + let output_ok = crate::api::bridge::quic_client_send_with_timeout( + api_client, api_url, api_data, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__recv_stream_read_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "recv_stream_read", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + let api_max_length = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicReadException>( + (move || async move { + let output_ok = + crate::api::bridge::recv_stream_read(api_stream, api_max_length) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__recv_stream_read_to_end_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "recv_stream_read_to_end", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + let api_max_length = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicReadToEndException>( + (move || async move { + let output_ok = + crate::api::bridge::recv_stream_read_to_end(api_stream, api_max_length) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__send_stream_finish_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "send_stream_finish", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, crate::errors::QuicWriteException>((move || { + let output_ok = crate::api::bridge::send_stream_finish(api_stream)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__send_stream_write_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "send_stream_write", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicWriteException>( + (move || async move { + let output_ok = + crate::api::bridge::send_stream_write(api_stream, api_data).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__send_stream_write_all_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "send_stream_write_all", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream = ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::errors::QuicWriteException>( + (move || async move { + let output_ok = + crate::api::bridge::send_stream_write_all(api_stream, api_data).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__bridge__server_config_with_single_cert_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "server_config_with_single_cert", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_cert_chain = >>::sse_decode(&mut deserializer); + let api_key = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::bridge::server_config_with_single_cert( + api_cert_chain, + api_key, + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__bridge__transport_config_new_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "transport_config_new", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::bridge::transport_config_new())?; + Ok(output_ok) + })()) + } + }, + ) +} + +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for QuicClient { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicConnection { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicEndpoint { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicEndpointConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicRecvStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicSendStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicServerConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for QuicTransportConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for Vec> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(>::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for crate::convenience::client::QuicClientConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_maxConnectionsPerHost = ::sse_decode(deserializer); + let mut var_connectTimeoutMs = ::sse_decode(deserializer); + let mut var_requestTimeoutMs = ::sse_decode(deserializer); + let mut var_retryAttempts = ::sse_decode(deserializer); + let mut var_retryDelayMs = ::sse_decode(deserializer); + let mut var_keepAliveTimeoutMs = ::sse_decode(deserializer); + return crate::convenience::client::QuicClientConfig { + max_connections_per_host: var_maxConnectionsPerHost, + connect_timeout_ms: var_connectTimeoutMs, + request_timeout_ms: var_requestTimeoutMs, + retry_attempts: var_retryAttempts, + retry_delay_ms: var_retryDelayMs, + keep_alive_timeout_ms: var_keepAliveTimeoutMs, + }; + } +} + +impl SseDecode for crate::core::connection::QuicConnectionStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_path = ::sse_decode(deserializer); + let mut var_frameTx = ::sse_decode(deserializer); + let mut var_frameRx = ::sse_decode(deserializer); + let mut var_udpTx = ::sse_decode(deserializer); + let mut var_udpRx = ::sse_decode(deserializer); + return crate::core::connection::QuicConnectionStats { + path: var_path, + frame_tx: var_frameTx, + frame_rx: var_frameRx, + udp_tx: var_udpTx, + udp_rx: var_udpRx, + }; + } +} + +impl SseDecode for crate::errors::QuicDatagramException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + return crate::errors::QuicDatagramException::UnsupportedByPeer; + } + 1 => { + let mut var_maxSize = ::sse_decode(deserializer); + return crate::errors::QuicDatagramException::TooLarge { + max_size: var_maxSize, + }; + } + 2 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicDatagramException::ConnectionLost(var_field0); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::errors::QuicError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Connection(var_field0); + } + 1 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Endpoint(var_field0); + } + 2 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Stream(var_field0); + } + 3 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Tls(var_field0); + } + 4 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Config(var_field0); + } + 5 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Network(var_field0); + } + 6 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicError::Write(var_field0); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::core::connection::QuicFrameStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_acks = ::sse_decode(deserializer); + let mut var_crypto = ::sse_decode(deserializer); + let mut var_connectionClose = ::sse_decode(deserializer); + let mut var_dataBlocked = ::sse_decode(deserializer); + let mut var_datagram = ::sse_decode(deserializer); + let mut var_handshakeDone = ::sse_decode(deserializer); + let mut var_maxData = ::sse_decode(deserializer); + let mut var_maxStreamData = ::sse_decode(deserializer); + let mut var_maxStreamsBidi = ::sse_decode(deserializer); + let mut var_maxStreamsUni = ::sse_decode(deserializer); + let mut var_newConnectionId = ::sse_decode(deserializer); + let mut var_newToken = ::sse_decode(deserializer); + let mut var_pathChallenge = ::sse_decode(deserializer); + let mut var_pathResponse = ::sse_decode(deserializer); + let mut var_ping = ::sse_decode(deserializer); + let mut var_resetStream = ::sse_decode(deserializer); + let mut var_retireConnectionId = ::sse_decode(deserializer); + let mut var_stream = ::sse_decode(deserializer); + let mut var_streamDataBlocked = ::sse_decode(deserializer); + let mut var_streamsBlockedBidi = ::sse_decode(deserializer); + let mut var_streamsBlockedUni = ::sse_decode(deserializer); + let mut var_stopSending = ::sse_decode(deserializer); + return crate::core::connection::QuicFrameStats { + acks: var_acks, + crypto: var_crypto, + connection_close: var_connectionClose, + data_blocked: var_dataBlocked, + datagram: var_datagram, + handshake_done: var_handshakeDone, + max_data: var_maxData, + max_stream_data: var_maxStreamData, + max_streams_bidi: var_maxStreamsBidi, + max_streams_uni: var_maxStreamsUni, + new_connection_id: var_newConnectionId, + new_token: var_newToken, + path_challenge: var_pathChallenge, + path_response: var_pathResponse, + ping: var_ping, + reset_stream: var_resetStream, + retire_connection_id: var_retireConnectionId, + stream: var_stream, + stream_data_blocked: var_streamDataBlocked, + streams_blocked_bidi: var_streamsBlockedBidi, + streams_blocked_uni: var_streamsBlockedUni, + stop_sending: var_stopSending, + }; + } +} + +impl SseDecode for crate::core::connection::QuicPathStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_rttMillis = ::sse_decode(deserializer); + let mut var_cwnd = ::sse_decode(deserializer); + let mut var_lostPackets = ::sse_decode(deserializer); + let mut var_lostBytes = ::sse_decode(deserializer); + let mut var_sentPackets = ::sse_decode(deserializer); + let mut var_congestionEvents = ::sse_decode(deserializer); + return crate::core::connection::QuicPathStats { + rtt_millis: var_rttMillis, + cwnd: var_cwnd, + lost_packets: var_lostPackets, + lost_bytes: var_lostBytes, + sent_packets: var_sentPackets, + congestion_events: var_congestionEvents, + }; + } +} + +impl SseDecode for crate::core::connection::QuicPeerTransportParams { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_initialMaxStreamsBidi = ::sse_decode(deserializer); + let mut var_initialMaxStreamsUni = ::sse_decode(deserializer); + let mut var_initialMaxData = ::sse_decode(deserializer); + return crate::core::connection::QuicPeerTransportParams { + initial_max_streams_bidi: var_initialMaxStreamsBidi, + initial_max_streams_uni: var_initialMaxStreamsUni, + initial_max_data: var_initialMaxData, + }; + } +} + +impl SseDecode for crate::errors::QuicReadException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicReadException::Reset(var_field0); + } + 1 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicReadException::ConnectionLost(var_field0); + } + 2 => { + return crate::errors::QuicReadException::ZeroRttRejected; + } + 3 => { + return crate::errors::QuicReadException::ClosedStream; + } + 4 => { + return crate::errors::QuicReadException::IllegalOrderedRead; + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::errors::QuicReadToEndException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicReadToEndException::Read(var_field0); + } + 1 => { + return crate::errors::QuicReadToEndException::TooLong; + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::core::connection::QuicUdpStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_datagrams = ::sse_decode(deserializer); + let mut var_bytes = ::sse_decode(deserializer); + let mut var_ios = ::sse_decode(deserializer); + return crate::core::connection::QuicUdpStats { + datagrams: var_datagrams, + bytes: var_bytes, + ios: var_ios, + }; + } +} + +impl SseDecode for crate::errors::QuicWriteException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicWriteException::Stopped(var_field0); + } + 1 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::errors::QuicWriteException::ConnectionLost(var_field0); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for (QuicClient, crate::convenience::client::QuicClientConfig) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = + ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicClient, String) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, QuicSendStream) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, QuicSendStream, QuicRecvStream) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + let mut var_field2 = ::sse_decode(deserializer); + return (var_field0, var_field1, var_field2); + } +} + +impl SseDecode for (QuicConnection, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, Option>) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >>::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, crate::core::connection::QuicConnectionStats) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = + ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode + for ( + QuicConnection, + crate::core::connection::QuicPeerTransportParams, + ) +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = + ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, crate::models::types::SocketAddress) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, u64) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicConnection, usize) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicEndpoint, QuicConnection) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicRecvStream, Vec) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicRecvStream, Option>) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >>::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (QuicSendStream, usize) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (Option, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = >::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for crate::models::types::SocketAddress { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_ip = ::sse_decode(deserializer); + let mut var_port = ::sse_decode(deserializer); + return crate::models::types::SocketAddress { + ip: var_ip, + port: var_port, + }; + } +} + +impl SseDecode for u16 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u16::().unwrap() + } +} + +impl SseDecode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u32::().unwrap() + } +} + +impl SseDecode for u64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__bridge___expose_connection_type_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 2 => wire__crate__api__bridge___expose_quic_client_config_type_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 3 => wire__crate__api__bridge___expose_quic_client_type_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 4 => wire__crate__api__bridge___expose_recv_stream_type_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 5 => wire__crate__api__bridge___expose_send_stream_type_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 6 => wire__crate__api__bridge___expose_types_for_frb_generation_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 7 => wire__crate__api__bridge__connection_accept_bi_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__bridge__connection_close_reason_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 9 => wire__crate__api__bridge__connection_datagram_send_buffer_space_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 10 => wire__crate__api__bridge__connection_local_ip_impl(port, ptr, rust_vec_len, data_len), + 11 => wire__crate__api__bridge__connection_max_datagram_size_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 12 => wire__crate__api__bridge__connection_open_bi_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__api__bridge__connection_open_uni_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__api__bridge__connection_peer_transport_params_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 15 => wire__crate__api__bridge__connection_read_datagram_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 16 => wire__crate__api__bridge__connection_remote_address_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 17 => { + wire__crate__api__bridge__connection_rtt_millis_impl(port, ptr, rust_vec_len, data_len) + } + 18 => wire__crate__api__bridge__connection_send_datagram_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 19 => wire__crate__api__bridge__connection_send_datagram_wait_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 20 => { + wire__crate__api__bridge__connection_stable_id_impl(port, ptr, rust_vec_len, data_len) + } + 21 => wire__crate__api__bridge__connection_stats_impl(port, ptr, rust_vec_len, data_len), + 22 => { + wire__crate__api__bridge__create_client_endpoint_impl(port, ptr, rust_vec_len, data_len) + } + 23 => { + wire__crate__api__bridge__create_server_endpoint_impl(port, ptr, rust_vec_len, data_len) + } + 24 => wire__crate__api__bridge__endpoint_config_new_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__bridge__endpoint_connect_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__bridge__init_app_impl(port, ptr, rust_vec_len, data_len), + 27 => { + wire__crate__api__bridge__quic_client_clear_pool_impl(port, ptr, rust_vec_len, data_len) + } + 28 => wire__crate__api__bridge__quic_client_config_impl(port, ptr, rust_vec_len, data_len), + 29 => { + wire__crate__api__bridge__quic_client_config_new_impl(port, ptr, rust_vec_len, data_len) + } + 30 => wire__crate__api__bridge__quic_client_create_impl(port, ptr, rust_vec_len, data_len), + 31 => wire__crate__api__bridge__quic_client_create_with_config_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 32 => wire__crate__api__bridge__quic_client_get_impl(port, ptr, rust_vec_len, data_len), + 33 => wire__crate__api__bridge__quic_client_get_with_timeout_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 34 => wire__crate__api__bridge__quic_client_post_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__bridge__quic_client_post_with_timeout_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 36 => wire__crate__api__bridge__quic_client_send_impl(port, ptr, rust_vec_len, data_len), + 37 => wire__crate__api__bridge__quic_client_send_with_timeout_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 38 => wire__crate__api__bridge__recv_stream_read_impl(port, ptr, rust_vec_len, data_len), + 39 => wire__crate__api__bridge__recv_stream_read_to_end_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 40 => wire__crate__api__bridge__send_stream_finish_impl(port, ptr, rust_vec_len, data_len), + 41 => wire__crate__api__bridge__send_stream_write_impl(port, ptr, rust_vec_len, data_len), + 42 => { + wire__crate__api__bridge__send_stream_write_all_impl(port, ptr, rust_vec_len, data_len) + } + 43 => wire__crate__api__bridge__server_config_with_single_cert_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 44 => { + wire__crate__api__bridge__transport_config_new_impl(port, ptr, rust_vec_len, data_len) + } + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for QuicClient { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for QuicConnection { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for QuicEndpoint { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} + +impl flutter_rust_bridge::IntoIntoDart> for QuicEndpointConfig { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for QuicRecvStream { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for QuicSendStream { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for QuicServerConfig { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} + +impl flutter_rust_bridge::IntoIntoDart> for QuicTransportConfig { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::convenience::client::QuicClientConfig { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.max_connections_per_host.into_into_dart().into_dart(), + self.connect_timeout_ms.into_into_dart().into_dart(), + self.request_timeout_ms.into_into_dart().into_dart(), + self.retry_attempts.into_into_dart().into_dart(), + self.retry_delay_ms.into_into_dart().into_dart(), + self.keep_alive_timeout_ms.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::convenience::client::QuicClientConfig +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::convenience::client::QuicClientConfig +{ + fn into_into_dart(self) -> crate::convenience::client::QuicClientConfig { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::core::connection::QuicConnectionStats { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.path.into_into_dart().into_dart(), + self.frame_tx.into_into_dart().into_dart(), + self.frame_rx.into_into_dart().into_dart(), + self.udp_tx.into_into_dart().into_dart(), + self.udp_rx.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::core::connection::QuicConnectionStats +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::core::connection::QuicConnectionStats +{ + fn into_into_dart(self) -> crate::core::connection::QuicConnectionStats { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::errors::QuicDatagramException { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::errors::QuicDatagramException::UnsupportedByPeer => [0.into_dart()].into_dart(), + crate::errors::QuicDatagramException::TooLarge { max_size } => { + [1.into_dart(), max_size.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicDatagramException::ConnectionLost(field0) => { + [2.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::errors::QuicDatagramException +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::errors::QuicDatagramException +{ + fn into_into_dart(self) -> crate::errors::QuicDatagramException { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::errors::QuicError { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::errors::QuicError::Connection(field0) => { + [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicError::Endpoint(field0) => { + [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicError::Stream(field0) => { + [2.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicError::Tls(field0) => { + [3.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicError::Config(field0) => { + [4.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicError::Network(field0) => { + [5.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicError::Write(field0) => { + [6.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::errors::QuicError {} +impl flutter_rust_bridge::IntoIntoDart for crate::errors::QuicError { + fn into_into_dart(self) -> crate::errors::QuicError { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::core::connection::QuicFrameStats { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.acks.into_into_dart().into_dart(), + self.crypto.into_into_dart().into_dart(), + self.connection_close.into_into_dart().into_dart(), + self.data_blocked.into_into_dart().into_dart(), + self.datagram.into_into_dart().into_dart(), + self.handshake_done.into_into_dart().into_dart(), + self.max_data.into_into_dart().into_dart(), + self.max_stream_data.into_into_dart().into_dart(), + self.max_streams_bidi.into_into_dart().into_dart(), + self.max_streams_uni.into_into_dart().into_dart(), + self.new_connection_id.into_into_dart().into_dart(), + self.new_token.into_into_dart().into_dart(), + self.path_challenge.into_into_dart().into_dart(), + self.path_response.into_into_dart().into_dart(), + self.ping.into_into_dart().into_dart(), + self.reset_stream.into_into_dart().into_dart(), + self.retire_connection_id.into_into_dart().into_dart(), + self.stream.into_into_dart().into_dart(), + self.stream_data_blocked.into_into_dart().into_dart(), + self.streams_blocked_bidi.into_into_dart().into_dart(), + self.streams_blocked_uni.into_into_dart().into_dart(), + self.stop_sending.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::core::connection::QuicFrameStats +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::core::connection::QuicFrameStats +{ + fn into_into_dart(self) -> crate::core::connection::QuicFrameStats { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::core::connection::QuicPathStats { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.rtt_millis.into_into_dart().into_dart(), + self.cwnd.into_into_dart().into_dart(), + self.lost_packets.into_into_dart().into_dart(), + self.lost_bytes.into_into_dart().into_dart(), + self.sent_packets.into_into_dart().into_dart(), + self.congestion_events.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::core::connection::QuicPathStats +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::core::connection::QuicPathStats +{ + fn into_into_dart(self) -> crate::core::connection::QuicPathStats { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::core::connection::QuicPeerTransportParams { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.initial_max_streams_bidi.into_into_dart().into_dart(), + self.initial_max_streams_uni.into_into_dart().into_dart(), + self.initial_max_data.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::core::connection::QuicPeerTransportParams +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::core::connection::QuicPeerTransportParams +{ + fn into_into_dart(self) -> crate::core::connection::QuicPeerTransportParams { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::errors::QuicReadException { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::errors::QuicReadException::Reset(field0) => { + [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicReadException::ConnectionLost(field0) => { + [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicReadException::ZeroRttRejected => [2.into_dart()].into_dart(), + crate::errors::QuicReadException::ClosedStream => [3.into_dart()].into_dart(), + crate::errors::QuicReadException::IllegalOrderedRead => [4.into_dart()].into_dart(), + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::errors::QuicReadException +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::errors::QuicReadException +{ + fn into_into_dart(self) -> crate::errors::QuicReadException { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::errors::QuicReadToEndException { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::errors::QuicReadToEndException::Read(field0) => { + [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicReadToEndException::TooLong => [1.into_dart()].into_dart(), + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::errors::QuicReadToEndException +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::errors::QuicReadToEndException +{ + fn into_into_dart(self) -> crate::errors::QuicReadToEndException { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::core::connection::QuicUdpStats { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.datagrams.into_into_dart().into_dart(), + self.bytes.into_into_dart().into_dart(), + self.ios.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::core::connection::QuicUdpStats +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::core::connection::QuicUdpStats +{ + fn into_into_dart(self) -> crate::core::connection::QuicUdpStats { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::errors::QuicWriteException { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::errors::QuicWriteException::Stopped(field0) => { + [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::errors::QuicWriteException::ConnectionLost(field0) => { + [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::errors::QuicWriteException +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::errors::QuicWriteException +{ + fn into_into_dart(self) -> crate::errors::QuicWriteException { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::models::types::SocketAddress { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.ip.into_into_dart().into_dart(), + self.port.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::models::types::SocketAddress +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::models::types::SocketAddress +{ + fn into_into_dart(self) -> crate::models::types::SocketAddress { + self + } +} + +impl SseEncode for QuicClient { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicConnection { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicEndpoint { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicEndpointConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicRecvStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicSendStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicServerConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for QuicTransportConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for Vec> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + >::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } + } +} + +impl SseEncode for crate::convenience::client::QuicClientConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.max_connections_per_host, serializer); + ::sse_encode(self.connect_timeout_ms, serializer); + ::sse_encode(self.request_timeout_ms, serializer); + ::sse_encode(self.retry_attempts, serializer); + ::sse_encode(self.retry_delay_ms, serializer); + ::sse_encode(self.keep_alive_timeout_ms, serializer); + } +} + +impl SseEncode for crate::core::connection::QuicConnectionStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.path, serializer); + ::sse_encode(self.frame_tx, serializer); + ::sse_encode(self.frame_rx, serializer); + ::sse_encode(self.udp_tx, serializer); + ::sse_encode(self.udp_rx, serializer); + } +} + +impl SseEncode for crate::errors::QuicDatagramException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::errors::QuicDatagramException::UnsupportedByPeer => { + ::sse_encode(0, serializer); + } + crate::errors::QuicDatagramException::TooLarge { max_size } => { + ::sse_encode(1, serializer); + ::sse_encode(max_size, serializer); + } + crate::errors::QuicDatagramException::ConnectionLost(field0) => { + ::sse_encode(2, serializer); + ::sse_encode(field0, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::errors::QuicError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::errors::QuicError::Connection(field0) => { + ::sse_encode(0, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicError::Endpoint(field0) => { + ::sse_encode(1, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicError::Stream(field0) => { + ::sse_encode(2, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicError::Tls(field0) => { + ::sse_encode(3, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicError::Config(field0) => { + ::sse_encode(4, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicError::Network(field0) => { + ::sse_encode(5, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicError::Write(field0) => { + ::sse_encode(6, serializer); + ::sse_encode(field0, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::core::connection::QuicFrameStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.acks, serializer); + ::sse_encode(self.crypto, serializer); + ::sse_encode(self.connection_close, serializer); + ::sse_encode(self.data_blocked, serializer); + ::sse_encode(self.datagram, serializer); + ::sse_encode(self.handshake_done, serializer); + ::sse_encode(self.max_data, serializer); + ::sse_encode(self.max_stream_data, serializer); + ::sse_encode(self.max_streams_bidi, serializer); + ::sse_encode(self.max_streams_uni, serializer); + ::sse_encode(self.new_connection_id, serializer); + ::sse_encode(self.new_token, serializer); + ::sse_encode(self.path_challenge, serializer); + ::sse_encode(self.path_response, serializer); + ::sse_encode(self.ping, serializer); + ::sse_encode(self.reset_stream, serializer); + ::sse_encode(self.retire_connection_id, serializer); + ::sse_encode(self.stream, serializer); + ::sse_encode(self.stream_data_blocked, serializer); + ::sse_encode(self.streams_blocked_bidi, serializer); + ::sse_encode(self.streams_blocked_uni, serializer); + ::sse_encode(self.stop_sending, serializer); + } +} + +impl SseEncode for crate::core::connection::QuicPathStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.rtt_millis, serializer); + ::sse_encode(self.cwnd, serializer); + ::sse_encode(self.lost_packets, serializer); + ::sse_encode(self.lost_bytes, serializer); + ::sse_encode(self.sent_packets, serializer); + ::sse_encode(self.congestion_events, serializer); + } +} + +impl SseEncode for crate::core::connection::QuicPeerTransportParams { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.initial_max_streams_bidi, serializer); + ::sse_encode(self.initial_max_streams_uni, serializer); + ::sse_encode(self.initial_max_data, serializer); + } +} + +impl SseEncode for crate::errors::QuicReadException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::errors::QuicReadException::Reset(field0) => { + ::sse_encode(0, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicReadException::ConnectionLost(field0) => { + ::sse_encode(1, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicReadException::ZeroRttRejected => { + ::sse_encode(2, serializer); + } + crate::errors::QuicReadException::ClosedStream => { + ::sse_encode(3, serializer); + } + crate::errors::QuicReadException::IllegalOrderedRead => { + ::sse_encode(4, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::errors::QuicReadToEndException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::errors::QuicReadToEndException::Read(field0) => { + ::sse_encode(0, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicReadToEndException::TooLong => { + ::sse_encode(1, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::core::connection::QuicUdpStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.datagrams, serializer); + ::sse_encode(self.bytes, serializer); + ::sse_encode(self.ios, serializer); + } +} + +impl SseEncode for crate::errors::QuicWriteException { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::errors::QuicWriteException::Stopped(field0) => { + ::sse_encode(0, serializer); + ::sse_encode(field0, serializer); + } + crate::errors::QuicWriteException::ConnectionLost(field0) => { + ::sse_encode(1, serializer); + ::sse_encode(field0, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for (QuicClient, crate::convenience::client::QuicClientConfig) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicClient, String) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, QuicSendStream) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, QuicSendStream, QuicRecvStream) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + ::sse_encode(self.2, serializer); + } +} + +impl SseEncode for (QuicConnection, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, Option>) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + >>::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, crate::core::connection::QuicConnectionStats) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode + for ( + QuicConnection, + crate::core::connection::QuicPeerTransportParams, + ) +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, crate::models::types::SocketAddress) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, u64) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicConnection, usize) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicEndpoint, QuicConnection) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicRecvStream, Vec) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicRecvStream, Option>) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + >>::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (QuicSendStream, usize) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (Option, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + } +} + +impl SseEncode for crate::models::types::SocketAddress { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.ip, serializer); + ::sse_encode(self.port, serializer); + } +} + +impl SseEncode for u16 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u16::(self).unwrap(); + } +} + +impl SseEncode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u32::(self).unwrap(); + } +} + +impl SseEncode for u64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u64::(self).unwrap(); + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.11.1. + + // Section: imports + + use super::*; + use crate::convenience::client::*; + use crate::core::config::*; + use crate::core::connection::*; + use crate::core::endpoint::*; + use crate::core::stream::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_flutter_quic_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.11.1. + + // Section: imports + + use super::*; + use crate::convenience::client::*; + use crate::core::config::*; + use crate::core::connection::*; + use crate::core::endpoint::*; + use crate::core::stream::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicConnection( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpoint( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicEndpointConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicRecvStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicSendStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicServerConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerQuicTransportConfig( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/vendor/flutter_quic/rust/src/lib.rs b/vendor/flutter_quic/rust/src/lib.rs new file mode 100644 index 0000000..f1965f6 --- /dev/null +++ b/vendor/flutter_quic/rust/src/lib.rs @@ -0,0 +1,69 @@ + + +pub mod api; +pub mod core; +pub mod convenience; +pub mod models; +pub mod errors; +mod frb_generated; + +// Re-export convenience API for easy access +pub use convenience::{QuicClient, QuicClientConfig}; + +#[cfg(test)] +mod tests { + use super::core::{QuicEndpoint, QuicConnection}; + use tokio; + + #[tokio::test] + async fn test_phase1_basic_endpoint_creation() { + // Test Task 1.2: QuicEndpoint.client() + let endpoint = QuicEndpoint::client().expect("Failed to create client endpoint"); + + // Basic validation - endpoint should be created successfully + println!("✅ QuicEndpoint.client() - endpoint created successfully"); + } + + #[tokio::test] + async fn test_phase1_connection_and_streams() { + // This test would require a real QUIC server + // For now, we'll test the endpoint creation and show the connection flow + + let endpoint = QuicEndpoint::client().expect("Failed to create client endpoint"); + + // Test connection attempt (will fail without real server, but validates API) + let result = endpoint.connect("127.0.0.1:4433".to_string(), "localhost".to_string()).await; + + match result { + Ok(connection) => { + println!("✅ Connection established successfully"); + + // Test bidirectional stream creation + let bi_result = connection.open_bi().await; + match bi_result { + Ok((send_stream, recv_stream)) => { + println!("✅ Bidirectional stream created successfully"); + } + Err(e) => { + println!("⚠️ Bidirectional stream creation failed (expected without server): {}", e); + } + } + + // Test unidirectional stream creation + let uni_result = connection.open_uni().await; + match uni_result { + Ok(send_stream) => { + println!("✅ Unidirectional stream created successfully"); + } + Err(e) => { + println!("⚠️ Unidirectional stream creation failed (expected without server): {}", e); + } + } + } + Err(e) => { + println!("⚠️ Connection failed (expected without server): {}", e); + println!("✅ Connection API validates correctly - error handling works"); + } + } + } +} diff --git a/vendor/flutter_quic/rust/src/models/certificate.rs b/vendor/flutter_quic/rust/src/models/certificate.rs new file mode 100644 index 0000000..1052733 --- /dev/null +++ b/vendor/flutter_quic/rust/src/models/certificate.rs @@ -0,0 +1,15 @@ +//! Certificate and TLS related types + +use flutter_rust_bridge::frb; + +/// Certificate chain for TLS +#[frb] +pub struct CertificateChain { + pub certificates: Vec>, +} + +/// Private key for TLS +#[frb] +pub struct PrivateKey { + pub key_data: Vec, +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/models/config_types.rs b/vendor/flutter_quic/rust/src/models/config_types.rs new file mode 100644 index 0000000..04f9116 --- /dev/null +++ b/vendor/flutter_quic/rust/src/models/config_types.rs @@ -0,0 +1,19 @@ +//! Configuration data structures + +use flutter_rust_bridge::frb; + +/// Transport configuration parameters +#[frb] +pub struct TransportConfig { + pub max_idle_timeout: Option, + pub max_uni_streams: Option, + pub max_bi_streams: Option, + pub max_data: Option, +} + +/// Endpoint configuration +#[frb] +pub struct EndpointConfig { + pub transport: TransportConfig, + pub alpn_protocols: Vec, +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/models/mod.rs b/vendor/flutter_quic/rust/src/models/mod.rs new file mode 100644 index 0000000..097ff60 --- /dev/null +++ b/vendor/flutter_quic/rust/src/models/mod.rs @@ -0,0 +1,8 @@ +//! Data models and structures +//! +//! Common data structures, enums, and type definitions used across +//! both Core and Convenience APIs. + +pub mod types; +pub mod certificate; +pub mod config_types; \ No newline at end of file diff --git a/vendor/flutter_quic/rust/src/models/types.rs b/vendor/flutter_quic/rust/src/models/types.rs new file mode 100644 index 0000000..e87b5f8 --- /dev/null +++ b/vendor/flutter_quic/rust/src/models/types.rs @@ -0,0 +1,23 @@ +//! Common types and data structures + +use flutter_rust_bridge::frb; + +/// QUIC connection ID +#[frb] +pub struct ConnectionId { + pub bytes: Vec, +} + +/// Network address information +#[frb] +pub struct SocketAddress { + pub ip: String, + pub port: u16, +} + +/// Stream direction enumeration +#[frb] +pub enum StreamDirection { + Bidirectional, + Unidirectional, +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/.cargo-ok b/vendor/flutter_quic/rust/vendor/quinn-proto/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/.cargo_vcs_info.json b/vendor/flutter_quic/rust/vendor/quinn-proto/.cargo_vcs_info.json new file mode 100644 index 0000000..0552d7a --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "2c315aa7f9c2a6c1db87f8f51f40623a427c78fd" + }, + "path_in_vcs": "quinn-proto" +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.lock b/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.lock new file mode 100644 index 0000000..ec4afb5 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.lock @@ -0,0 +1,1691 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed8cd42adddefbdb8507fb7443fa9b666631078616b78f70ed22117b5c27d90" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand", + "siphasher", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qlog" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f15b83c59e6b945f2261c95a1dd9faf239187f32ff0a96af1d1d28c4557f919" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "smallvec", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +dependencies = [ + "arbitrary", + "assert_matches", + "aws-lc-rs", + "bytes", + "fastbloom", + "getrandom 0.3.4", + "hex-literal", + "lazy_static", + "lru-slab", + "qlog", + "rand", + "rand_pcg", + "rcgen", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "tracing-subscriber", + "wasm-bindgen-test", + "web-time", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_pcg" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +dependencies = [ + "serde", + "serde_derive", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "thread_local", + "time", + "tracing", + "tracing-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.toml b/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.toml new file mode 100644 index 0000000..5c3f351 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.toml @@ -0,0 +1,194 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.74.1" +name = "quinn-proto" +version = "0.11.14" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "State machine for the QUIC transport protocol" +readme = false +keywords = ["quic"] +categories = [ + "network-programming", + "asynchronous", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/quinn-rs/quinn" + +[package.metadata.docs.rs] +features = [ + "rustls-aws-lc-rs", + "rustls-ring", + "platform-verifier", + "log", + "rustls-log", +] + +[features] +__rustls-post-quantum-test = [] +aws-lc-rs = [ + "dep:aws-lc-rs", + "aws-lc-rs?/aws-lc-sys", + "aws-lc-rs?/prebuilt-nasm", +] +aws-lc-rs-fips = [ + "aws-lc-rs", + "aws-lc-rs?/fips", +] +bloom = ["dep:fastbloom"] +default = [ + "rustls-ring", + "log", + "bloom", +] +log = ["tracing/log"] +platform-verifier = ["dep:rustls-platform-verifier"] +qlog = ["dep:qlog"] +ring = ["dep:ring"] +rustls = ["rustls-ring"] +rustls-aws-lc-rs = [ + "dep:rustls", + "rustls?/aws-lc-rs", + "aws-lc-rs", +] +rustls-aws-lc-rs-fips = [ + "rustls-aws-lc-rs", + "aws-lc-rs-fips", +] +rustls-log = ["rustls?/logging"] +rustls-ring = [ + "dep:rustls", + "rustls?/ring", + "ring", +] + +[lib] +name = "quinn_proto" +path = "src/lib.rs" + +[dependencies.arbitrary] +version = "1.0.1" +features = ["derive"] +optional = true + +[dependencies.aws-lc-rs] +version = "1.9" +optional = true +default-features = false + +[dependencies.bytes] +version = "1" + +[dependencies.fastbloom] +version = "0.14" +optional = true + +[dependencies.lru-slab] +version = "0.1.2" + +[dependencies.qlog] +version = "0.15.2" +optional = true + +[dependencies.rand] +version = "0.9" + +[dependencies.ring] +version = "0.17" +optional = true + +[dependencies.rustc-hash] +version = "2" + +[dependencies.rustls] +version = "0.23.5" +features = ["std"] +optional = true +default-features = false + +[dependencies.rustls-platform-verifier] +version = "0.6" +optional = true + +[dependencies.slab] +version = "0.4.6" + +[dependencies.thiserror] +version = "2.0.3" + +[dependencies.tinyvec] +version = "1.1" +features = [ + "alloc", + "alloc", +] + +[dependencies.tracing] +version = "0.1.10" +features = ["std"] +default-features = false + +[dev-dependencies.assert_matches] +version = "1.1" + +[dev-dependencies.hex-literal] +version = "1" + +[dev-dependencies.lazy_static] +version = "1" + +[dev-dependencies.rand_pcg] +version = "0.9" + +[dev-dependencies.rcgen] +version = "0.14" + +[dev-dependencies.tracing-subscriber] +version = "0.3.0" +features = [ + "env-filter", + "fmt", + "ansi", + "time", + "local-time", +] +default-features = false + +[dev-dependencies.wasm-bindgen-test] +version = "0.3.45" + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.getrandom] +version = "0.3" +features = ["wasm_js"] +default-features = false + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.ring] +version = "0.17" +features = ["wasm32_unknown_unknown_js"] + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.rustls-pki-types] +version = "1.7" +features = ["web"] + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.web-time] +version = "1" + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = ["cfg(fuzzing)"] diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.toml.orig b/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.toml.orig new file mode 100644 index 0000000..4212b35 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.toml.orig @@ -0,0 +1,84 @@ +[package] +name = "quinn-proto" +version = "0.11.14" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "State machine for the QUIC transport protocol" +keywords.workspace = true +categories.workspace = true +workspace = ".." + +[features] +# NOTE: Please keep this in sync with the feature list in `.github/workflows/codecov.yml`, see +# comment in that file for more information. +default = ["rustls-ring", "log", "bloom"] +aws-lc-rs = ["dep:aws-lc-rs", "aws-lc-rs?/aws-lc-sys", "aws-lc-rs?/prebuilt-nasm"] +aws-lc-rs-fips = ["aws-lc-rs", "aws-lc-rs?/fips"] +# Enables BloomTokenLog, and uses it by default +bloom = ["dep:fastbloom"] +# For backwards compatibility, `rustls` forwards to `rustls-ring` +rustls = ["rustls-ring"] +# Enable rustls with the `aws-lc-rs` crypto provider +rustls-aws-lc-rs = ["dep:rustls", "rustls?/aws-lc-rs", "aws-lc-rs"] +rustls-aws-lc-rs-fips = ["rustls-aws-lc-rs", "aws-lc-rs-fips"] +# Enable rustls with the `ring` crypto provider +rustls-ring = ["dep:rustls", "rustls?/ring", "ring"] +ring = ["dep:ring"] +# Enable rustls ring provider and direct ring usage +# Provides `ClientConfig::with_platform_verifier()` convenience method +platform-verifier = ["dep:rustls-platform-verifier"] +# Configure `tracing` to log events via `log` if no `tracing` subscriber exists. +log = ["tracing/log"] +# Enable rustls logging +rustls-log = ["rustls?/logging"] +# Enable qlog support +qlog = ["dep:qlog"] + +# Internal (PRIVATE!) features used to aid testing. +# Don't rely on these whatsoever. They may disappear at any time. + +__rustls-post-quantum-test = [] + +[dependencies] +arbitrary = { workspace = true, optional = true } +aws-lc-rs = { workspace = true, optional = true } +bytes = { workspace = true } +fastbloom = { workspace = true, optional = true } +lru-slab = { workspace = true } +qlog = { workspace = true, optional = true } +rustc-hash = { workspace = true } +rand = { workspace = true } +ring = { workspace = true, optional = true } +rustls = { workspace = true, optional = true } +rustls-platform-verifier = { workspace = true, optional = true } +slab = { workspace = true } +thiserror = { workspace = true } +tinyvec = { workspace = true, features = ["alloc"] } +tracing = { workspace = true } + +# Feature flags & dependencies for wasm +# wasm-bindgen is assumed for a wasm*-*-unknown target +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +ring = { workspace = true, features = ["wasm32_unknown_unknown_js"] } +getrandom = { workspace = true, features = ["wasm_js"] } +rustls-pki-types = { workspace = true, features = ["web"] } # only added as dependency to enforce the `web` feature for this target +web-time = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +hex-literal = { workspace = true } +rand_pcg = "0.9" +rcgen = { workspace = true } +tracing-subscriber = { workspace = true } +lazy_static = "1" +wasm-bindgen-test = { workspace = true } + +[lints.rust] +# https://rust-fuzz.github.io/book/cargo-fuzz/guide.html#cfgfuzzing +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } + +[package.metadata.docs.rs] +# all non-default features except fips (cannot build on docs.rs environment) +features = ["rustls-aws-lc-rs", "rustls-ring", "platform-verifier", "log", "rustls-log"] diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/LICENSE-APACHE b/vendor/flutter_quic/rust/vendor/quinn-proto/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/LICENSE-MIT b/vendor/flutter_quic/rust/vendor/quinn-proto/LICENSE-MIT new file mode 100644 index 0000000..f656104 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) 2018 The quinn Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/bloom_token_log.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/bloom_token_log.rs new file mode 100644 index 0000000..dd5e83d --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/bloom_token_log.rs @@ -0,0 +1,368 @@ +use std::{ + collections::HashSet, + f64::consts::LN_2, + hash::{BuildHasher, Hasher}, + mem::{size_of, take}, + sync::Mutex, +}; + +use fastbloom::BloomFilter; +use rustc_hash::FxBuildHasher; +use tracing::{trace, warn}; + +use crate::{Duration, SystemTime, TokenLog, TokenReuseError, UNIX_EPOCH}; + +/// Bloom filter-based [`TokenLog`] +/// +/// Parameterizable over an approximate maximum number of bytes to allocate. Starts out by storing +/// used tokens in a hash set. Once the hash set becomes too large, converts it to a bloom filter. +/// This achieves a memory profile of linear growth with an upper bound. +/// +/// Divides time into periods based on `lifetime` and stores two filters at any given moment, for +/// each of the two periods currently non-expired tokens could expire in. As such, turns over +/// filters as time goes on to avoid bloom filter false positive rate increasing infinitely over +/// time. +pub struct BloomTokenLog(Mutex); + +impl BloomTokenLog { + /// Construct with an approximate maximum memory usage and expected number of validation token + /// usages per expiration period + /// + /// Calculates the optimal bloom filter k number automatically. + pub fn new_expected_items(max_bytes: usize, expected_hits: u64) -> Self { + Self::new(max_bytes, optimal_k_num(max_bytes, expected_hits)) + } + + /// Construct with an approximate maximum memory usage and a [bloom filter k number][bloom] + /// + /// [bloom]: https://en.wikipedia.org/wiki/Bloom_filter + /// + /// If choosing a custom k number, note that `BloomTokenLog` always maintains two filters + /// between them and divides the allocation budget of `max_bytes` evenly between them. As such, + /// each bloom filter will contain `max_bytes * 4` bits. + pub fn new(max_bytes: usize, k_num: u32) -> Self { + Self(Mutex::new(State { + config: FilterConfig { + filter_max_bytes: max_bytes / 2, + k_num, + }, + period_1_start: UNIX_EPOCH, + filter_1: Filter::default(), + filter_2: Filter::default(), + })) + } +} + +impl TokenLog for BloomTokenLog { + fn check_and_insert( + &self, + nonce: u128, + issued: SystemTime, + lifetime: Duration, + ) -> Result<(), TokenReuseError> { + trace!(%nonce, "check_and_insert"); + + if lifetime.is_zero() { + // avoid divide-by-zero if lifetime is zero + return Err(TokenReuseError); + } + + let mut guard = self.0.lock().unwrap(); + let state = &mut *guard; + + // calculate how many periods past period 1 the token expires + let expires_at = issued + lifetime; + let Ok(periods_forward) = expires_at + .duration_since(state.period_1_start) + .map(|duration| duration.as_nanos() / lifetime.as_nanos()) + else { + // shouldn't happen unless time travels backwards or lifetime changes or the current + // system time is before the Unix epoch + warn!("BloomTokenLog presented with token too far in past"); + return Err(TokenReuseError); + }; + + // get relevant filter + let filter = match periods_forward { + 0 => &mut state.filter_1, + 1 => &mut state.filter_2, + 2 => { + // turn over filter 1 + state.filter_1 = take(&mut state.filter_2); + state.period_1_start += lifetime; + &mut state.filter_2 + } + _ => { + // turn over both filters + state.filter_1 = Filter::default(); + state.filter_2 = Filter::default(); + state.period_1_start = expires_at; + &mut state.filter_1 + } + }; + + // insert into the filter + // + // the token's nonce needs to guarantee uniqueness because of the role it plays in the + // encryption of the tokens, so it is 128 bits. but since the token log can tolerate false + // positives, we trim it down to 64 bits, which would still only have a small collision + // rate even at significant amounts of usage, while allowing us to store twice as many in + // the hash set variant. + // + // token nonce values are uniformly randomly generated server-side and cryptographically + // integrity-checked, so we don't need to employ secure hashing to trim it down to 64 bits, + // we can simply truncate. + // + // per the Rust reference, we can truncate by simply casting: + // https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#numeric-cast + filter.check_and_insert(nonce as u64, &state.config) + } +} + +/// Default to 20 MiB max memory consumption and expected one million hits +/// +/// With the default validation token lifetime of 2 weeks, this corresponds to one token usage per +/// 1.21 seconds. +impl Default for BloomTokenLog { + fn default() -> Self { + Self::new_expected_items(DEFAULT_MAX_BYTES, DEFAULT_EXPECTED_HITS) + } +} + +/// Lockable state of [`BloomTokenLog`] +struct State { + config: FilterConfig, + // filter_1 covers tokens that expire in the period starting at period_1_start and extending + // lifetime after. filter_2 covers tokens for the next lifetime after that. + period_1_start: SystemTime, + filter_1: Filter, + filter_2: Filter, +} + +/// Unchanging parameters governing [`Filter`] behavior +struct FilterConfig { + filter_max_bytes: usize, + k_num: u32, +} + +/// Period filter within [`State`] +enum Filter { + Set(HashSet), + Bloom(BloomFilter), +} + +impl Filter { + fn check_and_insert( + &mut self, + fingerprint: u64, + config: &FilterConfig, + ) -> Result<(), TokenReuseError> { + match self { + Self::Set(hset) => { + if !hset.insert(fingerprint) { + return Err(TokenReuseError); + } + + if hset.capacity() * size_of::() <= config.filter_max_bytes { + return Ok(()); + } + + // convert to bloom + // avoid panicking if user passed in filter_max_bytes of 0. we document that this + // limit is approximate, so just fudge it up to 1. + let mut bloom = BloomFilter::with_num_bits((config.filter_max_bytes * 8).max(1)) + .hasher(FxBuildHasher) + .hashes(config.k_num); + for item in &*hset { + bloom.insert(item); + } + *self = Self::Bloom(bloom); + } + Self::Bloom(bloom) => { + if bloom.insert(&fingerprint) { + return Err(TokenReuseError); + } + } + } + Ok(()) + } +} + +impl Default for Filter { + fn default() -> Self { + Self::Set(HashSet::default()) + } +} + +/// `BuildHasher` of `IdentityHasher` +#[derive(Default)] +struct IdentityBuildHasher; + +impl BuildHasher for IdentityBuildHasher { + type Hasher = IdentityHasher; + + fn build_hasher(&self) -> Self::Hasher { + IdentityHasher::default() + } +} + +/// Hasher that is the identity operation--it assumes that exactly 8 bytes will be hashed, and the +/// resultant hash is those bytes as a `u64` +#[derive(Default)] +struct IdentityHasher { + data: [u8; 8], + #[cfg(debug_assertions)] + wrote_8_byte_slice: bool, +} + +impl Hasher for IdentityHasher { + fn write(&mut self, bytes: &[u8]) { + #[cfg(debug_assertions)] + { + assert!(!self.wrote_8_byte_slice); + assert_eq!(bytes.len(), 8); + self.wrote_8_byte_slice = true; + } + self.data.copy_from_slice(bytes); + } + + fn finish(&self) -> u64 { + #[cfg(debug_assertions)] + assert!(self.wrote_8_byte_slice); + u64::from_ne_bytes(self.data) + } +} + +fn optimal_k_num(num_bytes: usize, expected_hits: u64) -> u32 { + // be more forgiving rather than panickey here. excessively high num_bits may occur if the user + // wishes it to be unbounded, so just saturate. expected_hits of 0 would cause divide-by-zero, + // so just fudge it up to 1 in that case. + let num_bits = (num_bytes as u64).saturating_mul(8); + let expected_hits = expected_hits.max(1); + // reference for this formula: https://programming.guide/bloom-filter-calculator.html + // optimal k = (m ln 2) / n + // wherein m is the number of bits, and n is the number of elements in the set. + // + // we also impose a minimum return value of 1, to avoid making the bloom filter entirely + // useless in the case that the user provided an absurdly high ratio of hits / bytes. + (((num_bits as f64 / expected_hits as f64) * LN_2).round() as u32).max(1) +} + +// remember to change the doc comment for `impl Default for BloomTokenLog` if these ever change +const DEFAULT_MAX_BYTES: usize = 10 << 20; +const DEFAULT_EXPECTED_HITS: u64 = 1_000_000; + +#[cfg(test)] +mod test { + use super::*; + use rand::prelude::*; + use rand_pcg::Pcg32; + + fn new_rng() -> impl Rng { + Pcg32::from_seed(0xdeadbeefdeadbeefdeadbeefdeadbeef_u128.to_le_bytes()) + } + + #[test] + fn identity_hash_test() { + let mut rng = new_rng(); + let builder = IdentityBuildHasher; + for _ in 0..100 { + let n = rng.random::(); + let hash = builder.hash_one(n); + assert_eq!(hash, n); + } + } + + #[test] + fn optimal_k_num_test() { + assert_eq!(optimal_k_num(10 << 20, 1_000_000), 58); + assert_eq!(optimal_k_num(10 << 20, 1_000_000_000_000_000), 1); + // assert that these don't panic: + optimal_k_num(10 << 20, 0); + optimal_k_num(usize::MAX, 1_000_000); + } + + #[test] + fn bloom_token_log_conversion() { + let mut rng = new_rng(); + let mut log = BloomTokenLog::new_expected_items(800, 200); + + let issued = SystemTime::now(); + let lifetime = Duration::from_secs(1_000_000); + + for i in 0..200 { + let token = rng.random::(); + let result = log.check_and_insert(token, issued, lifetime); + { + let filter = &log.0.lock().unwrap().filter_1; + if let Filter::Set(ref hset) = *filter { + assert!(hset.capacity() * size_of::() <= 800); + assert_eq!(hset.len(), i + 1); + assert!(result.is_ok()); + } else { + assert!(i > 10, "definitely bloomed too early"); + } + } + assert!(log.check_and_insert(token, issued, lifetime).is_err()); + } + + assert!( + matches!(log.0.get_mut().unwrap().filter_1, Filter::Bloom { .. }), + "didn't bloom" + ); + } + + #[test] + fn turn_over() { + let mut rng = new_rng(); + let log = BloomTokenLog::new_expected_items(800, 200); + let lifetime = Duration::from_secs(1_000); + let mut old = Vec::default(); + let mut accepted = 0; + + for i in 0..200 { + let token = rng.random::(); + let now = UNIX_EPOCH + lifetime * 10 + lifetime * i / 10; + let issued = now - lifetime.mul_f32(rng.random_range(0.0..3.0)); + let result = log.check_and_insert(token, issued, lifetime); + if result.is_ok() { + accepted += 1; + } + old.push((token, issued)); + let old_idx = rng.random_range(0..old.len()); + let (old_token, old_issued) = old[old_idx]; + assert!( + log.check_and_insert(old_token, old_issued, lifetime) + .is_err() + ); + } + assert!(accepted > 0); + } + + fn test_doesnt_panic(log: BloomTokenLog) { + let mut rng = new_rng(); + + let issued = SystemTime::now(); + let lifetime = Duration::from_secs(1_000_000); + + for _ in 0..200 { + let _ = log.check_and_insert(rng.random::(), issued, lifetime); + } + } + + #[test] + fn max_bytes_zero() { + // "max bytes" is documented to be approximate. but make sure it doesn't panic. + test_doesnt_panic(BloomTokenLog::new_expected_items(0, 200)); + } + + #[test] + fn expected_hits_zero() { + test_doesnt_panic(BloomTokenLog::new_expected_items(100, 0)); + } + + #[test] + fn k_num_zero() { + test_doesnt_panic(BloomTokenLog::new(100, 0)); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/cid_generator.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/cid_generator.rs new file mode 100644 index 0000000..e62415e --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/cid_generator.rs @@ -0,0 +1,180 @@ +use std::hash::Hasher; + +use rand::{Rng, RngCore}; + +use crate::Duration; +use crate::MAX_CID_SIZE; +use crate::shared::ConnectionId; + +/// Generates connection IDs for incoming connections +pub trait ConnectionIdGenerator: Send + Sync { + /// Generates a new CID + /// + /// Connection IDs MUST NOT contain any information that can be used by + /// an external observer (that is, one that does not cooperate with the + /// issuer) to correlate them with other connection IDs for the same + /// connection. They MUST have high entropy, e.g. due to encrypted data + /// or cryptographic-grade random data. + fn generate_cid(&mut self) -> ConnectionId; + + /// Quickly determine whether `cid` could have been generated by this generator + /// + /// False positives are permitted, but increase the cost of handling invalid packets. + fn validate(&self, _cid: &ConnectionId) -> Result<(), InvalidCid> { + Ok(()) + } + + /// Returns the length of a CID for connections created by this generator + fn cid_len(&self) -> usize; + /// Returns the lifetime of generated Connection IDs + /// + /// Connection IDs will be retired after the returned `Duration`, if any. Assumed to be constant. + fn cid_lifetime(&self) -> Option; +} + +/// The connection ID was not recognized by the [`ConnectionIdGenerator`] +#[derive(Debug, Copy, Clone)] +pub struct InvalidCid; + +/// Generates purely random connection IDs of a specified length +/// +/// Random CIDs can be smaller than those produced by [`HashedConnectionIdGenerator`], but cannot be +/// usefully [`validate`](ConnectionIdGenerator::validate)d. +#[derive(Debug, Clone, Copy)] +pub struct RandomConnectionIdGenerator { + cid_len: usize, + lifetime: Option, +} + +impl Default for RandomConnectionIdGenerator { + fn default() -> Self { + Self { + cid_len: 8, + lifetime: None, + } + } +} + +impl RandomConnectionIdGenerator { + /// Initialize Random CID generator with a fixed CID length + /// + /// The given length must be less than or equal to MAX_CID_SIZE. + pub fn new(cid_len: usize) -> Self { + debug_assert!(cid_len <= MAX_CID_SIZE); + Self { + cid_len, + ..Self::default() + } + } + + /// Set the lifetime of CIDs created by this generator + pub fn set_lifetime(&mut self, d: Duration) -> &mut Self { + self.lifetime = Some(d); + self + } +} + +impl ConnectionIdGenerator for RandomConnectionIdGenerator { + fn generate_cid(&mut self) -> ConnectionId { + let mut bytes_arr = [0; MAX_CID_SIZE]; + rand::rng().fill_bytes(&mut bytes_arr[..self.cid_len]); + + ConnectionId::new(&bytes_arr[..self.cid_len]) + } + + /// Provide the length of dst_cid in short header packet + fn cid_len(&self) -> usize { + self.cid_len + } + + fn cid_lifetime(&self) -> Option { + self.lifetime + } +} + +/// Generates 8-byte connection IDs that can be efficiently +/// [`validate`](ConnectionIdGenerator::validate)d +/// +/// This generator uses a non-cryptographic hash and can therefore still be spoofed, but nonetheless +/// helps prevents Quinn from responding to non-QUIC packets at very low cost. +pub struct HashedConnectionIdGenerator { + key: u64, + lifetime: Option, +} + +impl HashedConnectionIdGenerator { + /// Create a generator with a random key + pub fn new() -> Self { + Self::from_key(rand::rng().random()) + } + + /// Create a generator with a specific key + /// + /// Allows [`validate`](ConnectionIdGenerator::validate) to recognize a consistent set of + /// connection IDs across restarts + pub fn from_key(key: u64) -> Self { + Self { + key, + lifetime: None, + } + } + + /// Set the lifetime of CIDs created by this generator + pub fn set_lifetime(&mut self, d: Duration) -> &mut Self { + self.lifetime = Some(d); + self + } +} + +impl Default for HashedConnectionIdGenerator { + fn default() -> Self { + Self::new() + } +} + +impl ConnectionIdGenerator for HashedConnectionIdGenerator { + fn generate_cid(&mut self) -> ConnectionId { + let mut bytes_arr = [0; NONCE_LEN + SIGNATURE_LEN]; + rand::rng().fill_bytes(&mut bytes_arr[..NONCE_LEN]); + let mut hasher = rustc_hash::FxHasher::default(); + hasher.write_u64(self.key); + hasher.write(&bytes_arr[..NONCE_LEN]); + bytes_arr[NONCE_LEN..].copy_from_slice(&hasher.finish().to_le_bytes()[..SIGNATURE_LEN]); + ConnectionId::new(&bytes_arr) + } + + fn validate(&self, cid: &ConnectionId) -> Result<(), InvalidCid> { + let (nonce, signature) = cid.split_at(NONCE_LEN); + let mut hasher = rustc_hash::FxHasher::default(); + hasher.write_u64(self.key); + hasher.write(nonce); + let expected = hasher.finish().to_le_bytes(); + match expected[..SIGNATURE_LEN] == signature[..] { + true => Ok(()), + false => Err(InvalidCid), + } + } + + fn cid_len(&self) -> usize { + NONCE_LEN + SIGNATURE_LEN + } + + fn cid_lifetime(&self) -> Option { + self.lifetime + } +} + +const NONCE_LEN: usize = 3; // Good for more than 16 million connections +const SIGNATURE_LEN: usize = 8 - NONCE_LEN; // 8-byte total CID length + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_keyed_cid() { + let mut generator = HashedConnectionIdGenerator::new(); + let cid = generator.generate_cid(); + generator.validate(&cid).unwrap(); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/cid_queue.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/cid_queue.rs new file mode 100644 index 0000000..3e3eec9 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/cid_queue.rs @@ -0,0 +1,303 @@ +use std::ops::Range; + +use crate::{ConnectionId, ResetToken, frame::NewConnectionId}; + +/// DataType stored in CidQueue buffer +type CidData = (ConnectionId, Option); + +/// Sliding window of active Connection IDs +/// +/// May contain gaps due to packet loss or reordering +#[derive(Debug)] +pub(crate) struct CidQueue { + /// Ring buffer indexed by `self.cursor` + buffer: [Option; Self::LEN], + /// Index at which circular buffer addressing is based + cursor: usize, + /// Sequence number of `self.buffer[cursor]` + /// + /// The sequence number of the active CID; must be the smallest among CIDs in `buffer`. + offset: u64, +} + +impl CidQueue { + pub(crate) fn new(cid: ConnectionId) -> Self { + let mut buffer = [None; Self::LEN]; + buffer[0] = Some((cid, None)); + Self { + buffer, + cursor: 0, + offset: 0, + } + } + + /// Handle a `NEW_CONNECTION_ID` frame + /// + /// Returns a non-empty range of retired sequence numbers and the reset token of the new active + /// CID iff any CIDs were retired. + pub(crate) fn insert( + &mut self, + cid: NewConnectionId, + ) -> Result, ResetToken)>, InsertError> { + // Position of new CID wrt. the current active CID + let index = match cid.sequence.checked_sub(self.offset) { + None => return Err(InsertError::Retired), + Some(x) => x, + }; + + let retired_count = cid.retire_prior_to.saturating_sub(self.offset); + if index >= Self::LEN as u64 + retired_count { + return Err(InsertError::ExceedsLimit); + } + + // Discard retired CIDs, if any + for i in 0..(retired_count.min(Self::LEN as u64) as usize) { + self.buffer[(self.cursor + i) % Self::LEN] = None; + } + + // Record the new CID + let index = ((self.cursor as u64 + index) % Self::LEN as u64) as usize; + self.buffer[index] = Some((cid.id, Some(cid.reset_token))); + + if retired_count == 0 { + return Ok(None); + } + + // The active CID was retired. Find the first known CID with sequence number of at least + // retire_prior_to, and inform the caller that all prior CIDs have been retired, and of + // the new CID's reset token. + self.cursor = ((self.cursor as u64 + retired_count) % Self::LEN as u64) as usize; + let (i, (_, token)) = self + .iter() + .next() + .expect("it is impossible to retire a CID without supplying a new one"); + self.cursor = (self.cursor + i) % Self::LEN; + let orig_offset = self.offset; + self.offset = cid.retire_prior_to + i as u64; + // We don't immediately retire CIDs in the range (orig_offset + + // Self::LEN)..self.offset. These are CIDs that we haven't yet received from a + // NEW_CONNECTION_ID frame, since having previously received them would violate the + // connection ID limit we specified based on Self::LEN. If we do receive a such a frame + // in the future, e.g. due to reordering, we'll retire it then. This ensures we can't be + // made to buffer an arbitrarily large number of RETIRE_CONNECTION_ID frames. + Ok(Some(( + orig_offset..self.offset.min(orig_offset + Self::LEN as u64), + token.expect("non-initial CID missing reset token"), + ))) + } + + /// Switch to next active CID if possible, return + /// 1) the corresponding ResetToken and 2) a non-empty range preceding it to retire + pub(crate) fn next(&mut self) -> Option<(ResetToken, Range)> { + let (i, cid_data) = self.iter().nth(1)?; + self.buffer[self.cursor] = None; + + let orig_offset = self.offset; + self.offset += i as u64; + self.cursor = (self.cursor + i) % Self::LEN; + Some((cid_data.1.unwrap(), orig_offset..self.offset)) + } + + /// Iterate CIDs in CidQueue that are not `None`, including the active CID + fn iter(&self) -> impl Iterator + '_ { + (0..Self::LEN).filter_map(move |step| { + let index = (self.cursor + step) % Self::LEN; + self.buffer[index].map(|cid_data| (step, cid_data)) + }) + } + + /// Replace the initial CID + pub(crate) fn update_initial_cid(&mut self, cid: ConnectionId) { + debug_assert_eq!(self.offset, 0); + self.buffer[self.cursor] = Some((cid, None)); + } + + /// Return active remote CID itself + pub(crate) fn active(&self) -> ConnectionId { + self.buffer[self.cursor].unwrap().0 + } + + /// Return the sequence number of active remote CID + pub(crate) fn active_seq(&self) -> u64 { + self.offset + } + + pub(crate) const LEN: usize = 5; +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum InsertError { + /// CID was already retired + Retired, + /// Sequence number violates the leading edge of the window + ExceedsLimit, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn cid(sequence: u64, retire_prior_to: u64) -> NewConnectionId { + NewConnectionId { + sequence, + id: ConnectionId::new(&[0xAB; 8]), + reset_token: ResetToken::from([0xCD; crate::RESET_TOKEN_SIZE]), + retire_prior_to, + } + } + + fn initial_cid() -> ConnectionId { + ConnectionId::new(&[0xFF; 8]) + } + + #[test] + fn next_dense() { + let mut q = CidQueue::new(initial_cid()); + assert!(q.next().is_none()); + assert!(q.next().is_none()); + + for i in 1..CidQueue::LEN as u64 { + q.insert(cid(i, 0)).unwrap(); + } + for i in 1..CidQueue::LEN as u64 { + let (_, retire) = q.next().unwrap(); + assert_eq!(q.active_seq(), i); + assert_eq!(retire.end - retire.start, 1); + } + assert!(q.next().is_none()); + } + #[test] + fn next_sparse() { + let mut q = CidQueue::new(initial_cid()); + let seqs = (1..CidQueue::LEN as u64).filter(|x| x % 2 == 0); + for i in seqs.clone() { + q.insert(cid(i, 0)).unwrap(); + } + for i in seqs { + let (_, retire) = q.next().unwrap(); + dbg!(&retire); + assert_eq!(q.active_seq(), i); + assert_eq!(retire, (q.active_seq().saturating_sub(2))..q.active_seq()); + } + assert!(q.next().is_none()); + } + + #[test] + fn wrap() { + let mut q = CidQueue::new(initial_cid()); + + for i in 1..CidQueue::LEN as u64 { + q.insert(cid(i, 0)).unwrap(); + } + for _ in 1..(CidQueue::LEN as u64 - 1) { + q.next().unwrap(); + } + for i in CidQueue::LEN as u64..(CidQueue::LEN as u64 + 3) { + q.insert(cid(i, 0)).unwrap(); + } + for i in (CidQueue::LEN as u64 - 1)..(CidQueue::LEN as u64 + 3) { + q.next().unwrap(); + assert_eq!(q.active_seq(), i); + } + assert!(q.next().is_none()); + } + + #[test] + fn retire_dense() { + let mut q = CidQueue::new(initial_cid()); + + for i in 1..CidQueue::LEN as u64 { + q.insert(cid(i, 0)).unwrap(); + } + assert_eq!(q.active_seq(), 0); + + assert_eq!(q.insert(cid(4, 2)).unwrap().unwrap().0, 0..2); + assert_eq!(q.active_seq(), 2); + assert_eq!(q.insert(cid(4, 2)), Ok(None)); + + for i in 2..(CidQueue::LEN as u64 - 1) { + let _ = q.next().unwrap(); + assert_eq!(q.active_seq(), i + 1); + assert_eq!(q.insert(cid(i + 1, i + 1)), Ok(None)); + } + + assert!(q.next().is_none()); + } + + #[test] + fn retire_sparse() { + // Retiring CID 0 when CID 1 is not known should retire CID 1 as we move to CID 2 + let mut q = CidQueue::new(initial_cid()); + q.insert(cid(2, 0)).unwrap(); + assert_eq!(q.insert(cid(3, 1)).unwrap().unwrap().0, 0..2,); + assert_eq!(q.active_seq(), 2); + } + + #[test] + fn retire_many() { + let mut q = CidQueue::new(initial_cid()); + q.insert(cid(2, 0)).unwrap(); + assert_eq!( + q.insert(cid(1_000_000, 1_000_000)).unwrap().unwrap().0, + 0..CidQueue::LEN as u64, + ); + assert_eq!(q.active_seq(), 1_000_000); + } + + #[test] + fn insert_limit() { + let mut q = CidQueue::new(initial_cid()); + assert_eq!(q.insert(cid(CidQueue::LEN as u64 - 1, 0)), Ok(None)); + assert_eq!( + q.insert(cid(CidQueue::LEN as u64, 0)), + Err(InsertError::ExceedsLimit) + ); + } + + #[test] + fn insert_duplicate() { + let mut q = CidQueue::new(initial_cid()); + q.insert(cid(0, 0)).unwrap(); + q.insert(cid(0, 0)).unwrap(); + } + + #[test] + fn insert_retired() { + let mut q = CidQueue::new(initial_cid()); + assert_eq!( + q.insert(cid(0, 0)), + Ok(None), + "reinserting active CID succeeds" + ); + assert!(q.next().is_none(), "active CID isn't requeued"); + q.insert(cid(1, 0)).unwrap(); + q.next().unwrap(); + assert_eq!( + q.insert(cid(0, 0)), + Err(InsertError::Retired), + "previous active CID is already retired" + ); + } + + #[test] + fn retire_then_insert_next() { + let mut q = CidQueue::new(initial_cid()); + for i in 1..CidQueue::LEN as u64 { + q.insert(cid(i, 0)).unwrap(); + } + q.next().unwrap(); + q.insert(cid(CidQueue::LEN as u64, 0)).unwrap(); + assert_eq!( + q.insert(cid(CidQueue::LEN as u64 + 1, 0)), + Err(InsertError::ExceedsLimit) + ); + } + + #[test] + fn always_valid() { + let mut q = CidQueue::new(initial_cid()); + assert!(q.next().is_none()); + assert_eq!(q.active(), initial_cid()); + assert_eq!(q.active_seq(), 0); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/coding.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/coding.rs new file mode 100644 index 0000000..3104cbf --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/coding.rs @@ -0,0 +1,130 @@ +//! Coding related traits. + +use std::net::{Ipv4Addr, Ipv6Addr}; + +use bytes::{Buf, BufMut}; +use thiserror::Error; + +use crate::VarInt; + +/// Error indicating that the provided buffer was too small +#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)] +#[error("unexpected end of buffer")] +pub struct UnexpectedEnd; + +/// Coding result type +pub type Result = ::std::result::Result; + +/// Infallible encoding and decoding of QUIC primitives +pub trait Codec: Sized { + /// Decode a `Self` from the provided buffer, if the buffer is large enough + fn decode(buf: &mut B) -> Result; + /// Append the encoding of `self` to the provided buffer + fn encode(&self, buf: &mut B); +} + +impl Codec for u8 { + fn decode(buf: &mut B) -> Result { + if buf.remaining() < 1 { + return Err(UnexpectedEnd); + } + Ok(buf.get_u8()) + } + fn encode(&self, buf: &mut B) { + buf.put_u8(*self); + } +} + +impl Codec for u16 { + fn decode(buf: &mut B) -> Result { + if buf.remaining() < 2 { + return Err(UnexpectedEnd); + } + Ok(buf.get_u16()) + } + fn encode(&self, buf: &mut B) { + buf.put_u16(*self); + } +} + +impl Codec for u32 { + fn decode(buf: &mut B) -> Result { + if buf.remaining() < 4 { + return Err(UnexpectedEnd); + } + Ok(buf.get_u32()) + } + fn encode(&self, buf: &mut B) { + buf.put_u32(*self); + } +} + +impl Codec for u64 { + fn decode(buf: &mut B) -> Result { + if buf.remaining() < 8 { + return Err(UnexpectedEnd); + } + Ok(buf.get_u64()) + } + fn encode(&self, buf: &mut B) { + buf.put_u64(*self); + } +} + +impl Codec for Ipv4Addr { + fn decode(buf: &mut B) -> Result { + if buf.remaining() < 4 { + return Err(UnexpectedEnd); + } + let mut octets = [0; 4]; + buf.copy_to_slice(&mut octets); + Ok(octets.into()) + } + fn encode(&self, buf: &mut B) { + buf.put_slice(&self.octets()); + } +} + +impl Codec for Ipv6Addr { + fn decode(buf: &mut B) -> Result { + if buf.remaining() < 16 { + return Err(UnexpectedEnd); + } + let mut octets = [0; 16]; + buf.copy_to_slice(&mut octets); + Ok(octets.into()) + } + fn encode(&self, buf: &mut B) { + buf.put_slice(&self.octets()); + } +} + +pub(crate) trait BufExt { + fn get(&mut self) -> Result; + fn get_var(&mut self) -> Result; +} + +impl BufExt for T { + fn get(&mut self) -> Result { + U::decode(self) + } + + fn get_var(&mut self) -> Result { + Ok(VarInt::decode(self)?.into_inner()) + } +} + +pub(crate) trait BufMutExt { + fn write(&mut self, x: T); + fn write_var(&mut self, x: u64); +} + +impl BufMutExt for T { + fn write(&mut self, x: U) { + x.encode(self); + } + + fn write_var(&mut self, x: u64) { + VarInt::from_u64(x).unwrap().encode(self); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/config/mod.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/config/mod.rs new file mode 100644 index 0000000..aa755b5 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/config/mod.rs @@ -0,0 +1,697 @@ +use std::{ + fmt, + net::{SocketAddrV4, SocketAddrV6}, + num::TryFromIntError, + sync::Arc, +}; + +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +use rustls::client::WebPkiServerVerifier; +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use thiserror::Error; + +#[cfg(feature = "bloom")] +use crate::BloomTokenLog; +#[cfg(not(feature = "bloom"))] +use crate::NoneTokenLog; +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +use crate::crypto::rustls::{QuicServerConfig, configured_provider}; +use crate::{ + DEFAULT_SUPPORTED_VERSIONS, Duration, MAX_CID_SIZE, RandomConnectionIdGenerator, SystemTime, + TokenLog, TokenMemoryCache, TokenStore, VarInt, VarIntBoundsExceeded, + cid_generator::{ConnectionIdGenerator, HashedConnectionIdGenerator}, + crypto::{self, HandshakeTokenKey, HmacKey}, + shared::ConnectionId, +}; + +mod transport; +#[cfg(feature = "qlog")] +pub use transport::QlogConfig; +pub use transport::{AckFrequencyConfig, IdleTimeout, MtuDiscoveryConfig, TransportConfig}; + +/// Global configuration for the endpoint, affecting all connections +/// +/// Default values should be suitable for most internet applications. +#[derive(Clone)] +pub struct EndpointConfig { + pub(crate) reset_key: Arc, + pub(crate) max_udp_payload_size: VarInt, + /// CID generator factory + /// + /// Create a cid generator for local cid in Endpoint struct + pub(crate) connection_id_generator_factory: + Arc Box + Send + Sync>, + pub(crate) supported_versions: Vec, + pub(crate) grease_quic_bit: bool, + /// Minimum interval between outgoing stateless reset packets + pub(crate) min_reset_interval: Duration, + /// Optional seed to be used internally for random number generation + pub(crate) rng_seed: Option<[u8; 32]>, +} + +impl EndpointConfig { + /// Create a default config with a particular `reset_key` + pub fn new(reset_key: Arc) -> Self { + let cid_factory = + || -> Box { Box::::default() }; + Self { + reset_key, + max_udp_payload_size: (1500u32 - 28).into(), // Ethernet MTU minus IP + UDP headers + connection_id_generator_factory: Arc::new(cid_factory), + supported_versions: DEFAULT_SUPPORTED_VERSIONS.to_vec(), + grease_quic_bit: true, + min_reset_interval: Duration::from_millis(20), + rng_seed: None, + } + } + + /// Supply a custom connection ID generator factory + /// + /// Called once by each `Endpoint` constructed from this configuration to obtain the CID + /// generator which will be used to generate the CIDs used for incoming packets on all + /// connections involving that `Endpoint`. A custom CID generator allows applications to embed + /// information in local connection IDs, e.g. to support stateless packet-level load balancers. + /// + /// Defaults to [`HashedConnectionIdGenerator`]. + pub fn cid_generator Box + Send + Sync + 'static>( + &mut self, + factory: F, + ) -> &mut Self { + self.connection_id_generator_factory = Arc::new(factory); + self + } + + /// Private key used to send authenticated connection resets to peers who were + /// communicating with a previous instance of this endpoint. + pub fn reset_key(&mut self, key: Arc) -> &mut Self { + self.reset_key = key; + self + } + + /// Maximum UDP payload size accepted from peers (excluding UDP and IP overhead). + /// + /// Must be greater or equal than 1200. + /// + /// Defaults to 1472, which is the largest UDP payload that can be transmitted in the typical + /// 1500 byte Ethernet MTU. Deployments on links with larger MTUs (e.g. loopback or Ethernet + /// with jumbo frames) can raise this to improve performance at the cost of a linear increase in + /// datagram receive buffer size. + pub fn max_udp_payload_size(&mut self, value: u16) -> Result<&mut Self, ConfigError> { + if !(1200..=65_527).contains(&value) { + return Err(ConfigError::OutOfBounds); + } + + self.max_udp_payload_size = value.into(); + Ok(self) + } + + /// Get the current value of [`max_udp_payload_size`](Self::max_udp_payload_size) + // + // While most parameters don't need to be readable, this must be exposed to allow higher-level + // layers, e.g. the `quinn` crate, to determine how large a receive buffer to allocate to + // support an externally-defined `EndpointConfig`. + // + // While `get_` accessors are typically unidiomatic in Rust, we favor concision for setters, + // which will be used far more heavily. + pub fn get_max_udp_payload_size(&self) -> u64 { + self.max_udp_payload_size.into() + } + + /// Override supported QUIC versions + pub fn supported_versions(&mut self, supported_versions: Vec) -> &mut Self { + self.supported_versions = supported_versions; + self + } + + /// Whether to accept QUIC packets containing any value for the fixed bit + /// + /// Enabled by default. Helps protect against protocol ossification and makes traffic less + /// identifiable to observers. Disable if helping observers identify this traffic as QUIC is + /// desired. + pub fn grease_quic_bit(&mut self, value: bool) -> &mut Self { + self.grease_quic_bit = value; + self + } + + /// Minimum interval between outgoing stateless reset packets + /// + /// Defaults to 20ms. Limits the impact of attacks which flood an endpoint with garbage packets, + /// e.g. [ISAKMP/IKE amplification]. Larger values provide a stronger defense, but may delay + /// detection of some error conditions by clients. Using a [`ConnectionIdGenerator`] with a low + /// rate of false positives in [`validate`](ConnectionIdGenerator::validate) reduces the risk + /// incurred by a small minimum reset interval. + /// + /// [ISAKMP/IKE + /// amplification]: https://bughunters.google.com/blog/5960150648750080/preventing-cross-service-udp-loops-in-quic#isakmp-ike-amplification-vs-quic + pub fn min_reset_interval(&mut self, value: Duration) -> &mut Self { + self.min_reset_interval = value; + self + } + + /// Optional seed to be used internally for random number generation + /// + /// By default, quinn will initialize an endpoint's rng using a platform entropy source. + /// However, you can seed the rng yourself through this method (e.g. if you need to run quinn + /// deterministically or if you are using quinn in an environment that doesn't have a source of + /// entropy available). + pub fn rng_seed(&mut self, seed: Option<[u8; 32]>) -> &mut Self { + self.rng_seed = seed; + self + } +} + +impl fmt::Debug for EndpointConfig { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("EndpointConfig") + // reset_key not debug + .field("max_udp_payload_size", &self.max_udp_payload_size) + // cid_generator_factory not debug + .field("supported_versions", &self.supported_versions) + .field("grease_quic_bit", &self.grease_quic_bit) + .field("rng_seed", &self.rng_seed) + .finish_non_exhaustive() + } +} + +#[cfg(any(feature = "aws-lc-rs", feature = "ring"))] +impl Default for EndpointConfig { + fn default() -> Self { + #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] + use aws_lc_rs::hmac; + use rand::RngCore; + #[cfg(feature = "ring")] + use ring::hmac; + + let mut reset_key = [0; 64]; + rand::rng().fill_bytes(&mut reset_key); + + Self::new(Arc::new(hmac::Key::new(hmac::HMAC_SHA256, &reset_key))) + } +} + +/// Parameters governing incoming connections +/// +/// Default values should be suitable for most internet applications. +#[derive(Clone)] +pub struct ServerConfig { + /// Transport configuration to use for incoming connections + pub transport: Arc, + + /// TLS configuration used for incoming connections + /// + /// Must be set to use TLS 1.3 only. + pub crypto: Arc, + + /// Configuration for sending and handling validation tokens + pub validation_token: ValidationTokenConfig, + + /// Used to generate one-time AEAD keys to protect handshake tokens + pub(crate) token_key: Arc, + + /// Duration after a retry token was issued for which it's considered valid + pub(crate) retry_token_lifetime: Duration, + + /// Whether to allow clients to migrate to new addresses + /// + /// Improves behavior for clients that move between different internet connections or suffer NAT + /// rebinding. Enabled by default. + pub(crate) migration: bool, + + pub(crate) preferred_address_v4: Option, + pub(crate) preferred_address_v6: Option, + + pub(crate) max_incoming: usize, + pub(crate) incoming_buffer_size: u64, + pub(crate) incoming_buffer_size_total: u64, + + pub(crate) time_source: Arc, +} + +impl ServerConfig { + /// Create a default config with a particular handshake token key + pub fn new( + crypto: Arc, + token_key: Arc, + ) -> Self { + Self { + transport: Arc::new(TransportConfig::default()), + crypto, + + token_key, + retry_token_lifetime: Duration::from_secs(15), + + migration: true, + + validation_token: ValidationTokenConfig::default(), + + preferred_address_v4: None, + preferred_address_v6: None, + + max_incoming: 1 << 16, + incoming_buffer_size: 10 << 20, + incoming_buffer_size_total: 100 << 20, + + time_source: Arc::new(StdSystemTime), + } + } + + /// Set a custom [`TransportConfig`] + pub fn transport_config(&mut self, transport: Arc) -> &mut Self { + self.transport = transport; + self + } + + /// Set a custom [`ValidationTokenConfig`] + pub fn validation_token_config( + &mut self, + validation_token: ValidationTokenConfig, + ) -> &mut Self { + self.validation_token = validation_token; + self + } + + /// Private key used to authenticate data included in handshake tokens + pub fn token_key(&mut self, value: Arc) -> &mut Self { + self.token_key = value; + self + } + + /// Duration after a retry token was issued for which it's considered valid + /// + /// Defaults to 15 seconds. + pub fn retry_token_lifetime(&mut self, value: Duration) -> &mut Self { + self.retry_token_lifetime = value; + self + } + + /// Whether to allow clients to migrate to new addresses + /// + /// Improves behavior for clients that move between different internet connections or suffer NAT + /// rebinding. Enabled by default. + pub fn migration(&mut self, value: bool) -> &mut Self { + self.migration = value; + self + } + + /// The preferred IPv4 address that will be communicated to clients during handshaking + /// + /// If the client is able to reach this address, it will switch to it. + pub fn preferred_address_v4(&mut self, address: Option) -> &mut Self { + self.preferred_address_v4 = address; + self + } + + /// The preferred IPv6 address that will be communicated to clients during handshaking + /// + /// If the client is able to reach this address, it will switch to it. + pub fn preferred_address_v6(&mut self, address: Option) -> &mut Self { + self.preferred_address_v6 = address; + self + } + + /// Maximum number of [`Incoming`][crate::Incoming] to allow to exist at a time + /// + /// An [`Incoming`][crate::Incoming] comes into existence when an incoming connection attempt + /// is received and stops existing when the application either accepts it or otherwise disposes + /// of it. While this limit is reached, new incoming connection attempts are immediately + /// refused. Larger values have greater worst-case memory consumption, but accommodate greater + /// application latency in handling incoming connection attempts. + /// + /// The default value is set to 65536. With a typical Ethernet MTU of 1500 bytes, this limits + /// memory consumption from this to under 100 MiB--a generous amount that still prevents memory + /// exhaustion in most contexts. + pub fn max_incoming(&mut self, max_incoming: usize) -> &mut Self { + self.max_incoming = max_incoming; + self + } + + /// Maximum number of received bytes to buffer for each [`Incoming`][crate::Incoming] + /// + /// An [`Incoming`][crate::Incoming] comes into existence when an incoming connection attempt + /// is received and stops existing when the application either accepts it or otherwise disposes + /// of it. This limit governs only packets received within that period, and does not include + /// the first packet. Packets received in excess of this limit are dropped, which may cause + /// 0-RTT or handshake data to have to be retransmitted. + /// + /// The default value is set to 10 MiB--an amount such that in most situations a client would + /// not transmit that much 0-RTT data faster than the server handles the corresponding + /// [`Incoming`][crate::Incoming]. + pub fn incoming_buffer_size(&mut self, incoming_buffer_size: u64) -> &mut Self { + self.incoming_buffer_size = incoming_buffer_size; + self + } + + /// Maximum number of received bytes to buffer for all [`Incoming`][crate::Incoming] + /// collectively + /// + /// An [`Incoming`][crate::Incoming] comes into existence when an incoming connection attempt + /// is received and stops existing when the application either accepts it or otherwise disposes + /// of it. This limit governs only packets received within that period, and does not include + /// the first packet. Packets received in excess of this limit are dropped, which may cause + /// 0-RTT or handshake data to have to be retransmitted. + /// + /// The default value is set to 100 MiB--a generous amount that still prevents memory + /// exhaustion in most contexts. + pub fn incoming_buffer_size_total(&mut self, incoming_buffer_size_total: u64) -> &mut Self { + self.incoming_buffer_size_total = incoming_buffer_size_total; + self + } + + /// Object to get current [`SystemTime`] + /// + /// This exists to allow system time to be mocked in tests, or wherever else desired. + /// + /// Defaults to [`StdSystemTime`], which simply calls [`SystemTime::now()`](SystemTime::now). + pub fn time_source(&mut self, time_source: Arc) -> &mut Self { + self.time_source = time_source; + self + } + + pub(crate) fn has_preferred_address(&self) -> bool { + self.preferred_address_v4.is_some() || self.preferred_address_v6.is_some() + } +} + +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +impl ServerConfig { + /// Create a server config with the given certificate chain to be presented to clients + /// + /// Uses a randomized handshake token key. + pub fn with_single_cert( + cert_chain: Vec>, + key: PrivateKeyDer<'static>, + ) -> Result { + Ok(Self::with_crypto(Arc::new(QuicServerConfig::new( + cert_chain, key, + )?))) + } +} + +#[cfg(any(feature = "aws-lc-rs", feature = "ring"))] +impl ServerConfig { + /// Create a server config with the given [`crypto::ServerConfig`] + /// + /// Uses a randomized handshake token key. + pub fn with_crypto(crypto: Arc) -> Self { + #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] + use aws_lc_rs::hkdf; + use rand::RngCore; + #[cfg(feature = "ring")] + use ring::hkdf; + + let rng = &mut rand::rng(); + let mut master_key = [0u8; 64]; + rng.fill_bytes(&mut master_key); + let master_key = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key); + + Self::new(crypto, Arc::new(master_key)) + } +} + +impl fmt::Debug for ServerConfig { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("ServerConfig") + .field("transport", &self.transport) + // crypto not debug + // token not debug + .field("retry_token_lifetime", &self.retry_token_lifetime) + .field("validation_token", &self.validation_token) + .field("migration", &self.migration) + .field("preferred_address_v4", &self.preferred_address_v4) + .field("preferred_address_v6", &self.preferred_address_v6) + .field("max_incoming", &self.max_incoming) + .field("incoming_buffer_size", &self.incoming_buffer_size) + .field( + "incoming_buffer_size_total", + &self.incoming_buffer_size_total, + ) + // system_time_clock not debug + .finish_non_exhaustive() + } +} + +/// Configuration for sending and handling validation tokens in incoming connections +/// +/// Default values should be suitable for most internet applications. +/// +/// ## QUIC Tokens +/// +/// The QUIC protocol defines a concept of "[address validation][1]". Essentially, one side of a +/// QUIC connection may appear to be receiving QUIC packets from a particular remote UDP address, +/// but it will only consider that remote address "validated" once it has convincing evidence that +/// the address is not being [spoofed][2]. +/// +/// Validation is important primarily because of QUIC's "anti-amplification limit." This limit +/// prevents a QUIC server from sending a client more than three times the number of bytes it has +/// received from the client on a given address until that address is validated. This is designed +/// to mitigate the ability of attackers to use QUIC-based servers as reflectors in [amplification +/// attacks][3]. +/// +/// A path may become validated in several ways. The server is always considered validated by the +/// client. The client usually begins in an unvalidated state upon first connecting or migrating, +/// but then becomes validated through various mechanisms that usually take one network round trip. +/// However, in some cases, a client which has previously attempted to connect to a server may have +/// been given a one-time use cryptographically secured "token" that it can send in a subsequent +/// connection attempt to be validated immediately. +/// +/// There are two ways these tokens can originate: +/// +/// - If the server responds to an incoming connection with `retry`, a "retry token" is minted and +/// sent to the client, which the client immediately uses to attempt to connect again. Retry +/// tokens operate on short timescales, such as 15 seconds. +/// - If a client's path within an active connection is validated, the server may send the client +/// one or more "validation tokens," which the client may store for use in later connections to +/// the same server. Validation tokens may be valid for much longer lifetimes than retry token. +/// +/// The usage of validation tokens is most impactful in situations where 0-RTT data is also being +/// used--in particular, in situations where the server sends the client more than three times more +/// 0.5-RTT data than it has received 0-RTT data. Since the successful completion of a connection +/// handshake implicitly causes the client's address to be validated, transmission of 0.5-RTT data +/// is the main situation where a server might be sending application data to an address that could +/// be validated by token usage earlier than it would become validated without token usage. +/// +/// [1]: https://www.rfc-editor.org/rfc/rfc9000.html#section-8 +/// [2]: https://en.wikipedia.org/wiki/IP_address_spoofing +/// [3]: https://en.wikipedia.org/wiki/Denial-of-service_attack#Amplification +/// +/// These tokens should not be confused with "stateless reset tokens," which are similarly named +/// but entirely unrelated. +#[derive(Clone)] +pub struct ValidationTokenConfig { + pub(crate) lifetime: Duration, + pub(crate) log: Arc, + pub(crate) sent: u32, +} + +impl ValidationTokenConfig { + /// Duration after an address validation token was issued for which it's considered valid + /// + /// This refers only to tokens sent in NEW_TOKEN frames, in contrast to retry tokens. + /// + /// Defaults to 2 weeks. + pub fn lifetime(&mut self, value: Duration) -> &mut Self { + self.lifetime = value; + self + } + + #[allow(rustdoc::redundant_explicit_links)] // which links are redundant depends on features + /// Set a custom [`TokenLog`] + /// + /// If the `bloom` feature is enabled (which it is by default), defaults to a default + /// [`BloomTokenLog`][crate::BloomTokenLog], which is suitable for most internet applications. + /// + /// If the `bloom` feature is disabled, defaults to [`NoneTokenLog`][crate::NoneTokenLog], + /// which makes the server ignore all address validation tokens (that is, tokens originating + /// from NEW_TOKEN frames--retry tokens are not affected). + pub fn log(&mut self, log: Arc) -> &mut Self { + self.log = log; + self + } + + /// Number of address validation tokens sent to a client when its path is validated + /// + /// This refers only to tokens sent in NEW_TOKEN frames, in contrast to retry tokens. + /// + /// If the `bloom` feature is enabled (which it is by default), defaults to 2. Otherwise, + /// defaults to 0. + pub fn sent(&mut self, value: u32) -> &mut Self { + self.sent = value; + self + } +} + +impl Default for ValidationTokenConfig { + fn default() -> Self { + #[cfg(feature = "bloom")] + let log = Arc::new(BloomTokenLog::default()); + #[cfg(not(feature = "bloom"))] + let log = Arc::new(NoneTokenLog); + Self { + lifetime: Duration::from_secs(2 * 7 * 24 * 60 * 60), + log, + sent: if cfg!(feature = "bloom") { 2 } else { 0 }, + } + } +} + +impl fmt::Debug for ValidationTokenConfig { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("ServerValidationTokenConfig") + .field("lifetime", &self.lifetime) + // log not debug + .field("sent", &self.sent) + .finish_non_exhaustive() + } +} + +/// Configuration for outgoing connections +/// +/// Default values should be suitable for most internet applications. +#[derive(Clone)] +#[non_exhaustive] +pub struct ClientConfig { + /// Transport configuration to use + pub(crate) transport: Arc, + + /// Cryptographic configuration to use + pub(crate) crypto: Arc, + + /// Validation token store to use + pub(crate) token_store: Arc, + + /// Provider that populates the destination connection ID of Initial Packets + pub(crate) initial_dst_cid_provider: Arc ConnectionId + Send + Sync>, + + /// QUIC protocol version to use + pub(crate) version: u32, +} + +impl ClientConfig { + /// Create a default config with a particular cryptographic config + pub fn new(crypto: Arc) -> Self { + Self { + transport: Default::default(), + crypto, + token_store: Arc::new(TokenMemoryCache::default()), + initial_dst_cid_provider: Arc::new(|| { + RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid() + }), + version: 1, + } + } + + /// Configure how to populate the destination CID of the initial packet when attempting to + /// establish a new connection + /// + /// By default, it's populated with random bytes with reasonable length, so unless you have + /// a good reason, you do not need to change it. + /// + /// When prefer to override the default, please note that the generated connection ID MUST be + /// at least 8 bytes long and unpredictable, as per section 7.2 of RFC 9000. + pub fn initial_dst_cid_provider( + &mut self, + initial_dst_cid_provider: Arc ConnectionId + Send + Sync>, + ) -> &mut Self { + self.initial_dst_cid_provider = initial_dst_cid_provider; + self + } + + /// Set a custom [`TransportConfig`] + pub fn transport_config(&mut self, transport: Arc) -> &mut Self { + self.transport = transport; + self + } + + /// Set a custom [`TokenStore`] + /// + /// Defaults to [`TokenMemoryCache`], which is suitable for most internet applications. + pub fn token_store(&mut self, store: Arc) -> &mut Self { + self.token_store = store; + self + } + + /// Set the QUIC version to use + pub fn version(&mut self, version: u32) -> &mut Self { + self.version = version; + self + } +} + +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +impl ClientConfig { + /// Create a client configuration that trusts the platform's native roots + #[deprecated(since = "0.11.13", note = "use `try_with_platform_verifier()` instead")] + #[cfg(feature = "platform-verifier")] + pub fn with_platform_verifier() -> Self { + Self::try_with_platform_verifier().expect("use try_with_platform_verifier() instead") + } + + /// Create a client configuration that trusts the platform's native roots + #[cfg(feature = "platform-verifier")] + pub fn try_with_platform_verifier() -> Result { + Ok(Self::new(Arc::new( + crypto::rustls::QuicClientConfig::with_platform_verifier()?, + ))) + } + + /// Create a client configuration that trusts specified trust anchors + pub fn with_root_certificates( + roots: Arc, + ) -> Result { + Ok(Self::new(Arc::new(crypto::rustls::QuicClientConfig::new( + WebPkiServerVerifier::builder_with_provider(roots, configured_provider()).build()?, + )))) + } +} + +impl fmt::Debug for ClientConfig { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("ClientConfig") + .field("transport", &self.transport) + // crypto not debug + // token_store not debug + .field("version", &self.version) + .finish_non_exhaustive() + } +} + +/// Errors in the configuration of an endpoint +#[derive(Debug, Error, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConfigError { + /// Value exceeds supported bounds + #[error("value exceeds supported bounds")] + OutOfBounds, +} + +impl From for ConfigError { + fn from(_: TryFromIntError) -> Self { + Self::OutOfBounds + } +} + +impl From for ConfigError { + fn from(_: VarIntBoundsExceeded) -> Self { + Self::OutOfBounds + } +} + +/// Object to get current [`SystemTime`] +/// +/// This exists to allow system time to be mocked in tests, or wherever else desired. +pub trait TimeSource: Send + Sync { + /// Get [`SystemTime::now()`](SystemTime::now) or the mocked equivalent + fn now(&self) -> SystemTime; +} + +/// Default implementation of [`TimeSource`] +/// +/// Implements `now` by calling [`SystemTime::now()`](SystemTime::now). +pub struct StdSystemTime; + +impl TimeSource for StdSystemTime { + fn now(&self) -> SystemTime { + SystemTime::now() + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/config/transport.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/config/transport.rs new file mode 100644 index 0000000..0198e45 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/config/transport.rs @@ -0,0 +1,785 @@ +use std::{fmt, sync::Arc}; +#[cfg(feature = "qlog")] +use std::{io, sync::Mutex, time::Instant}; + +#[cfg(feature = "qlog")] +use qlog::streamer::QlogStreamer; + +#[cfg(feature = "qlog")] +use crate::QlogStream; +use crate::{ + Duration, INITIAL_MTU, MAX_UDP_PAYLOAD, VarInt, VarIntBoundsExceeded, congestion, + connection::qlog::QlogSink, +}; + +/// Parameters governing the core QUIC state machine +/// +/// Default values should be suitable for most internet applications. Applications protocols which +/// forbid remotely-initiated streams should set `max_concurrent_bidi_streams` and +/// `max_concurrent_uni_streams` to zero. +/// +/// In some cases, performance or resource requirements can be improved by tuning these values to +/// suit a particular application and/or network connection. In particular, data window sizes can be +/// tuned for a particular expected round trip time, link capacity, and memory availability. Tuning +/// for higher bandwidths and latencies increases worst-case memory consumption, but does not impair +/// performance at lower bandwidths and latencies. The default configuration is tuned for a 100Mbps +/// link with a 100ms round trip time. +pub struct TransportConfig { + pub(crate) max_concurrent_bidi_streams: VarInt, + pub(crate) max_concurrent_uni_streams: VarInt, + pub(crate) max_idle_timeout: Option, + pub(crate) stream_receive_window: VarInt, + pub(crate) receive_window: VarInt, + pub(crate) send_window: u64, + pub(crate) send_fairness: bool, + + pub(crate) packet_threshold: u32, + pub(crate) time_threshold: f32, + pub(crate) initial_rtt: Duration, + pub(crate) initial_mtu: u16, + pub(crate) min_mtu: u16, + pub(crate) mtu_discovery_config: Option, + pub(crate) pad_to_mtu: bool, + pub(crate) ack_frequency_config: Option, + + pub(crate) persistent_congestion_threshold: u32, + pub(crate) keep_alive_interval: Option, + pub(crate) crypto_buffer_size: usize, + pub(crate) allow_spin: bool, + pub(crate) datagram_receive_buffer_size: Option, + pub(crate) datagram_send_buffer_size: usize, + #[cfg(test)] + pub(crate) deterministic_packet_numbers: bool, + + pub(crate) congestion_controller_factory: Arc, + + pub(crate) enable_segmentation_offload: bool, + + pub(crate) qlog_sink: QlogSink, +} + +impl TransportConfig { + /// Maximum number of incoming bidirectional streams that may be open concurrently + /// + /// Must be nonzero for the peer to open any bidirectional streams. + /// + /// Worst-case memory use is directly proportional to `max_concurrent_bidi_streams * + /// stream_receive_window`, with an upper bound proportional to `receive_window`. + pub fn max_concurrent_bidi_streams(&mut self, value: VarInt) -> &mut Self { + self.max_concurrent_bidi_streams = value; + self + } + + /// Variant of `max_concurrent_bidi_streams` affecting unidirectional streams + pub fn max_concurrent_uni_streams(&mut self, value: VarInt) -> &mut Self { + self.max_concurrent_uni_streams = value; + self + } + + /// Maximum duration of inactivity to accept before timing out the connection. + /// + /// The true idle timeout is the minimum of this and the peer's own max idle timeout. `None` + /// represents an infinite timeout. Defaults to 30 seconds. + /// + /// **WARNING**: If a peer or its network path malfunctions or acts maliciously, an infinite + /// idle timeout can result in permanently hung futures! + /// + /// ``` + /// # use std::{convert::TryInto, time::Duration}; + /// # use quinn_proto::{TransportConfig, VarInt, VarIntBoundsExceeded}; + /// # fn main() -> Result<(), VarIntBoundsExceeded> { + /// let mut config = TransportConfig::default(); + /// + /// // Set the idle timeout as `VarInt`-encoded milliseconds + /// config.max_idle_timeout(Some(VarInt::from_u32(10_000).into())); + /// + /// // Set the idle timeout as a `Duration` + /// config.max_idle_timeout(Some(Duration::from_secs(10).try_into()?)); + /// # Ok(()) + /// # } + /// ``` + pub fn max_idle_timeout(&mut self, value: Option) -> &mut Self { + self.max_idle_timeout = value.map(|t| t.0); + self + } + + /// Maximum number of bytes the peer may transmit without acknowledgement on any one stream + /// before becoming blocked. + /// + /// This should be set to at least the expected connection latency multiplied by the maximum + /// desired throughput. Setting this smaller than `receive_window` helps ensure that a single + /// stream doesn't monopolize receive buffers, which may otherwise occur if the application + /// chooses not to read from a large stream for a time while still requiring data on other + /// streams. + pub fn stream_receive_window(&mut self, value: VarInt) -> &mut Self { + self.stream_receive_window = value; + self + } + + /// Maximum number of bytes the peer may transmit across all streams of a connection before + /// becoming blocked. + /// + /// This should be set to at least the expected connection latency multiplied by the maximum + /// desired throughput. Larger values can be useful to allow maximum throughput within a + /// stream while another is blocked. + pub fn receive_window(&mut self, value: VarInt) -> &mut Self { + self.receive_window = value; + self + } + + /// Maximum number of bytes to transmit to a peer without acknowledgment + /// + /// Provides an upper bound on memory when communicating with peers that issue large amounts of + /// flow control credit. Endpoints that wish to handle large numbers of connections robustly + /// should take care to set this low enough to guarantee memory exhaustion does not occur if + /// every connection uses the entire window. + pub fn send_window(&mut self, value: u64) -> &mut Self { + self.send_window = value; + self + } + + /// Whether to implement fair queuing for send streams having the same priority. + /// + /// When enabled, connections schedule data from outgoing streams having the same priority in a + /// round-robin fashion. When disabled, streams are scheduled in the order they are written to. + /// + /// Note that this only affects streams with the same priority. Higher priority streams always + /// take precedence over lower priority streams. + /// + /// Disabling fairness can reduce fragmentation and protocol overhead for workloads that use + /// many small streams. + pub fn send_fairness(&mut self, value: bool) -> &mut Self { + self.send_fairness = value; + self + } + + /// Maximum reordering in packet number space before FACK style loss detection considers a + /// packet lost. Should not be less than 3, per RFC5681. + pub fn packet_threshold(&mut self, value: u32) -> &mut Self { + self.packet_threshold = value; + self + } + + /// Maximum reordering in time space before time based loss detection considers a packet lost, + /// as a factor of RTT + pub fn time_threshold(&mut self, value: f32) -> &mut Self { + self.time_threshold = value; + self + } + + /// The RTT used before an RTT sample is taken + pub fn initial_rtt(&mut self, value: Duration) -> &mut Self { + self.initial_rtt = value; + self + } + + /// The initial value to be used as the maximum UDP payload size before running MTU discovery + /// (see [`TransportConfig::mtu_discovery_config`]). + /// + /// Must be at least 1200, which is the default, and known to be safe for typical internet + /// applications. Larger values are more efficient, but increase the risk of packet loss due to + /// exceeding the network path's IP MTU. If the provided value is higher than what the network + /// path actually supports, packet loss will eventually trigger black hole detection and bring + /// it down to [`TransportConfig::min_mtu`]. + pub fn initial_mtu(&mut self, value: u16) -> &mut Self { + self.initial_mtu = value.max(INITIAL_MTU); + self + } + + pub(crate) fn get_initial_mtu(&self) -> u16 { + self.initial_mtu.max(self.min_mtu) + } + + /// The maximum UDP payload size guaranteed to be supported by the network. + /// + /// Must be at least 1200, which is the default, and lower than or equal to + /// [`TransportConfig::initial_mtu`]. + /// + /// Real-world MTUs can vary according to ISP, VPN, and properties of intermediate network links + /// outside of either endpoint's control. Extreme care should be used when raising this value + /// outside of private networks where these factors are fully controlled. If the provided value + /// is higher than what the network path actually supports, the result will be unpredictable and + /// catastrophic packet loss, without a possibility of repair. Prefer + /// [`TransportConfig::initial_mtu`] together with + /// [`TransportConfig::mtu_discovery_config`] to set a maximum UDP payload size that robustly + /// adapts to the network. + pub fn min_mtu(&mut self, value: u16) -> &mut Self { + self.min_mtu = value.max(INITIAL_MTU); + self + } + + /// Specifies the MTU discovery config (see [`MtuDiscoveryConfig`] for details). + /// + /// Enabled by default. + pub fn mtu_discovery_config(&mut self, value: Option) -> &mut Self { + self.mtu_discovery_config = value; + self + } + + /// Pad UDP datagrams carrying application data to current maximum UDP payload size + /// + /// Disabled by default. UDP datagrams containing loss probes are exempt from padding. + /// + /// Enabling this helps mitigate traffic analysis by network observers, but it increases + /// bandwidth usage. Without this mitigation precise plain text size of application datagrams as + /// well as the total size of stream write bursts can be inferred by observers under certain + /// conditions. This analysis requires either an uncongested connection or application datagrams + /// too large to be coalesced. + pub fn pad_to_mtu(&mut self, value: bool) -> &mut Self { + self.pad_to_mtu = value; + self + } + + /// Specifies the ACK frequency config (see [`AckFrequencyConfig`] for details) + /// + /// The provided configuration will be ignored if the peer does not support the acknowledgement + /// frequency QUIC extension. + /// + /// Defaults to `None`, which disables controlling the peer's acknowledgement frequency. Even + /// if set to `None`, the local side still supports the acknowledgement frequency QUIC + /// extension and may use it in other ways. + pub fn ack_frequency_config(&mut self, value: Option) -> &mut Self { + self.ack_frequency_config = value; + self + } + + /// Number of consecutive PTOs after which network is considered to be experiencing persistent congestion. + pub fn persistent_congestion_threshold(&mut self, value: u32) -> &mut Self { + self.persistent_congestion_threshold = value; + self + } + + /// Period of inactivity before sending a keep-alive packet + /// + /// Keep-alive packets prevent an inactive but otherwise healthy connection from timing out. + /// + /// `None` to disable, which is the default. Only one side of any given connection needs keep-alive + /// enabled for the connection to be preserved. Must be set lower than the idle_timeout of both + /// peers to be effective. + pub fn keep_alive_interval(&mut self, value: Option) -> &mut Self { + self.keep_alive_interval = value; + self + } + + /// Maximum quantity of out-of-order crypto layer data to buffer + pub fn crypto_buffer_size(&mut self, value: usize) -> &mut Self { + self.crypto_buffer_size = value; + self + } + + /// Whether the implementation is permitted to set the spin bit on this connection + /// + /// This allows passive observers to easily judge the round trip time of a connection, which can + /// be useful for network administration but sacrifices a small amount of privacy. + pub fn allow_spin(&mut self, value: bool) -> &mut Self { + self.allow_spin = value; + self + } + + /// Maximum number of incoming application datagram bytes to buffer, or None to disable + /// incoming datagrams + /// + /// The peer is forbidden to send single datagrams larger than this size. If the aggregate size + /// of all datagrams that have been received from the peer but not consumed by the application + /// exceeds this value, old datagrams are dropped until it is no longer exceeded. + pub fn datagram_receive_buffer_size(&mut self, value: Option) -> &mut Self { + self.datagram_receive_buffer_size = value; + self + } + + /// Maximum number of outgoing application datagram bytes to buffer + /// + /// While datagrams are sent ASAP, it is possible for an application to generate data faster + /// than the link, or even the underlying hardware, can transmit them. This limits the amount of + /// memory that may be consumed in that case. When the send buffer is full and a new datagram is + /// sent, older datagrams are dropped until sufficient space is available. + pub fn datagram_send_buffer_size(&mut self, value: usize) -> &mut Self { + self.datagram_send_buffer_size = value; + self + } + + /// Whether to force every packet number to be used + /// + /// By default, packet numbers are occasionally skipped to ensure peers aren't ACKing packets + /// before they see them. + #[cfg(test)] + pub(crate) fn deterministic_packet_numbers(&mut self, enabled: bool) -> &mut Self { + self.deterministic_packet_numbers = enabled; + self + } + + /// How to construct new `congestion::Controller`s + /// + /// Typically the refcounted configuration of a `congestion::Controller`, + /// e.g. a `congestion::NewRenoConfig`. + /// + /// # Example + /// ``` + /// # use quinn_proto::*; use std::sync::Arc; + /// let mut config = TransportConfig::default(); + /// config.congestion_controller_factory(Arc::new(congestion::NewRenoConfig::default())); + /// ``` + pub fn congestion_controller_factory( + &mut self, + factory: Arc, + ) -> &mut Self { + self.congestion_controller_factory = factory; + self + } + + /// Whether to use "Generic Segmentation Offload" to accelerate transmits, when supported by the + /// environment + /// + /// Defaults to `true`. + /// + /// GSO dramatically reduces CPU consumption when sending large numbers of packets with the same + /// headers, such as when transmitting bulk data on a connection. However, it is not supported + /// by all network interface drivers or packet inspection tools. `quinn-udp` will attempt to + /// disable GSO automatically when unavailable, but this can lead to spurious packet loss at + /// startup, temporarily degrading performance. + pub fn enable_segmentation_offload(&mut self, enabled: bool) -> &mut Self { + self.enable_segmentation_offload = enabled; + self + } + + /// qlog capture configuration to use for a particular connection + #[cfg(feature = "qlog")] + pub fn qlog_stream(&mut self, stream: Option) -> &mut Self { + self.qlog_sink = stream.into(); + self + } +} + +impl Default for TransportConfig { + fn default() -> Self { + const EXPECTED_RTT: u32 = 100; // ms + const MAX_STREAM_BANDWIDTH: u32 = 12500 * 1000; // bytes/s + // Window size needed to avoid pipeline + // stalls + const STREAM_RWND: u32 = MAX_STREAM_BANDWIDTH / 1000 * EXPECTED_RTT; + + Self { + max_concurrent_bidi_streams: 100u32.into(), + max_concurrent_uni_streams: 100u32.into(), + // 30 second default recommended by RFC 9308 § 3.2 + max_idle_timeout: Some(VarInt(30_000)), + stream_receive_window: STREAM_RWND.into(), + receive_window: VarInt::MAX, + send_window: (8 * STREAM_RWND).into(), + send_fairness: true, + + packet_threshold: 3, + time_threshold: 9.0 / 8.0, + initial_rtt: Duration::from_millis(333), // per spec, intentionally distinct from EXPECTED_RTT + initial_mtu: INITIAL_MTU, + min_mtu: INITIAL_MTU, + mtu_discovery_config: Some(MtuDiscoveryConfig::default()), + pad_to_mtu: false, + ack_frequency_config: None, + + persistent_congestion_threshold: 3, + keep_alive_interval: None, + crypto_buffer_size: 16 * 1024, + allow_spin: true, + datagram_receive_buffer_size: Some(STREAM_RWND as usize), + datagram_send_buffer_size: 1024 * 1024, + #[cfg(test)] + deterministic_packet_numbers: false, + + congestion_controller_factory: Arc::new(congestion::CubicConfig::default()), + + enable_segmentation_offload: true, + + qlog_sink: QlogSink::default(), + } + } +} + +impl fmt::Debug for TransportConfig { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + max_concurrent_bidi_streams, + max_concurrent_uni_streams, + max_idle_timeout, + stream_receive_window, + receive_window, + send_window, + send_fairness, + packet_threshold, + time_threshold, + initial_rtt, + initial_mtu, + min_mtu, + mtu_discovery_config, + pad_to_mtu, + ack_frequency_config, + persistent_congestion_threshold, + keep_alive_interval, + crypto_buffer_size, + allow_spin, + datagram_receive_buffer_size, + datagram_send_buffer_size, + #[cfg(test)] + deterministic_packet_numbers: _, + congestion_controller_factory: _, + enable_segmentation_offload, + qlog_sink, + } = self; + let mut s = fmt.debug_struct("TransportConfig"); + + s.field("max_concurrent_bidi_streams", max_concurrent_bidi_streams) + .field("max_concurrent_uni_streams", max_concurrent_uni_streams) + .field("max_idle_timeout", max_idle_timeout) + .field("stream_receive_window", stream_receive_window) + .field("receive_window", receive_window) + .field("send_window", send_window) + .field("send_fairness", send_fairness) + .field("packet_threshold", packet_threshold) + .field("time_threshold", time_threshold) + .field("initial_rtt", initial_rtt) + .field("initial_mtu", initial_mtu) + .field("min_mtu", min_mtu) + .field("mtu_discovery_config", mtu_discovery_config) + .field("pad_to_mtu", pad_to_mtu) + .field("ack_frequency_config", ack_frequency_config) + .field( + "persistent_congestion_threshold", + persistent_congestion_threshold, + ) + .field("keep_alive_interval", keep_alive_interval) + .field("crypto_buffer_size", crypto_buffer_size) + .field("allow_spin", allow_spin) + .field("datagram_receive_buffer_size", datagram_receive_buffer_size) + .field("datagram_send_buffer_size", datagram_send_buffer_size) + // congestion_controller_factory not debug + .field("enable_segmentation_offload", enable_segmentation_offload); + if cfg!(feature = "qlog") { + s.field("qlog_stream", &qlog_sink.is_enabled()); + } + + s.finish_non_exhaustive() + } +} + +/// Parameters for controlling the peer's acknowledgement frequency +/// +/// The parameters provided in this config will be sent to the peer at the beginning of the +/// connection, so it can take them into account when sending acknowledgements (see each parameter's +/// description for details on how it influences acknowledgement frequency). +/// +/// Quinn's implementation follows the fourth draft of the +/// [QUIC Acknowledgement Frequency extension](https://datatracker.ietf.org/doc/html/draft-ietf-quic-ack-frequency-04). +/// The defaults produce behavior slightly different than the behavior without this extension, +/// because they change the way reordered packets are handled (see +/// [`AckFrequencyConfig::reordering_threshold`] for details). +#[derive(Clone, Debug)] +pub struct AckFrequencyConfig { + pub(crate) ack_eliciting_threshold: VarInt, + pub(crate) max_ack_delay: Option, + pub(crate) reordering_threshold: VarInt, +} + +impl AckFrequencyConfig { + /// The ack-eliciting threshold we will request the peer to use + /// + /// This threshold represents the number of ack-eliciting packets an endpoint may receive + /// without immediately sending an ACK. + /// + /// The remote peer should send at least one ACK frame when more than this number of + /// ack-eliciting packets have been received. A value of 0 results in a receiver immediately + /// acknowledging every ack-eliciting packet. + /// + /// Defaults to 1, which sends ACK frames for every other ack-eliciting packet. + pub fn ack_eliciting_threshold(&mut self, value: VarInt) -> &mut Self { + self.ack_eliciting_threshold = value; + self + } + + /// The `max_ack_delay` we will request the peer to use + /// + /// This parameter represents the maximum amount of time that an endpoint waits before sending + /// an ACK when the ack-eliciting threshold hasn't been reached. + /// + /// The effective `max_ack_delay` will be clamped to be at least the peer's `min_ack_delay` + /// transport parameter, and at most the greater of the current path RTT or 25ms. + /// + /// Defaults to `None`, in which case the peer's original `max_ack_delay` will be used, as + /// obtained from its transport parameters. + pub fn max_ack_delay(&mut self, value: Option) -> &mut Self { + self.max_ack_delay = value; + self + } + + /// The reordering threshold we will request the peer to use + /// + /// This threshold represents the amount of out-of-order packets that will trigger an endpoint + /// to send an ACK, without waiting for `ack_eliciting_threshold` to be exceeded or for + /// `max_ack_delay` to be elapsed. + /// + /// A value of 0 indicates out-of-order packets do not elicit an immediate ACK. A value of 1 + /// immediately acknowledges any packets that are received out of order (this is also the + /// behavior when the extension is disabled). + /// + /// It is recommended to set this value to [`TransportConfig::packet_threshold`] minus one. + /// Since the default value for [`TransportConfig::packet_threshold`] is 3, this value defaults + /// to 2. + pub fn reordering_threshold(&mut self, value: VarInt) -> &mut Self { + self.reordering_threshold = value; + self + } +} + +impl Default for AckFrequencyConfig { + fn default() -> Self { + Self { + ack_eliciting_threshold: VarInt(1), + max_ack_delay: None, + reordering_threshold: VarInt(2), + } + } +} + +/// Configuration for qlog trace logging +#[cfg(feature = "qlog")] +pub struct QlogConfig { + writer: Option>, + title: Option, + description: Option, + start_time: Instant, +} + +#[cfg(feature = "qlog")] +impl QlogConfig { + /// Where to write a qlog `TraceSeq` + pub fn writer(&mut self, writer: Box) -> &mut Self { + self.writer = Some(writer); + self + } + + /// Title to record in the qlog capture + pub fn title(&mut self, title: Option) -> &mut Self { + self.title = title; + self + } + + /// Description to record in the qlog capture + pub fn description(&mut self, description: Option) -> &mut Self { + self.description = description; + self + } + + /// Epoch qlog event times are recorded relative to + pub fn start_time(&mut self, start_time: Instant) -> &mut Self { + self.start_time = start_time; + self + } + + /// Construct the [`QlogStream`] described by this configuration + pub fn into_stream(self) -> Option { + use tracing::warn; + + let writer = self.writer?; + let trace = qlog::TraceSeq::new( + qlog::VantagePoint { + name: None, + ty: qlog::VantagePointType::Unknown, + flow: None, + }, + self.title.clone(), + self.description.clone(), + Some(qlog::Configuration { + time_offset: Some(0.0), + original_uris: None, + }), + None, + ); + + let mut streamer = QlogStreamer::new( + qlog::QLOG_VERSION.into(), + self.title, + self.description, + None, + self.start_time, + trace, + qlog::events::EventImportance::Core, + writer, + ); + + match streamer.start_log() { + Ok(()) => Some(QlogStream(Arc::new(Mutex::new(streamer)))), + Err(e) => { + warn!("could not initialize endpoint qlog streamer: {e}"); + None + } + } + } +} + +#[cfg(feature = "qlog")] +impl Default for QlogConfig { + fn default() -> Self { + Self { + writer: None, + title: None, + description: None, + start_time: Instant::now(), + } + } +} + +/// Parameters governing MTU discovery. +/// +/// # The why of MTU discovery +/// +/// By design, QUIC ensures during the handshake that the network path between the client and the +/// server is able to transmit unfragmented UDP packets with a body of 1200 bytes. In other words, +/// once the connection is established, we know that the network path's maximum transmission unit +/// (MTU) is of at least 1200 bytes (plus IP and UDP headers). Because of this, a QUIC endpoint can +/// split outgoing data in packets of 1200 bytes, with confidence that the network will be able to +/// deliver them (if the endpoint were to send bigger packets, they could prove too big and end up +/// being dropped). +/// +/// There is, however, a significant overhead associated to sending a packet. If the same +/// information can be sent in fewer packets, that results in higher throughput. The amount of +/// packets that need to be sent is inversely proportional to the MTU: the higher the MTU, the +/// bigger the packets that can be sent, and the fewer packets that are needed to transmit a given +/// amount of bytes. +/// +/// Most networks have an MTU higher than 1200. Through MTU discovery, endpoints can detect the +/// path's MTU and, if it turns out to be higher, start sending bigger packets. +/// +/// # MTU discovery internals +/// +/// Quinn implements MTU discovery through DPLPMTUD (Datagram Packetization Layer Path MTU +/// Discovery), described in [section 14.3 of RFC +/// 9000](https://www.rfc-editor.org/rfc/rfc9000.html#section-14.3). This method consists of sending +/// QUIC packets padded to a particular size (called PMTU probes), and waiting to see if the remote +/// peer responds with an ACK. If an ACK is received, that means the probe arrived at the remote +/// peer, which in turn means that the network path's MTU is of at least the packet's size. If the +/// probe is lost, it is sent another 2 times before concluding that the MTU is lower than the +/// packet's size. +/// +/// MTU discovery runs on a schedule (e.g. every 600 seconds) specified through +/// [`MtuDiscoveryConfig::interval`]. The first run happens right after the handshake, and +/// subsequent discoveries are scheduled to run when the interval has elapsed, starting from the +/// last time when MTU discovery completed. +/// +/// Since the search space for MTUs is quite big (the smallest possible MTU is 1200, and the highest +/// is 65527), Quinn performs a binary search to keep the number of probes as low as possible. The +/// lower bound of the search is equal to [`TransportConfig::initial_mtu`] in the +/// initial MTU discovery run, and is equal to the currently discovered MTU in subsequent runs. The +/// upper bound is determined by the minimum of [`MtuDiscoveryConfig::upper_bound`] and the +/// `max_udp_payload_size` transport parameter received from the peer during the handshake. +/// +/// # Black hole detection +/// +/// If, at some point, the network path no longer accepts packets of the detected size, packet loss +/// will eventually trigger black hole detection and reset the detected MTU to 1200. In that case, +/// MTU discovery will be triggered after [`MtuDiscoveryConfig::black_hole_cooldown`] (ignoring the +/// timer that was set based on [`MtuDiscoveryConfig::interval`]). +/// +/// # Interaction between peers +/// +/// There is no guarantee that the MTU on the path between A and B is the same as the MTU of the +/// path between B and A. Therefore, each peer in the connection needs to run MTU discovery +/// independently in order to discover the path's MTU. +#[derive(Clone, Debug)] +pub struct MtuDiscoveryConfig { + pub(crate) interval: Duration, + pub(crate) upper_bound: u16, + pub(crate) minimum_change: u16, + pub(crate) black_hole_cooldown: Duration, +} + +impl MtuDiscoveryConfig { + /// Specifies the time to wait after completing MTU discovery before starting a new MTU + /// discovery run. + /// + /// Defaults to 600 seconds, as recommended by [RFC + /// 8899](https://www.rfc-editor.org/rfc/rfc8899). + pub fn interval(&mut self, value: Duration) -> &mut Self { + self.interval = value; + self + } + + /// Specifies the upper bound to the max UDP payload size that MTU discovery will search for. + /// + /// Defaults to 1452, to stay within Ethernet's MTU when using IPv4 and IPv6. The highest + /// allowed value is 65527, which corresponds to the maximum permitted UDP payload on IPv6. + /// + /// It is safe to use an arbitrarily high upper bound, regardless of the network path's MTU. The + /// only drawback is that MTU discovery might take more time to finish. + pub fn upper_bound(&mut self, value: u16) -> &mut Self { + self.upper_bound = value.min(MAX_UDP_PAYLOAD); + self + } + + /// Specifies the amount of time that MTU discovery should wait after a black hole was detected + /// before running again. Defaults to one minute. + /// + /// Black hole detection can be spuriously triggered in case of congestion, so it makes sense to + /// try MTU discovery again after a short period of time. + pub fn black_hole_cooldown(&mut self, value: Duration) -> &mut Self { + self.black_hole_cooldown = value; + self + } + + /// Specifies the minimum MTU change to stop the MTU discovery phase. + /// Defaults to 20. + pub fn minimum_change(&mut self, value: u16) -> &mut Self { + self.minimum_change = value; + self + } +} + +impl Default for MtuDiscoveryConfig { + fn default() -> Self { + Self { + interval: Duration::from_secs(600), + upper_bound: 1452, + black_hole_cooldown: Duration::from_secs(60), + minimum_change: 20, + } + } +} + +/// Maximum duration of inactivity to accept before timing out the connection +/// +/// This wraps an underlying [`VarInt`], representing the duration in milliseconds. Values can be +/// constructed by converting directly from `VarInt`, or using `TryFrom`. +/// +/// ``` +/// # use std::{convert::TryFrom, time::Duration}; +/// # use quinn_proto::{IdleTimeout, VarIntBoundsExceeded, VarInt}; +/// # fn main() -> Result<(), VarIntBoundsExceeded> { +/// // A `VarInt`-encoded value in milliseconds +/// let timeout = IdleTimeout::from(VarInt::from_u32(10_000)); +/// +/// // Try to convert a `Duration` into a `VarInt`-encoded timeout +/// let timeout = IdleTimeout::try_from(Duration::from_secs(10))?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Default, Copy, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct IdleTimeout(VarInt); + +impl From for IdleTimeout { + fn from(inner: VarInt) -> Self { + Self(inner) + } +} + +impl std::convert::TryFrom for IdleTimeout { + type Error = VarIntBoundsExceeded; + + fn try_from(timeout: Duration) -> Result { + let inner = VarInt::try_from(timeout.as_millis())?; + Ok(Self(inner)) + } +} + +impl fmt::Debug for IdleTimeout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion.rs new file mode 100644 index 0000000..391e4d1 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion.rs @@ -0,0 +1,105 @@ +//! Logic for controlling the rate at which data is sent + +use crate::Instant; +use crate::connection::RttEstimator; +use std::any::Any; +use std::sync::Arc; + +mod bbr; +mod cubic; +mod new_reno; + +pub use bbr::{Bbr, BbrConfig}; +pub use cubic::{Cubic, CubicConfig}; +pub use new_reno::{NewReno, NewRenoConfig}; + +/// Common interface for different congestion controllers +pub trait Controller: Send + Sync { + /// One or more packets were just sent + #[allow(unused_variables)] + fn on_sent(&mut self, now: Instant, bytes: u64, last_packet_number: u64) {} + + /// Packet deliveries were confirmed + /// + /// `app_limited` indicates whether the connection was blocked on outgoing + /// application data prior to receiving these acknowledgements. + #[allow(unused_variables)] + fn on_ack( + &mut self, + now: Instant, + sent: Instant, + bytes: u64, + app_limited: bool, + rtt: &RttEstimator, + ) { + } + + /// Packets are acked in batches, all with the same `now` argument. This indicates one of those batches has completed. + #[allow(unused_variables)] + fn on_end_acks( + &mut self, + now: Instant, + in_flight: u64, + app_limited: bool, + largest_packet_num_acked: Option, + ) { + } + + /// Packets were deemed lost or marked congested + /// + /// `in_persistent_congestion` indicates whether all packets sent within the persistent + /// congestion threshold period ending when the most recent packet in this batch was sent were + /// lost. + /// `lost_bytes` indicates how many bytes were lost. This value will be 0 for ECN triggers. + fn on_congestion_event( + &mut self, + now: Instant, + sent: Instant, + is_persistent_congestion: bool, + lost_bytes: u64, + ); + + /// The known MTU for the current network path has been updated + fn on_mtu_update(&mut self, new_mtu: u16); + + /// Number of ack-eliciting bytes that may be in flight + fn window(&self) -> u64; + + /// Retrieve implementation-specific metrics used to populate `qlog` traces when they are enabled + fn metrics(&self) -> ControllerMetrics { + ControllerMetrics { + congestion_window: self.window(), + ssthresh: None, + pacing_rate: None, + } + } + + /// Duplicate the controller's state + fn clone_box(&self) -> Box; + + /// Initial congestion window + fn initial_window(&self) -> u64; + + /// Returns Self for use in down-casting to extract implementation details + fn into_any(self: Box) -> Box; +} + +/// Common congestion controller metrics +#[derive(Default)] +#[non_exhaustive] +pub struct ControllerMetrics { + /// Congestion window (bytes) + pub congestion_window: u64, + /// Slow start threshold (bytes) + pub ssthresh: Option, + /// Pacing rate (bits/s) + pub pacing_rate: Option, +} + +/// Constructs controllers on demand +pub trait ControllerFactory { + /// Construct a fresh `Controller` + fn build(self: Arc, now: Instant, current_mtu: u16) -> Box; +} + +const BASE_DATAGRAM_SIZE: u64 = 1200; diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/bw_estimation.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/bw_estimation.rs new file mode 100644 index 0000000..84ea4e6 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/bw_estimation.rs @@ -0,0 +1,101 @@ +use std::fmt::{Debug, Display, Formatter}; + +use super::min_max::MinMax; +use crate::{Duration, Instant}; + +#[derive(Clone, Debug, Default)] +pub(crate) struct BandwidthEstimation { + total_acked: u64, + prev_total_acked: u64, + acked_time: Option, + prev_acked_time: Option, + total_sent: u64, + prev_total_sent: u64, + sent_time: Option, + prev_sent_time: Option, + max_filter: MinMax, + acked_at_last_window: u64, +} + +impl BandwidthEstimation { + pub(crate) fn on_sent(&mut self, now: Instant, bytes: u64) { + self.prev_total_sent = self.total_sent; + self.total_sent += bytes; + self.prev_sent_time = self.sent_time; + self.sent_time = Some(now); + } + + pub(crate) fn on_ack( + &mut self, + now: Instant, + _sent: Instant, + bytes: u64, + round: u64, + app_limited: bool, + ) { + self.prev_total_acked = self.total_acked; + self.total_acked += bytes; + self.prev_acked_time = self.acked_time; + self.acked_time = Some(now); + + let prev_sent_time = match self.prev_sent_time { + Some(prev_sent_time) => prev_sent_time, + None => return, + }; + + let send_rate = match self.sent_time { + Some(sent_time) if sent_time > prev_sent_time => Self::bw_from_delta( + self.total_sent - self.prev_total_sent, + sent_time - prev_sent_time, + ) + .unwrap_or(0), + _ => u64::MAX, // will take the min of send and ack, so this is just a skip + }; + + let ack_rate = match self.prev_acked_time { + Some(prev_acked_time) => Self::bw_from_delta( + self.total_acked - self.prev_total_acked, + now - prev_acked_time, + ) + .unwrap_or(0), + None => 0, + }; + + let bandwidth = send_rate.min(ack_rate); + if !app_limited && self.max_filter.get() < bandwidth { + self.max_filter.update_max(round, bandwidth); + } + } + + pub(crate) fn bytes_acked_this_window(&self) -> u64 { + self.total_acked - self.acked_at_last_window + } + + pub(crate) fn end_acks(&mut self, _current_round: u64, _app_limited: bool) { + self.acked_at_last_window = self.total_acked; + } + + pub(crate) fn get_estimate(&self) -> u64 { + self.max_filter.get() + } + + pub(crate) const fn bw_from_delta(bytes: u64, delta: Duration) -> Option { + let window_duration_ns = delta.as_nanos(); + if window_duration_ns == 0 { + return None; + } + let b_ns = bytes * 1_000_000_000; + let bytes_per_second = b_ns / (window_duration_ns as u64); + Some(bytes_per_second) + } +} + +impl Display for BandwidthEstimation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:.3} MB/s", + self.get_estimate() as f32 / (1024 * 1024) as f32 + ) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/min_max.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/min_max.rs new file mode 100644 index 0000000..97a701b --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/min_max.rs @@ -0,0 +1,152 @@ +/* + * Based on Google code released under BSD license here: + * https://groups.google.com/forum/#!topic/bbr-dev/3RTgkzi5ZD8 + */ + +/* + * Kathleen Nichols' algorithm for tracking the minimum (or maximum) + * value of a data stream over some fixed time interval. (E.g., + * the minimum RTT over the past five minutes.) It uses constant + * space and constant time per update yet almost always delivers + * the same minimum as an implementation that has to keep all the + * data in the window. + * + * The algorithm keeps track of the best, 2nd best & 3rd best min + * values, maintaining an invariant that the measurement time of + * the n'th best >= n-1'th best. It also makes sure that the three + * values are widely separated in the time window since that bounds + * the worse case error when that data is monotonically increasing + * over the window. + * + * Upon getting a new min, we can forget everything earlier because + * it has no value - the new min is <= everything else in the window + * by definition and it samples the most recent. So we restart fresh on + * every new min and overwrites 2nd & 3rd choices. The same property + * holds for 2nd & 3rd best. + */ + +use std::fmt::Debug; + +#[derive(Copy, Clone, Debug)] +pub(super) struct MinMax { + /// round count, not a timestamp + window: u64, + samples: [MinMaxSample; 3], +} + +impl MinMax { + pub(super) fn get(&self) -> u64 { + self.samples[0].value + } + + fn fill(&mut self, sample: MinMaxSample) { + self.samples.fill(sample); + } + + pub(super) fn reset(&mut self) { + self.fill(Default::default()) + } + + /// update_min is also defined in the original source, but removed here since it is not used. + pub(super) fn update_max(&mut self, current_round: u64, measurement: u64) { + let sample = MinMaxSample { + time: current_round, + value: measurement, + }; + + if self.samples[0].value == 0 /* uninitialised */ + || /* found new max? */ sample.value >= self.samples[0].value + || /* nothing left in window? */ sample.time - self.samples[2].time > self.window + { + self.fill(sample); /* forget earlier samples */ + return; + } + + if sample.value >= self.samples[1].value { + self.samples[2] = sample; + self.samples[1] = sample; + } else if sample.value >= self.samples[2].value { + self.samples[2] = sample; + } + + self.subwin_update(sample); + } + + /* As time advances, update the 1st, 2nd, and 3rd choices. */ + fn subwin_update(&mut self, sample: MinMaxSample) { + let dt = sample.time - self.samples[0].time; + if dt > self.window { + /* + * Passed entire window without a new sample so make 2nd + * choice the new sample & 3rd choice the new 2nd choice. + * we may have to iterate this since our 2nd choice + * may also be outside the window (we checked on entry + * that the third choice was in the window). + */ + self.samples[0] = self.samples[1]; + self.samples[1] = self.samples[2]; + self.samples[2] = sample; + if sample.time - self.samples[0].time > self.window { + self.samples[0] = self.samples[1]; + self.samples[1] = self.samples[2]; + self.samples[2] = sample; + } + } else if self.samples[1].time == self.samples[0].time && dt > self.window / 4 { + /* + * We've passed a quarter of the window without a new sample + * so take a 2nd choice from the 2nd quarter of the window. + */ + self.samples[2] = sample; + self.samples[1] = sample; + } else if self.samples[2].time == self.samples[1].time && dt > self.window / 2 { + /* + * We've passed half the window without finding a new sample + * so take a 3rd choice from the last half of the window + */ + self.samples[2] = sample; + } + } +} + +impl Default for MinMax { + fn default() -> Self { + Self { + window: 10, + samples: [Default::default(); 3], + } + } +} + +#[derive(Debug, Copy, Clone, Default)] +struct MinMaxSample { + /// round number, not a timestamp + time: u64, + value: u64, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test() { + let round = 25; + let mut min_max = MinMax::default(); + min_max.update_max(round + 1, 100); + assert_eq!(100, min_max.get()); + min_max.update_max(round + 3, 120); + assert_eq!(120, min_max.get()); + min_max.update_max(round + 5, 160); + assert_eq!(160, min_max.get()); + min_max.update_max(round + 7, 100); + assert_eq!(160, min_max.get()); + min_max.update_max(round + 10, 100); + assert_eq!(160, min_max.get()); + min_max.update_max(round + 14, 100); + assert_eq!(160, min_max.get()); + min_max.update_max(round + 16, 100); + assert_eq!(100, min_max.get()); + min_max.update_max(round + 18, 130); + assert_eq!(130, min_max.get()); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/mod.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/mod.rs new file mode 100644 index 0000000..edf356f --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/bbr/mod.rs @@ -0,0 +1,650 @@ +use std::any::Any; +use std::fmt::Debug; +use std::sync::Arc; + +use rand::{Rng, SeedableRng}; + +use crate::congestion::ControllerMetrics; +use crate::congestion::bbr::bw_estimation::BandwidthEstimation; +use crate::congestion::bbr::min_max::MinMax; +use crate::connection::RttEstimator; +use crate::{Duration, Instant}; + +use super::{BASE_DATAGRAM_SIZE, Controller, ControllerFactory}; + +mod bw_estimation; +mod min_max; + +/// Experimental! Use at your own risk. +/// +/// Aims for reduced buffer bloat and improved performance over high bandwidth-delay product networks. +/// Based on google's quiche implementation +/// of BBR . +/// More discussion and links at . +#[derive(Debug, Clone)] +pub struct Bbr { + config: Arc, + current_mtu: u64, + max_bandwidth: BandwidthEstimation, + acked_bytes: u64, + mode: Mode, + loss_state: LossState, + recovery_state: RecoveryState, + recovery_window: u64, + is_at_full_bandwidth: bool, + pacing_gain: f32, + high_gain: f32, + drain_gain: f32, + cwnd_gain: f32, + high_cwnd_gain: f32, + last_cycle_start: Option, + current_cycle_offset: u8, + init_cwnd: u64, + min_cwnd: u64, + prev_in_flight_count: u64, + exit_probe_rtt_at: Option, + probe_rtt_last_started_at: Option, + min_rtt: Duration, + exiting_quiescence: bool, + pacing_rate: u64, + max_acked_packet_number: u64, + max_sent_packet_number: u64, + end_recovery_at_packet_number: u64, + cwnd: u64, + current_round_trip_end_packet_number: u64, + round_count: u64, + bw_at_last_round: u64, + round_wo_bw_gain: u64, + ack_aggregation: AckAggregationState, + random_number_generator: rand::rngs::StdRng, +} + +impl Bbr { + /// Construct a state using the given `config` and current time `now` + pub fn new(config: Arc, current_mtu: u16) -> Self { + let initial_window = config.initial_window; + Self { + config, + current_mtu: current_mtu as u64, + max_bandwidth: BandwidthEstimation::default(), + acked_bytes: 0, + mode: Mode::Startup, + loss_state: Default::default(), + recovery_state: RecoveryState::NotInRecovery, + recovery_window: 0, + is_at_full_bandwidth: false, + pacing_gain: K_DEFAULT_HIGH_GAIN, + high_gain: K_DEFAULT_HIGH_GAIN, + drain_gain: 1.0 / K_DEFAULT_HIGH_GAIN, + cwnd_gain: K_DEFAULT_HIGH_GAIN, + high_cwnd_gain: K_DEFAULT_HIGH_GAIN, + last_cycle_start: None, + current_cycle_offset: 0, + init_cwnd: initial_window, + min_cwnd: calculate_min_window(current_mtu as u64), + prev_in_flight_count: 0, + exit_probe_rtt_at: None, + probe_rtt_last_started_at: None, + min_rtt: Default::default(), + exiting_quiescence: false, + pacing_rate: 0, + max_acked_packet_number: 0, + max_sent_packet_number: 0, + end_recovery_at_packet_number: 0, + cwnd: initial_window, + current_round_trip_end_packet_number: 0, + round_count: 0, + bw_at_last_round: 0, + round_wo_bw_gain: 0, + ack_aggregation: AckAggregationState::default(), + random_number_generator: rand::rngs::StdRng::from_os_rng(), + } + } + + fn enter_startup_mode(&mut self) { + self.mode = Mode::Startup; + self.pacing_gain = self.high_gain; + self.cwnd_gain = self.high_cwnd_gain; + } + + fn enter_probe_bandwidth_mode(&mut self, now: Instant) { + self.mode = Mode::ProbeBw; + self.cwnd_gain = K_DERIVED_HIGH_CWNDGAIN; + self.last_cycle_start = Some(now); + // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is + // excluded because in that case increased gain and decreased gain would not + // follow each other. + let mut rand_index = self + .random_number_generator + .random_range(0..K_PACING_GAIN.len() as u8 - 1); + if rand_index >= 1 { + rand_index += 1; + } + self.current_cycle_offset = rand_index; + self.pacing_gain = K_PACING_GAIN[rand_index as usize]; + } + + fn update_recovery_state(&mut self, is_round_start: bool) { + // Exit recovery when there are no losses for a round. + if self.loss_state.has_losses() { + self.end_recovery_at_packet_number = self.max_sent_packet_number; + } + match self.recovery_state { + // Enter conservation on the first loss. + RecoveryState::NotInRecovery if self.loss_state.has_losses() => { + self.recovery_state = RecoveryState::Conservation; + // This will cause the |recovery_window| to be set to the + // correct value in CalculateRecoveryWindow(). + self.recovery_window = 0; + // Since the conservation phase is meant to be lasting for a whole + // round, extend the current round as if it were started right now. + self.current_round_trip_end_packet_number = self.max_sent_packet_number; + } + RecoveryState::Growth | RecoveryState::Conservation => { + if self.recovery_state == RecoveryState::Conservation && is_round_start { + self.recovery_state = RecoveryState::Growth; + } + // Exit recovery if appropriate. + if !self.loss_state.has_losses() + && self.max_acked_packet_number > self.end_recovery_at_packet_number + { + self.recovery_state = RecoveryState::NotInRecovery; + } + } + _ => {} + } + } + + fn update_gain_cycle_phase(&mut self, now: Instant, in_flight: u64) { + // In most cases, the cycle is advanced after an RTT passes. + let mut should_advance_gain_cycling = self + .last_cycle_start + .map(|last_cycle_start| now.duration_since(last_cycle_start) > self.min_rtt) + .unwrap_or(false); + // If the pacing gain is above 1.0, the connection is trying to probe the + // bandwidth by increasing the number of bytes in flight to at least + // pacing_gain * BDP. Make sure that it actually reaches the target, as + // long as there are no losses suggesting that the buffers are not able to + // hold that much. + if self.pacing_gain > 1.0 + && !self.loss_state.has_losses() + && self.prev_in_flight_count < self.get_target_cwnd(self.pacing_gain) + { + should_advance_gain_cycling = false; + } + + // If pacing gain is below 1.0, the connection is trying to drain the extra + // queue which could have been incurred by probing prior to it. If the + // number of bytes in flight falls down to the estimated BDP value earlier, + // conclude that the queue has been successfully drained and exit this cycle + // early. + if self.pacing_gain < 1.0 && in_flight <= self.get_target_cwnd(1.0) { + should_advance_gain_cycling = true; + } + + if should_advance_gain_cycling { + self.current_cycle_offset = (self.current_cycle_offset + 1) % K_PACING_GAIN.len() as u8; + self.last_cycle_start = Some(now); + // Stay in low gain mode until the target BDP is hit. Low gain mode + // will be exited immediately when the target BDP is achieved. + if DRAIN_TO_TARGET + && self.pacing_gain < 1.0 + && (K_PACING_GAIN[self.current_cycle_offset as usize] - 1.0).abs() < f32::EPSILON + && in_flight > self.get_target_cwnd(1.0) + { + return; + } + self.pacing_gain = K_PACING_GAIN[self.current_cycle_offset as usize]; + } + } + + fn maybe_exit_startup_or_drain(&mut self, now: Instant, in_flight: u64) { + if self.mode == Mode::Startup && self.is_at_full_bandwidth { + self.mode = Mode::Drain; + self.pacing_gain = self.drain_gain; + self.cwnd_gain = self.high_cwnd_gain; + } + if self.mode == Mode::Drain && in_flight <= self.get_target_cwnd(1.0) { + self.enter_probe_bandwidth_mode(now); + } + } + + fn is_min_rtt_expired(&self, now: Instant, app_limited: bool) -> bool { + !app_limited + && self + .probe_rtt_last_started_at + .map(|last| now.saturating_duration_since(last) > Duration::from_secs(10)) + .unwrap_or(true) + } + + fn maybe_enter_or_exit_probe_rtt( + &mut self, + now: Instant, + is_round_start: bool, + bytes_in_flight: u64, + app_limited: bool, + ) { + let min_rtt_expired = self.is_min_rtt_expired(now, app_limited); + if min_rtt_expired && !self.exiting_quiescence && self.mode != Mode::ProbeRtt { + self.mode = Mode::ProbeRtt; + self.pacing_gain = 1.0; + // Do not decide on the time to exit ProbeRtt until the + // |bytes_in_flight| is at the target small value. + self.exit_probe_rtt_at = None; + self.probe_rtt_last_started_at = Some(now); + } + + if self.mode == Mode::ProbeRtt { + match self.exit_probe_rtt_at { + None => { + // If the window has reached the appropriate size, schedule exiting + // ProbeRtt. The CWND during ProbeRtt is + // kMinimumCongestionWindow, but we allow an extra packet since QUIC + // checks CWND before sending a packet. + if bytes_in_flight < self.get_probe_rtt_cwnd() + self.current_mtu { + const K_PROBE_RTT_TIME: Duration = Duration::from_millis(200); + self.exit_probe_rtt_at = Some(now + K_PROBE_RTT_TIME); + } + } + Some(exit_time) if is_round_start && now >= exit_time => { + if !self.is_at_full_bandwidth { + self.enter_startup_mode(); + } else { + self.enter_probe_bandwidth_mode(now); + } + } + Some(_) => {} + } + } + + self.exiting_quiescence = false; + } + + fn get_target_cwnd(&self, gain: f32) -> u64 { + let bw = self.max_bandwidth.get_estimate(); + let bdp = self.min_rtt.as_micros() as u64 * bw; + let bdpf = bdp as f64; + let cwnd = ((gain as f64 * bdpf) / 1_000_000f64) as u64; + // BDP estimate will be zero if no bandwidth samples are available yet. + if cwnd == 0 { + return self.init_cwnd; + } + cwnd.max(self.min_cwnd) + } + + fn get_probe_rtt_cwnd(&self) -> u64 { + const K_MODERATE_PROBE_RTT_MULTIPLIER: f32 = 0.75; + if PROBE_RTT_BASED_ON_BDP { + return self.get_target_cwnd(K_MODERATE_PROBE_RTT_MULTIPLIER); + } + self.min_cwnd + } + + fn calculate_pacing_rate(&mut self) { + let bw = self.max_bandwidth.get_estimate(); + if bw == 0 { + return; + } + let target_rate = (bw as f64 * self.pacing_gain as f64) as u64; + if self.is_at_full_bandwidth { + self.pacing_rate = target_rate; + return; + } + + // Pace at the rate of initial_window / RTT as soon as RTT measurements are + // available. + if self.pacing_rate == 0 && self.min_rtt.as_nanos() != 0 { + self.pacing_rate = + BandwidthEstimation::bw_from_delta(self.init_cwnd, self.min_rtt).unwrap(); + return; + } + + // Do not decrease the pacing rate during startup. + if self.pacing_rate < target_rate { + self.pacing_rate = target_rate; + } + } + + fn calculate_cwnd(&mut self, bytes_acked: u64, excess_acked: u64) { + if self.mode == Mode::ProbeRtt { + return; + } + let mut target_window = self.get_target_cwnd(self.cwnd_gain); + if self.is_at_full_bandwidth { + // Add the max recently measured ack aggregation to CWND. + target_window += self.ack_aggregation.max_ack_height.get(); + } else { + // Add the most recent excess acked. Because CWND never decreases in + // STARTUP, this will automatically create a very localized max filter. + target_window += excess_acked; + } + // Instead of immediately setting the target CWND as the new one, BBR grows + // the CWND towards |target_window| by only increasing it |bytes_acked| at a + // time. + if self.is_at_full_bandwidth { + self.cwnd = target_window.min(self.cwnd + bytes_acked); + } else if (self.cwnd_gain < target_window as f32) || (self.acked_bytes < self.init_cwnd) { + // If the connection is not yet out of startup phase, do not decrease + // the window. + self.cwnd += bytes_acked; + } + + // Enforce the limits on the congestion window. + if self.cwnd < self.min_cwnd { + self.cwnd = self.min_cwnd; + } + } + + fn calculate_recovery_window(&mut self, bytes_acked: u64, bytes_lost: u64, in_flight: u64) { + if !self.recovery_state.in_recovery() { + return; + } + // Set up the initial recovery window. + if self.recovery_window == 0 { + self.recovery_window = self.min_cwnd.max(in_flight + bytes_acked); + return; + } + + // Remove losses from the recovery window, while accounting for a potential + // integer underflow. + if self.recovery_window >= bytes_lost { + self.recovery_window -= bytes_lost; + } else { + // k_max_segment_size = current_mtu + self.recovery_window = self.current_mtu; + } + // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, + // release additional |bytes_acked| to achieve a slow-start-like behavior. + if self.recovery_state == RecoveryState::Growth { + self.recovery_window += bytes_acked; + } + + // Sanity checks. Ensure that we always allow to send at least an MSS or + // |bytes_acked| in response, whichever is larger. + self.recovery_window = self + .recovery_window + .max(in_flight + bytes_acked) + .max(self.min_cwnd); + } + + /// + fn check_if_full_bw_reached(&mut self, app_limited: bool) { + if app_limited { + return; + } + let target = (self.bw_at_last_round as f64 * K_STARTUP_GROWTH_TARGET as f64) as u64; + let bw = self.max_bandwidth.get_estimate(); + if bw >= target { + self.bw_at_last_round = bw; + self.round_wo_bw_gain = 0; + self.ack_aggregation.max_ack_height.reset(); + return; + } + + self.round_wo_bw_gain += 1; + if self.round_wo_bw_gain >= K_ROUND_TRIPS_WITHOUT_GROWTH_BEFORE_EXITING_STARTUP as u64 + || (self.recovery_state.in_recovery()) + { + self.is_at_full_bandwidth = true; + } + } +} + +impl Controller for Bbr { + fn on_sent(&mut self, now: Instant, bytes: u64, last_packet_number: u64) { + self.max_sent_packet_number = last_packet_number; + self.max_bandwidth.on_sent(now, bytes); + } + + fn on_ack( + &mut self, + now: Instant, + sent: Instant, + bytes: u64, + app_limited: bool, + rtt: &RttEstimator, + ) { + self.max_bandwidth + .on_ack(now, sent, bytes, self.round_count, app_limited); + self.acked_bytes += bytes; + if self.is_min_rtt_expired(now, app_limited) || self.min_rtt > rtt.min() { + self.min_rtt = rtt.min(); + } + } + + fn on_end_acks( + &mut self, + now: Instant, + in_flight: u64, + app_limited: bool, + largest_packet_num_acked: Option, + ) { + let bytes_acked = self.max_bandwidth.bytes_acked_this_window(); + let excess_acked = self.ack_aggregation.update_ack_aggregation_bytes( + bytes_acked, + now, + self.round_count, + self.max_bandwidth.get_estimate(), + ); + self.max_bandwidth.end_acks(self.round_count, app_limited); + if let Some(largest_acked_packet) = largest_packet_num_acked { + self.max_acked_packet_number = largest_acked_packet; + } + + let mut is_round_start = false; + if bytes_acked > 0 { + is_round_start = + self.max_acked_packet_number > self.current_round_trip_end_packet_number; + if is_round_start { + self.current_round_trip_end_packet_number = self.max_sent_packet_number; + self.round_count += 1; + } + } + + self.update_recovery_state(is_round_start); + + if self.mode == Mode::ProbeBw { + self.update_gain_cycle_phase(now, in_flight); + } + + if is_round_start && !self.is_at_full_bandwidth { + self.check_if_full_bw_reached(app_limited); + } + + self.maybe_exit_startup_or_drain(now, in_flight); + + self.maybe_enter_or_exit_probe_rtt(now, is_round_start, in_flight, app_limited); + + // After the model is updated, recalculate the pacing rate and congestion window. + self.calculate_pacing_rate(); + self.calculate_cwnd(bytes_acked, excess_acked); + self.calculate_recovery_window(bytes_acked, self.loss_state.lost_bytes, in_flight); + + self.prev_in_flight_count = in_flight; + self.loss_state.reset(); + } + + fn on_congestion_event( + &mut self, + _now: Instant, + _sent: Instant, + _is_persistent_congestion: bool, + lost_bytes: u64, + ) { + self.loss_state.lost_bytes += lost_bytes; + } + + fn on_mtu_update(&mut self, new_mtu: u16) { + self.current_mtu = new_mtu as u64; + self.min_cwnd = calculate_min_window(self.current_mtu); + self.init_cwnd = self.config.initial_window.max(self.min_cwnd); + self.cwnd = self.cwnd.max(self.min_cwnd); + } + + fn window(&self) -> u64 { + if self.mode == Mode::ProbeRtt { + return self.get_probe_rtt_cwnd(); + } else if self.recovery_state.in_recovery() && self.mode != Mode::Startup { + return self.cwnd.min(self.recovery_window); + } + self.cwnd + } + + fn metrics(&self) -> ControllerMetrics { + ControllerMetrics { + congestion_window: self.window(), + ssthresh: None, + pacing_rate: Some(self.pacing_rate * 8), + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn initial_window(&self) -> u64 { + self.config.initial_window + } + + fn into_any(self: Box) -> Box { + self + } +} + +/// Configuration for the [`Bbr`] congestion controller +#[derive(Debug, Clone)] +pub struct BbrConfig { + initial_window: u64, +} + +impl BbrConfig { + /// Default limit on the amount of outstanding data in bytes. + /// + /// Recommended value: `min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))` + pub fn initial_window(&mut self, value: u64) -> &mut Self { + self.initial_window = value; + self + } +} + +impl Default for BbrConfig { + fn default() -> Self { + Self { + initial_window: K_MAX_INITIAL_CONGESTION_WINDOW * BASE_DATAGRAM_SIZE, + } + } +} + +impl ControllerFactory for BbrConfig { + fn build(self: Arc, _now: Instant, current_mtu: u16) -> Box { + Box::new(Bbr::new(self, current_mtu)) + } +} + +#[derive(Debug, Default, Copy, Clone)] +struct AckAggregationState { + max_ack_height: MinMax, + aggregation_epoch_start_time: Option, + aggregation_epoch_bytes: u64, +} + +impl AckAggregationState { + fn update_ack_aggregation_bytes( + &mut self, + newly_acked_bytes: u64, + now: Instant, + round: u64, + max_bandwidth: u64, + ) -> u64 { + // Compute how many bytes are expected to be delivered, assuming max + // bandwidth is correct. + let expected_bytes_acked = max_bandwidth + * now + .saturating_duration_since(self.aggregation_epoch_start_time.unwrap_or(now)) + .as_micros() as u64 + / 1_000_000; + + // Reset the current aggregation epoch as soon as the ack arrival rate is + // less than or equal to the max bandwidth. + if self.aggregation_epoch_bytes <= expected_bytes_acked { + // Reset to start measuring a new aggregation epoch. + self.aggregation_epoch_bytes = newly_acked_bytes; + self.aggregation_epoch_start_time = Some(now); + return 0; + } + + // Compute how many extra bytes were delivered vs max bandwidth. + // Include the bytes most recently acknowledged to account for stretch acks. + self.aggregation_epoch_bytes += newly_acked_bytes; + let diff = self.aggregation_epoch_bytes - expected_bytes_acked; + self.max_ack_height.update_max(round, diff); + diff + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum Mode { + // Startup phase of the connection. + Startup, + // After achieving the highest possible bandwidth during the startup, lower + // the pacing rate in order to drain the queue. + Drain, + // Cruising mode. + ProbeBw, + // Temporarily slow down sending in order to empty the buffer and measure + // the real minimum RTT. + ProbeRtt, +} + +// Indicates how the congestion control limits the amount of bytes in flight. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum RecoveryState { + // Do not limit. + NotInRecovery, + // Allow an extra outstanding byte for each byte acknowledged. + Conservation, + // Allow two extra outstanding bytes for each byte acknowledged (slow + // start). + Growth, +} + +impl RecoveryState { + pub(super) fn in_recovery(&self) -> bool { + !matches!(self, Self::NotInRecovery) + } +} + +#[derive(Debug, Clone, Default)] +struct LossState { + lost_bytes: u64, +} + +impl LossState { + pub(super) fn reset(&mut self) { + self.lost_bytes = 0; + } + + pub(super) fn has_losses(&self) -> bool { + self.lost_bytes != 0 + } +} + +fn calculate_min_window(current_mtu: u64) -> u64 { + 4 * current_mtu +} + +// The gain used for the STARTUP, equal to 2/ln(2). +const K_DEFAULT_HIGH_GAIN: f32 = 2.885; +// The newly derived CWND gain for STARTUP, 2. +const K_DERIVED_HIGH_CWNDGAIN: f32 = 2.0; +// The cycle of gains used during the ProbeBw stage. +const K_PACING_GAIN: [f32; 8] = [1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]; + +const K_STARTUP_GROWTH_TARGET: f32 = 1.25; +const K_ROUND_TRIPS_WITHOUT_GROWTH_BEFORE_EXITING_STARTUP: u8 = 3; + +// Do not allow initial congestion window to be greater than 200 packets. +const K_MAX_INITIAL_CONGESTION_WINDOW: u64 = 200; + +const PROBE_RTT_BASED_ON_BDP: bool = true; +const DRAIN_TO_TARGET: bool = true; diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/cubic.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/cubic.rs new file mode 100644 index 0000000..1bd9c69 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/cubic.rs @@ -0,0 +1,272 @@ +use std::any::Any; +use std::cmp; +use std::sync::Arc; + +use super::{BASE_DATAGRAM_SIZE, Controller, ControllerFactory}; +use crate::connection::RttEstimator; +use crate::{Duration, Instant}; + +/// CUBIC Constants. +/// +/// These are recommended value in RFC8312. +const BETA_CUBIC: f64 = 0.7; + +const C: f64 = 0.4; + +/// CUBIC State Variables. +/// +/// We need to keep those variables across the connection. +/// k, w_max are described in the RFC. +#[derive(Debug, Default, Clone)] +pub(super) struct State { + k: f64, + + w_max: f64, + + // Store cwnd increment during congestion avoidance. + cwnd_inc: u64, +} + +/// CUBIC Functions. +/// +/// Note that these calculations are based on a count of cwnd as bytes, +/// not packets. +/// Unit of t (duration) and RTT are based on seconds (f64). +impl State { + // K = cbrt(w_max * (1 - beta_cubic) / C) (Eq. 2) + fn cubic_k(&self, max_datagram_size: u64) -> f64 { + let w_max = self.w_max / max_datagram_size as f64; + (w_max * (1.0 - BETA_CUBIC) / C).cbrt() + } + + // W_cubic(t) = C * (t - K)^3 - w_max (Eq. 1) + fn w_cubic(&self, t: Duration, max_datagram_size: u64) -> f64 { + let w_max = self.w_max / max_datagram_size as f64; + + (C * (t.as_secs_f64() - self.k).powi(3) + w_max) * max_datagram_size as f64 + } + + // W_est(t) = w_max * beta_cubic + 3 * (1 - beta_cubic) / (1 + beta_cubic) * + // (t / RTT) (Eq. 4) + fn w_est(&self, t: Duration, rtt: Duration, max_datagram_size: u64) -> f64 { + let w_max = self.w_max / max_datagram_size as f64; + (w_max * BETA_CUBIC + + 3.0 * (1.0 - BETA_CUBIC) / (1.0 + BETA_CUBIC) * t.as_secs_f64() / rtt.as_secs_f64()) + * max_datagram_size as f64 + } +} + +/// The RFC8312 congestion controller, as widely used for TCP +#[derive(Debug, Clone)] +pub struct Cubic { + config: Arc, + /// Maximum number of bytes in flight that may be sent. + window: u64, + /// Slow start threshold in bytes. When the congestion window is below ssthresh, the mode is + /// slow start and the window grows by the number of bytes acknowledged. + ssthresh: u64, + /// The time when QUIC first detects a loss, causing it to enter recovery. When a packet sent + /// after this time is acknowledged, QUIC exits recovery. + recovery_start_time: Option, + cubic_state: State, + current_mtu: u64, +} + +impl Cubic { + /// Construct a state using the given `config` and current time `now` + pub fn new(config: Arc, _now: Instant, current_mtu: u16) -> Self { + Self { + window: config.initial_window, + ssthresh: u64::MAX, + recovery_start_time: None, + config, + cubic_state: Default::default(), + current_mtu: current_mtu as u64, + } + } + + fn minimum_window(&self) -> u64 { + 2 * self.current_mtu + } +} + +impl Controller for Cubic { + fn on_ack( + &mut self, + now: Instant, + sent: Instant, + bytes: u64, + app_limited: bool, + rtt: &RttEstimator, + ) { + if app_limited + || self + .recovery_start_time + .map(|recovery_start_time| sent <= recovery_start_time) + .unwrap_or(false) + { + return; + } + + if self.window < self.ssthresh { + // Slow start + self.window += bytes; + } else { + // Congestion avoidance. + let ca_start_time; + + match self.recovery_start_time { + Some(t) => ca_start_time = t, + None => { + // When we come here without congestion_event() triggered, + // initialize congestion_recovery_start_time, w_max and k. + ca_start_time = now; + self.recovery_start_time = Some(now); + + self.cubic_state.w_max = self.window as f64; + self.cubic_state.k = 0.0; + } + } + + let t = now - ca_start_time; + + // w_cubic(t + rtt) + let w_cubic = self.cubic_state.w_cubic(t + rtt.get(), self.current_mtu); + + // w_est(t) + let w_est = self.cubic_state.w_est(t, rtt.get(), self.current_mtu); + + let mut cubic_cwnd = self.window; + + if w_cubic < w_est { + // TCP friendly region. + cubic_cwnd = cmp::max(cubic_cwnd, w_est as u64); + } else if cubic_cwnd < w_cubic as u64 { + // Concave region or convex region use same increment. + let cubic_inc = + (w_cubic - cubic_cwnd as f64) / cubic_cwnd as f64 * self.current_mtu as f64; + + cubic_cwnd += cubic_inc as u64; + } + + // Update the increment and increase cwnd by MSS. + self.cubic_state.cwnd_inc += cubic_cwnd - self.window; + + // cwnd_inc can be more than 1 MSS in the late stage of max probing. + // however RFC9002 §7.3.3 (Congestion Avoidance) limits + // the increase of cwnd to 1 max_datagram_size per cwnd acknowledged. + if self.cubic_state.cwnd_inc >= self.current_mtu { + self.window += self.current_mtu; + self.cubic_state.cwnd_inc = 0; + } + } + } + + fn on_congestion_event( + &mut self, + now: Instant, + sent: Instant, + is_persistent_congestion: bool, + _lost_bytes: u64, + ) { + if self + .recovery_start_time + .map(|recovery_start_time| sent <= recovery_start_time) + .unwrap_or(false) + { + return; + } + + self.recovery_start_time = Some(now); + + // Fast convergence + if (self.window as f64) < self.cubic_state.w_max { + self.cubic_state.w_max = self.window as f64 * (1.0 + BETA_CUBIC) / 2.0; + } else { + self.cubic_state.w_max = self.window as f64; + } + + self.ssthresh = cmp::max( + (self.cubic_state.w_max * BETA_CUBIC) as u64, + self.minimum_window(), + ); + self.window = self.ssthresh; + self.cubic_state.k = self.cubic_state.cubic_k(self.current_mtu); + + self.cubic_state.cwnd_inc = (self.cubic_state.cwnd_inc as f64 * BETA_CUBIC) as u64; + + if is_persistent_congestion { + self.recovery_start_time = None; + self.cubic_state.w_max = self.window as f64; + + // 4.7 Timeout - reduce ssthresh based on BETA_CUBIC + self.ssthresh = cmp::max( + (self.window as f64 * BETA_CUBIC) as u64, + self.minimum_window(), + ); + + self.cubic_state.cwnd_inc = 0; + + self.window = self.minimum_window(); + } + } + + fn on_mtu_update(&mut self, new_mtu: u16) { + self.current_mtu = new_mtu as u64; + self.window = self.window.max(self.minimum_window()); + } + + fn window(&self) -> u64 { + self.window + } + + fn metrics(&self) -> super::ControllerMetrics { + super::ControllerMetrics { + congestion_window: self.window(), + ssthresh: Some(self.ssthresh), + pacing_rate: None, + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn initial_window(&self) -> u64 { + self.config.initial_window + } + + fn into_any(self: Box) -> Box { + self + } +} + +/// Configuration for the `Cubic` congestion controller +#[derive(Debug, Clone)] +pub struct CubicConfig { + initial_window: u64, +} + +impl CubicConfig { + /// Default limit on the amount of outstanding data in bytes. + /// + /// Recommended value: `min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))` + pub fn initial_window(&mut self, value: u64) -> &mut Self { + self.initial_window = value; + self + } +} + +impl Default for CubicConfig { + fn default() -> Self { + Self { + initial_window: 14720.clamp(2 * BASE_DATAGRAM_SIZE, 10 * BASE_DATAGRAM_SIZE), + } + } +} + +impl ControllerFactory for CubicConfig { + fn build(self: Arc, now: Instant, current_mtu: u16) -> Box { + Box::new(Cubic::new(self, now, current_mtu)) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/new_reno.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/new_reno.rs new file mode 100644 index 0000000..7bc61c6 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/congestion/new_reno.rs @@ -0,0 +1,172 @@ +use std::any::Any; +use std::sync::Arc; + +use super::{BASE_DATAGRAM_SIZE, Controller, ControllerFactory}; +use crate::Instant; +use crate::connection::RttEstimator; + +/// A simple, standard congestion controller +#[derive(Debug, Clone)] +pub struct NewReno { + config: Arc, + current_mtu: u64, + /// Maximum number of bytes in flight that may be sent. + window: u64, + /// Slow start threshold in bytes. When the congestion window is below ssthresh, the mode is + /// slow start and the window grows by the number of bytes acknowledged. + ssthresh: u64, + /// The time when QUIC first detects a loss, causing it to enter recovery. When a packet sent + /// after this time is acknowledged, QUIC exits recovery. + recovery_start_time: Instant, + /// Bytes which had been acked by the peer since leaving slow start + bytes_acked: u64, +} + +impl NewReno { + /// Construct a state using the given `config` and current time `now` + pub fn new(config: Arc, now: Instant, current_mtu: u16) -> Self { + Self { + window: config.initial_window, + ssthresh: u64::MAX, + recovery_start_time: now, + current_mtu: current_mtu as u64, + config, + bytes_acked: 0, + } + } + + fn minimum_window(&self) -> u64 { + 2 * self.current_mtu + } +} + +impl Controller for NewReno { + fn on_ack( + &mut self, + _now: Instant, + sent: Instant, + bytes: u64, + app_limited: bool, + _rtt: &RttEstimator, + ) { + if app_limited || sent <= self.recovery_start_time { + return; + } + + if self.window < self.ssthresh { + // Slow start + self.window += bytes; + + if self.window >= self.ssthresh { + // Exiting slow start + // Initialize `bytes_acked` for congestion avoidance. The idea + // here is that any bytes over `sshthresh` will already be counted + // towards the congestion avoidance phase - independent of when + // how close to `sshthresh` the `window` was when switching states, + // and independent of datagram sizes. + self.bytes_acked = self.window - self.ssthresh; + } + } else { + // Congestion avoidance + // This implementation uses the method which does not require + // floating point math, which also increases the window by 1 datagram + // for every round trip. + // This mechanism is called Appropriate Byte Counting in + // https://tools.ietf.org/html/rfc3465 + self.bytes_acked += bytes; + + if self.bytes_acked >= self.window { + self.bytes_acked -= self.window; + self.window += self.current_mtu; + } + } + } + + fn on_congestion_event( + &mut self, + now: Instant, + sent: Instant, + is_persistent_congestion: bool, + _lost_bytes: u64, + ) { + if sent <= self.recovery_start_time { + return; + } + + self.recovery_start_time = now; + self.window = (self.window as f32 * self.config.loss_reduction_factor) as u64; + self.window = self.window.max(self.minimum_window()); + self.ssthresh = self.window; + + if is_persistent_congestion { + self.window = self.minimum_window(); + } + } + + fn on_mtu_update(&mut self, new_mtu: u16) { + self.current_mtu = new_mtu as u64; + self.window = self.window.max(self.minimum_window()); + } + + fn window(&self) -> u64 { + self.window + } + + fn metrics(&self) -> super::ControllerMetrics { + super::ControllerMetrics { + congestion_window: self.window(), + ssthresh: Some(self.ssthresh), + pacing_rate: None, + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn initial_window(&self) -> u64 { + self.config.initial_window + } + + fn into_any(self: Box) -> Box { + self + } +} + +/// Configuration for the `NewReno` congestion controller +#[derive(Debug, Clone)] +pub struct NewRenoConfig { + initial_window: u64, + loss_reduction_factor: f32, +} + +impl NewRenoConfig { + /// Default limit on the amount of outstanding data in bytes. + /// + /// Recommended value: `min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))` + pub fn initial_window(&mut self, value: u64) -> &mut Self { + self.initial_window = value; + self + } + + /// Reduction in congestion window when a new loss event is detected. + pub fn loss_reduction_factor(&mut self, value: f32) -> &mut Self { + self.loss_reduction_factor = value; + self + } +} + +impl Default for NewRenoConfig { + fn default() -> Self { + Self { + initial_window: 14720.clamp(2 * BASE_DATAGRAM_SIZE, 10 * BASE_DATAGRAM_SIZE), + loss_reduction_factor: 0.5, + } + } +} + +impl ControllerFactory for NewRenoConfig { + fn build(self: Arc, now: Instant, current_mtu: u16) -> Box { + Box::new(NewReno::new(self, now, current_mtu)) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/ack_frequency.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/ack_frequency.rs new file mode 100644 index 0000000..8de43d7 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/ack_frequency.rs @@ -0,0 +1,155 @@ +use crate::Duration; +use crate::connection::spaces::PendingAcks; +use crate::frame::AckFrequency; +use crate::transport_parameters::TransportParameters; +use crate::{AckFrequencyConfig, TIMER_GRANULARITY, TransportError, VarInt}; + +/// State associated to ACK frequency +pub(super) struct AckFrequencyState { + // + // Sending ACK_FREQUENCY frames + // + in_flight_ack_frequency_frame: Option<(u64, Duration)>, + next_outgoing_sequence_number: VarInt, + pub(super) peer_max_ack_delay: Duration, + + // + // Receiving ACK_FREQUENCY frames + // + last_ack_frequency_frame: Option, + pub(super) max_ack_delay: Duration, +} + +impl AckFrequencyState { + pub(super) fn new(default_max_ack_delay: Duration) -> Self { + Self { + in_flight_ack_frequency_frame: None, + next_outgoing_sequence_number: VarInt(0), + peer_max_ack_delay: default_max_ack_delay, + + last_ack_frequency_frame: None, + max_ack_delay: default_max_ack_delay, + } + } + + /// Returns the `max_ack_delay` that should be requested of the peer when sending an + /// ACK_FREQUENCY frame + pub(super) fn candidate_max_ack_delay( + &self, + rtt: Duration, + config: &AckFrequencyConfig, + peer_params: &TransportParameters, + ) -> Duration { + // Use the peer's max_ack_delay if no custom max_ack_delay was provided in the config + let min_ack_delay = + Duration::from_micros(peer_params.min_ack_delay.map_or(0, |x| x.into())); + config + .max_ack_delay + .unwrap_or(self.peer_max_ack_delay) + .clamp(min_ack_delay, rtt.max(MIN_AUTOMATIC_ACK_DELAY)) + } + + /// Returns the `max_ack_delay` for the purposes of calculating the PTO + /// + /// This `max_ack_delay` is defined as the maximum of the peer's current `max_ack_delay` and all + /// in-flight `max_ack_delay`s (i.e. proposed values that haven't been acknowledged yet, but + /// might be already in use by the peer). + pub(super) fn max_ack_delay_for_pto(&self) -> Duration { + // Note: we have at most one in-flight ACK_FREQUENCY frame + if let Some((_, max_ack_delay)) = self.in_flight_ack_frequency_frame { + self.peer_max_ack_delay.max(max_ack_delay) + } else { + self.peer_max_ack_delay + } + } + + /// Returns the next sequence number for an ACK_FREQUENCY frame + pub(super) fn next_sequence_number(&mut self) -> VarInt { + assert!(self.next_outgoing_sequence_number <= VarInt::MAX); + + let seq = self.next_outgoing_sequence_number; + self.next_outgoing_sequence_number.0 += 1; + seq + } + + /// Returns true if we should send an ACK_FREQUENCY frame + pub(super) fn should_send_ack_frequency( + &self, + rtt: Duration, + config: &AckFrequencyConfig, + peer_params: &TransportParameters, + ) -> bool { + if self.next_outgoing_sequence_number.0 == 0 { + // Always send at startup + return true; + } + let current = self + .in_flight_ack_frequency_frame + .map_or(self.peer_max_ack_delay, |(_, pending)| pending); + let desired = self.candidate_max_ack_delay(rtt, config, peer_params); + let error = (desired.as_secs_f32() / current.as_secs_f32()) - 1.0; + error.abs() > MAX_RTT_ERROR + } + + /// Notifies the [`AckFrequencyState`] that a packet containing an ACK_FREQUENCY frame was sent + pub(super) fn ack_frequency_sent(&mut self, pn: u64, requested_max_ack_delay: Duration) { + self.in_flight_ack_frequency_frame = Some((pn, requested_max_ack_delay)); + } + + /// Notifies the [`AckFrequencyState`] that a packet has been ACKed + pub(super) fn on_acked(&mut self, pn: u64) { + match self.in_flight_ack_frequency_frame { + Some((number, requested_max_ack_delay)) if number == pn => { + self.in_flight_ack_frequency_frame = None; + self.peer_max_ack_delay = requested_max_ack_delay; + } + _ => {} + } + } + + /// Notifies the [`AckFrequencyState`] that an ACK_FREQUENCY frame was received + /// + /// Updates the endpoint's params according to the payload of the ACK_FREQUENCY frame, or + /// returns an error in case the requested `max_ack_delay` is invalid. + /// + /// Returns `true` if the frame was processed and `false` if it was ignored because of being + /// stale. + pub(super) fn ack_frequency_received( + &mut self, + frame: &AckFrequency, + pending_acks: &mut PendingAcks, + ) -> Result { + if self + .last_ack_frequency_frame + .is_some_and(|highest_sequence_nr| frame.sequence.into_inner() <= highest_sequence_nr) + { + return Ok(false); + } + + self.last_ack_frequency_frame = Some(frame.sequence.into_inner()); + + // Update max_ack_delay + let max_ack_delay = Duration::from_micros(frame.request_max_ack_delay.into_inner()); + if max_ack_delay < TIMER_GRANULARITY { + return Err(TransportError::PROTOCOL_VIOLATION( + "Requested Max Ack Delay in ACK_FREQUENCY frame is less than min_ack_delay", + )); + } + self.max_ack_delay = max_ack_delay; + + // Update the rest of the params + pending_acks.set_ack_frequency_params(frame); + + Ok(true) + } +} + +/// Maximum proportion difference between the most recently requested max ACK delay and the +/// currently desired one before a new request is sent, when the peer supports the ACK frequency +/// extension and an explicit max ACK delay is not configured. +const MAX_RTT_ERROR: f32 = 0.2; + +/// Minimum value to request the peer set max ACK delay to when the peer supports the ACK frequency +/// extension and an explicit max ACK delay is not configured. +// Keep in sync with `AckFrequencyConfig::max_ack_delay` documentation +const MIN_AUTOMATIC_ACK_DELAY: Duration = Duration::from_millis(25); diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/assembler.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/assembler.rs new file mode 100644 index 0000000..735e659 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/assembler.rs @@ -0,0 +1,658 @@ +use std::{ + cmp::Ordering, + collections::{BinaryHeap, binary_heap::PeekMut}, + mem, +}; + +use bytes::{Buf, Bytes, BytesMut}; + +use crate::range_set::RangeSet; + +/// Helper to assemble unordered stream frames into an ordered stream +#[derive(Debug, Default)] +pub(super) struct Assembler { + state: State, + data: BinaryHeap, + /// Total number of buffered bytes, including duplicates in ordered mode. + buffered: usize, + /// Estimated number of allocated bytes, will never be less than `buffered`. + allocated: usize, + /// Number of bytes read by the application. When only ordered reads have been used, this is the + /// length of the contiguous prefix of the stream which has been consumed by the application, + /// aka the stream offset. + bytes_read: u64, + end: u64, +} + +impl Assembler { + pub(super) fn new() -> Self { + Self::default() + } + + /// Reset to the initial state + pub(super) fn reinit(&mut self) { + let old_data = mem::take(&mut self.data); + *self = Self::default(); + self.data = old_data; + self.data.clear(); + } + + pub(super) fn ensure_ordering(&mut self, ordered: bool) -> Result<(), IllegalOrderedRead> { + if ordered && !self.state.is_ordered() { + return Err(IllegalOrderedRead); + } else if !ordered && self.state.is_ordered() { + // Enter unordered mode + if !self.data.is_empty() { + // Get rid of possible duplicates + self.defragment(); + } + let mut recvd = RangeSet::new(); + recvd.insert(0..self.bytes_read); + for chunk in &self.data { + recvd.insert(chunk.offset..chunk.offset + chunk.bytes.len() as u64); + } + self.state = State::Unordered { recvd }; + } + Ok(()) + } + + /// Get the the next chunk + pub(super) fn read(&mut self, max_length: usize, ordered: bool) -> Option { + loop { + let mut chunk = self.data.peek_mut()?; + + if ordered { + if chunk.offset > self.bytes_read { + // Next chunk is after current read index + return None; + } else if (chunk.offset + chunk.bytes.len() as u64) <= self.bytes_read { + // Next chunk is useless as the read index is beyond its end + self.buffered -= chunk.bytes.len(); + self.allocated -= chunk.allocation_size; + PeekMut::pop(chunk); + continue; + } + + // Determine `start` and `len` of the slice of useful data in chunk + let start = (self.bytes_read - chunk.offset) as usize; + if start > 0 { + chunk.bytes.advance(start); + chunk.offset += start as u64; + self.buffered -= start; + } + } + + return Some(if max_length < chunk.bytes.len() { + self.bytes_read += max_length as u64; + let offset = chunk.offset; + chunk.offset += max_length as u64; + self.buffered -= max_length; + Chunk::new(offset, chunk.bytes.split_to(max_length)) + } else { + self.bytes_read += chunk.bytes.len() as u64; + self.buffered -= chunk.bytes.len(); + self.allocated -= chunk.allocation_size; + let chunk = PeekMut::pop(chunk); + Chunk::new(chunk.offset, chunk.bytes) + }); + } + } + + /// Copy fragmented chunk data to new chunks backed by a single buffer + /// + /// This makes sure we're not unnecessarily holding on to many larger allocations. + /// We merge contiguous chunks in the process of doing so. + fn defragment(&mut self) { + let new = BinaryHeap::with_capacity(self.data.len()); + let old = mem::replace(&mut self.data, new); + let mut buffers = old.into_sorted_vec(); + self.buffered = 0; + let mut fragmented_buffered = 0; + let mut offset = 0; + for chunk in buffers.iter_mut().rev() { + chunk.try_mark_defragment(offset); + let size = chunk.bytes.len(); + offset = chunk.offset + size as u64; + self.buffered += size; + if !chunk.defragmented { + fragmented_buffered += size; + } + } + self.allocated = self.buffered; + let mut buffer = BytesMut::with_capacity(fragmented_buffered); + let mut offset = 0; + for chunk in buffers.into_iter().rev() { + if chunk.defragmented { + // bytes might be empty after try_mark_defragment + if !chunk.bytes.is_empty() { + self.data.push(chunk); + } + continue; + } + // Overlap is resolved by try_mark_defragment + if chunk.offset != offset + (buffer.len() as u64) { + if !buffer.is_empty() { + self.data + .push(Buffer::new_defragmented(offset, buffer.split().freeze())); + } + offset = chunk.offset; + } + buffer.extend_from_slice(&chunk.bytes); + } + if !buffer.is_empty() { + self.data + .push(Buffer::new_defragmented(offset, buffer.split().freeze())); + } + } + + // Note: If a packet contains many frames from the same stream, the estimated over-allocation + // will be much higher because we are counting the same allocation multiple times. + pub(super) fn insert(&mut self, mut offset: u64, mut bytes: Bytes, allocation_size: usize) { + debug_assert!( + bytes.len() <= allocation_size, + "allocation_size less than bytes.len(): {:?} < {:?}", + allocation_size, + bytes.len() + ); + self.end = self.end.max(offset + bytes.len() as u64); + if let State::Unordered { ref mut recvd } = self.state { + // Discard duplicate data + for duplicate in recvd.replace(offset..offset + bytes.len() as u64) { + if duplicate.start > offset { + let buffer = Buffer::new( + offset, + bytes.split_to((duplicate.start - offset) as usize), + allocation_size, + ); + self.buffered += buffer.bytes.len(); + self.allocated += buffer.allocation_size; + self.data.push(buffer); + offset = duplicate.start; + } + bytes.advance((duplicate.end - offset) as usize); + offset = duplicate.end; + } + } else if offset < self.bytes_read { + if (offset + bytes.len() as u64) <= self.bytes_read { + return; + } else { + let diff = self.bytes_read - offset; + offset += diff; + bytes.advance(diff as usize); + } + } + + if bytes.is_empty() { + return; + } + let buffer = Buffer::new(offset, bytes, allocation_size); + self.buffered += buffer.bytes.len(); + self.allocated += buffer.allocation_size; + self.data.push(buffer); + // `self.buffered` also counts duplicate bytes, therefore we use + // `self.end - self.bytes_read` as an upper bound of buffered unique + // bytes. This will cause a defragmentation if the amount of duplicate + // bytes exceedes a proportion of the receive window size. + let buffered = self.buffered.min((self.end - self.bytes_read) as usize); + let over_allocation = self.allocated - buffered; + // Rationale: on the one hand, we want to defragment rarely, ideally never + // in non-pathological scenarios. However, a pathological or malicious + // peer could send us one-byte frames, and since we use reference-counted + // buffers in order to prevent copying, this could result in keeping a lot + // of memory allocated. This limits over-allocation in proportion to the + // buffered data. The constants are chosen somewhat arbitrarily and try to + // balance between defragmentation overhead and over-allocation. + let threshold = 32768.max(buffered * 3 / 2); + if over_allocation > threshold { + self.defragment() + } + } + + /// Number of bytes consumed by the application + pub(super) fn bytes_read(&self) -> u64 { + self.bytes_read + } + + /// Discard all buffered data + pub(super) fn clear(&mut self) { + self.data.clear(); + self.buffered = 0; + self.allocated = 0; + } +} + +/// A chunk of data from the receive stream +#[derive(Debug, PartialEq, Eq)] +pub struct Chunk { + /// The offset in the stream + pub offset: u64, + /// The contents of the chunk + pub bytes: Bytes, +} + +impl Chunk { + fn new(offset: u64, bytes: Bytes) -> Self { + Self { offset, bytes } + } +} + +#[derive(Debug, Eq)] +struct Buffer { + offset: u64, + bytes: Bytes, + /// Size of the allocation behind `bytes`, if `defragmented == false`. + /// Otherwise this will be set to `bytes.len()` by `try_mark_defragment`. + /// Will never be less than `bytes.len()`. + allocation_size: usize, + defragmented: bool, +} + +impl Buffer { + /// Constructs a new fragmented Buffer + fn new(offset: u64, bytes: Bytes, allocation_size: usize) -> Self { + Self { + offset, + bytes, + allocation_size, + defragmented: false, + } + } + + /// Constructs a new defragmented Buffer + fn new_defragmented(offset: u64, bytes: Bytes) -> Self { + let allocation_size = bytes.len(); + Self { + offset, + bytes, + allocation_size, + defragmented: true, + } + } + + /// Discards data before `offset` and flags `self` as defragmented if it has good utilization + fn try_mark_defragment(&mut self, offset: u64) { + let duplicate = offset.saturating_sub(self.offset) as usize; + self.offset = self.offset.max(offset); + if duplicate >= self.bytes.len() { + // All bytes are duplicate + self.bytes = Bytes::new(); + self.defragmented = true; + self.allocation_size = 0; + return; + } + self.bytes.advance(duplicate); + // Make sure that fragmented buffers with high utilization become defragmented and + // defragmented buffers remain defragmented + self.defragmented = self.defragmented || self.bytes.len() * 6 / 5 >= self.allocation_size; + if self.defragmented { + // Make sure that defragmented buffers do not contribute to over-allocation + self.allocation_size = self.bytes.len(); + } + } +} + +impl Ord for Buffer { + // Invert ordering based on offset (max-heap, min offset first), + // prioritize longer chunks at the same offset. + fn cmp(&self, other: &Self) -> Ordering { + self.offset + .cmp(&other.offset) + .reverse() + .then(self.bytes.len().cmp(&other.bytes.len())) + } +} + +impl PartialOrd for Buffer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Buffer { + fn eq(&self, other: &Self) -> bool { + (self.offset, self.bytes.len()) == (other.offset, other.bytes.len()) + } +} + +#[derive(Debug, Default)] +enum State { + #[default] + Ordered, + Unordered { + /// The set of offsets that have been received from the peer, including portions not yet + /// read by the application. + recvd: RangeSet, + }, +} + +impl State { + fn is_ordered(&self) -> bool { + matches!(self, Self::Ordered) + } +} + +/// Error indicating that an ordered read was performed on a stream after an unordered read +#[derive(Debug)] +pub struct IllegalOrderedRead; + +#[cfg(test)] +mod test { + use super::*; + use assert_matches::assert_matches; + + #[test] + fn assemble_ordered() { + let mut x = Assembler::new(); + assert_matches!(next(&mut x, 32), None); + x.insert(0, Bytes::from_static(b"123"), 3); + assert_matches!(next(&mut x, 1), Some(ref y) if &y[..] == b"1"); + assert_matches!(next(&mut x, 3), Some(ref y) if &y[..] == b"23"); + x.insert(3, Bytes::from_static(b"456"), 3); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"456"); + x.insert(6, Bytes::from_static(b"789"), 3); + x.insert(9, Bytes::from_static(b"10"), 2); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"789"); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"10"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_unordered() { + let mut x = Assembler::new(); + x.ensure_ordering(false).unwrap(); + x.insert(3, Bytes::from_static(b"456"), 3); + assert_matches!(next(&mut x, 32), None); + x.insert(0, Bytes::from_static(b"123"), 3); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"123"); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"456"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_duplicate() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"123"), 3); + x.insert(0, Bytes::from_static(b"123"), 3); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"123"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_duplicate_compact() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"123"), 3); + x.insert(0, Bytes::from_static(b"123"), 3); + x.defragment(); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"123"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_contained() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"12345"), 5); + x.insert(1, Bytes::from_static(b"234"), 3); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"12345"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_contained_compact() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"12345"), 5); + x.insert(1, Bytes::from_static(b"234"), 3); + x.defragment(); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"12345"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_contains() { + let mut x = Assembler::new(); + x.insert(1, Bytes::from_static(b"234"), 3); + x.insert(0, Bytes::from_static(b"12345"), 5); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"12345"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_contains_compact() { + let mut x = Assembler::new(); + x.insert(1, Bytes::from_static(b"234"), 3); + x.insert(0, Bytes::from_static(b"12345"), 5); + x.defragment(); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"12345"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_overlapping() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"123"), 3); + x.insert(1, Bytes::from_static(b"234"), 3); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"123"); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"4"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_overlapping_compact() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"123"), 4); + x.insert(1, Bytes::from_static(b"234"), 4); + x.defragment(); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"1234"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_complex() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"1"), 1); + x.insert(2, Bytes::from_static(b"3"), 1); + x.insert(4, Bytes::from_static(b"5"), 1); + x.insert(0, Bytes::from_static(b"123456"), 6); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"123456"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_complex_compact() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"1"), 1); + x.insert(2, Bytes::from_static(b"3"), 1); + x.insert(4, Bytes::from_static(b"5"), 1); + x.insert(0, Bytes::from_static(b"123456"), 6); + x.defragment(); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"123456"); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn assemble_old() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"1234"), 4); + assert_matches!(next(&mut x, 32), Some(ref y) if &y[..] == b"1234"); + x.insert(0, Bytes::from_static(b"1234"), 4); + assert_matches!(next(&mut x, 32), None); + } + + #[test] + fn compact() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"abc"), 4); + x.insert(3, Bytes::from_static(b"def"), 4); + x.insert(9, Bytes::from_static(b"jkl"), 4); + x.insert(12, Bytes::from_static(b"mno"), 4); + x.defragment(); + assert_eq!( + next_unordered(&mut x), + Chunk::new(0, Bytes::from_static(b"abcdef")) + ); + assert_eq!( + next_unordered(&mut x), + Chunk::new(9, Bytes::from_static(b"jklmno")) + ); + } + + #[test] + fn defrag_with_missing_prefix() { + let mut x = Assembler::new(); + x.insert(3, Bytes::from_static(b"def"), 3); + x.defragment(); + assert_eq!( + next_unordered(&mut x), + Chunk::new(3, Bytes::from_static(b"def")) + ); + } + + #[test] + fn defrag_read_chunk() { + let mut x = Assembler::new(); + x.insert(3, Bytes::from_static(b"def"), 4); + x.insert(0, Bytes::from_static(b"abc"), 4); + x.insert(7, Bytes::from_static(b"hij"), 4); + x.insert(11, Bytes::from_static(b"lmn"), 4); + x.defragment(); + assert_matches!(x.read(usize::MAX, true), Some(ref y) if &y.bytes[..] == b"abcdef"); + x.insert(5, Bytes::from_static(b"fghijklmn"), 9); + assert_matches!(x.read(usize::MAX, true), Some(ref y) if &y.bytes[..] == b"ghijklmn"); + x.insert(13, Bytes::from_static(b"nopq"), 4); + assert_matches!(x.read(usize::MAX, true), Some(ref y) if &y.bytes[..] == b"opq"); + x.insert(15, Bytes::from_static(b"pqrs"), 4); + assert_matches!(x.read(usize::MAX, true), Some(ref y) if &y.bytes[..] == b"rs"); + assert_matches!(x.read(usize::MAX, true), None); + } + + #[test] + fn unordered_happy_path() { + let mut x = Assembler::new(); + x.ensure_ordering(false).unwrap(); + x.insert(0, Bytes::from_static(b"abc"), 3); + assert_eq!( + next_unordered(&mut x), + Chunk::new(0, Bytes::from_static(b"abc")) + ); + assert_eq!(x.read(usize::MAX, false), None); + x.insert(3, Bytes::from_static(b"def"), 3); + assert_eq!( + next_unordered(&mut x), + Chunk::new(3, Bytes::from_static(b"def")) + ); + assert_eq!(x.read(usize::MAX, false), None); + } + + #[test] + fn unordered_dedup() { + let mut x = Assembler::new(); + x.ensure_ordering(false).unwrap(); + x.insert(3, Bytes::from_static(b"def"), 3); + assert_eq!( + next_unordered(&mut x), + Chunk::new(3, Bytes::from_static(b"def")) + ); + assert_eq!(x.read(usize::MAX, false), None); + x.insert(0, Bytes::from_static(b"a"), 1); + x.insert(0, Bytes::from_static(b"abcdefghi"), 9); + x.insert(0, Bytes::from_static(b"abcd"), 4); + assert_eq!( + next_unordered(&mut x), + Chunk::new(0, Bytes::from_static(b"a")) + ); + assert_eq!( + next_unordered(&mut x), + Chunk::new(1, Bytes::from_static(b"bc")) + ); + assert_eq!( + next_unordered(&mut x), + Chunk::new(6, Bytes::from_static(b"ghi")) + ); + assert_eq!(x.read(usize::MAX, false), None); + x.insert(8, Bytes::from_static(b"ijkl"), 4); + assert_eq!( + next_unordered(&mut x), + Chunk::new(9, Bytes::from_static(b"jkl")) + ); + assert_eq!(x.read(usize::MAX, false), None); + x.insert(12, Bytes::from_static(b"mno"), 3); + assert_eq!( + next_unordered(&mut x), + Chunk::new(12, Bytes::from_static(b"mno")) + ); + assert_eq!(x.read(usize::MAX, false), None); + x.insert(2, Bytes::from_static(b"cde"), 3); + assert_eq!(x.read(usize::MAX, false), None); + } + + #[test] + fn chunks_dedup() { + let mut x = Assembler::new(); + x.insert(3, Bytes::from_static(b"def"), 3); + assert_eq!(x.read(usize::MAX, true), None); + x.insert(0, Bytes::from_static(b"a"), 1); + x.insert(1, Bytes::from_static(b"bcdefghi"), 9); + x.insert(0, Bytes::from_static(b"abcd"), 4); + assert_eq!( + x.read(usize::MAX, true), + Some(Chunk::new(0, Bytes::from_static(b"abcd"))) + ); + assert_eq!( + x.read(usize::MAX, true), + Some(Chunk::new(4, Bytes::from_static(b"efghi"))) + ); + assert_eq!(x.read(usize::MAX, true), None); + x.insert(8, Bytes::from_static(b"ijkl"), 4); + assert_eq!( + x.read(usize::MAX, true), + Some(Chunk::new(9, Bytes::from_static(b"jkl"))) + ); + assert_eq!(x.read(usize::MAX, true), None); + x.insert(12, Bytes::from_static(b"mno"), 3); + assert_eq!( + x.read(usize::MAX, true), + Some(Chunk::new(12, Bytes::from_static(b"mno"))) + ); + assert_eq!(x.read(usize::MAX, true), None); + x.insert(2, Bytes::from_static(b"cde"), 3); + assert_eq!(x.read(usize::MAX, true), None); + } + + #[test] + fn ordered_eager_discard() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"abc"), 3); + assert_eq!(x.data.len(), 1); + assert_eq!( + x.read(usize::MAX, true), + Some(Chunk::new(0, Bytes::from_static(b"abc"))) + ); + x.insert(0, Bytes::from_static(b"ab"), 2); + assert_eq!(x.data.len(), 0); + x.insert(2, Bytes::from_static(b"cd"), 2); + assert_eq!( + x.data.peek(), + Some(&Buffer::new(3, Bytes::from_static(b"d"), 2)) + ); + } + + #[test] + fn ordered_insert_unordered_read() { + let mut x = Assembler::new(); + x.insert(0, Bytes::from_static(b"abc"), 3); + x.insert(0, Bytes::from_static(b"abc"), 3); + x.ensure_ordering(false).unwrap(); + assert_eq!( + x.read(3, false), + Some(Chunk::new(0, Bytes::from_static(b"abc"))) + ); + assert_eq!(x.read(3, false), None); + } + + fn next_unordered(x: &mut Assembler) -> Chunk { + x.read(usize::MAX, false).unwrap() + } + + fn next(x: &mut Assembler, size: usize) -> Option { + x.read(size, true).map(|chunk| chunk.bytes) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/cid_state.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/cid_state.rs new file mode 100644 index 0000000..08ad20c --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/cid_state.rs @@ -0,0 +1,223 @@ +//! Maintain the state of local connection IDs +use std::collections::VecDeque; + +use rustc_hash::FxHashSet; +use tracing::{debug, trace}; + +use crate::{Duration, Instant, TransportError, shared::IssuedCid}; + +/// Local connection ID management +pub(super) struct CidState { + /// Timestamp when issued cids should be retired + retire_timestamp: VecDeque, + /// Number of local connection IDs that have been issued in NEW_CONNECTION_ID frames. + issued: u64, + /// Sequence numbers of local connection IDs not yet retired by the peer + active_seq: FxHashSet, + /// Sequence number the peer has already retired all CIDs below at our request via `retire_prior_to` + prev_retire_seq: u64, + /// Sequence number to set in retire_prior_to field in NEW_CONNECTION_ID frame + retire_seq: u64, + /// cid length used to decode short packet + cid_len: usize, + //// cid lifetime + cid_lifetime: Option, +} + +impl CidState { + pub(crate) fn new( + cid_len: usize, + cid_lifetime: Option, + now: Instant, + issued: u64, + ) -> Self { + let mut active_seq = FxHashSet::default(); + // Add sequence number of CIDs used in handshaking into tracking set + for seq in 0..issued { + active_seq.insert(seq); + } + let mut this = Self { + retire_timestamp: VecDeque::new(), + issued, + active_seq, + prev_retire_seq: 0, + retire_seq: 0, + cid_len, + cid_lifetime, + }; + // Track lifetime of CIDs used in handshaking + for seq in 0..issued { + this.track_lifetime(seq, now); + } + this + } + + /// Find the next timestamp when previously issued CID should be retired + pub(crate) fn next_timeout(&mut self) -> Option { + self.retire_timestamp.front().map(|nc| { + trace!("CID {} will expire at {:?}", nc.sequence, nc.timestamp); + nc.timestamp + }) + } + + /// Track the lifetime of issued cids in `retire_timestamp` + fn track_lifetime(&mut self, new_cid_seq: u64, now: Instant) { + let lifetime = match self.cid_lifetime { + Some(lifetime) => lifetime, + None => return, + }; + + let expire_timestamp = now.checked_add(lifetime); + let expire_at = match expire_timestamp { + Some(expire_at) => expire_at, + None => return, + }; + + let last_record = self.retire_timestamp.back_mut(); + if let Some(last) = last_record { + // Compare the timestamp with the last inserted record + // Combine into a single batch if timestamp of current cid is same as the last record + if expire_at == last.timestamp { + debug_assert!(new_cid_seq > last.sequence); + last.sequence = new_cid_seq; + return; + } + } + + self.retire_timestamp.push_back(CidTimestamp { + sequence: new_cid_seq, + timestamp: expire_at, + }); + } + + /// Update local CID state when previously issued CID is retired + /// + /// Return whether a new CID needs to be pushed that notifies remote peer to respond `RETIRE_CONNECTION_ID` + pub(crate) fn on_cid_timeout(&mut self) -> bool { + // Whether the peer hasn't retired all the CIDs we asked it to yet + let unretired_ids_found = + (self.prev_retire_seq..self.retire_seq).any(|seq| self.active_seq.contains(&seq)); + + let current_retire_prior_to = self.retire_seq; + let next_retire_sequence = self + .retire_timestamp + .pop_front() + .map(|seq| seq.sequence + 1); + + // According to RFC: + // Endpoints SHOULD NOT issue updates of the Retire Prior To field + // before receiving RETIRE_CONNECTION_ID frames that retire all + // connection IDs indicated by the previous Retire Prior To value. + // https://tools.ietf.org/html/draft-ietf-quic-transport-29#section-5.1.2 + if !unretired_ids_found { + // All Cids are retired, `prev_retire_cid_seq` can be assigned to `retire_cid_seq` + self.prev_retire_seq = self.retire_seq; + // Advance `retire_seq` if next cid that needs to be retired exists + if let Some(next_retire_prior_to) = next_retire_sequence { + self.retire_seq = next_retire_prior_to; + } + } + + // Check if retirement of all CIDs that reach their lifetime is still needed + // According to RFC: + // An endpoint MUST NOT + // provide more connection IDs than the peer's limit. An endpoint MAY + // send connection IDs that temporarily exceed a peer's limit if the + // NEW_CONNECTION_ID frame also requires the retirement of any excess, + // by including a sufficiently large value in the Retire Prior To field. + // + // If yes (return true), a new CID must be pushed with updated `retire_prior_to` field to remote peer. + // If no (return false), it means CIDs that reach the end of lifetime have been retired already. Do not push a new CID in order to avoid violating above RFC. + (current_retire_prior_to..self.retire_seq).any(|seq| self.active_seq.contains(&seq)) + } + + /// Update cid state when `NewIdentifiers` event is received + pub(crate) fn new_cids(&mut self, ids: &[IssuedCid], now: Instant) { + // `ids` could be `None` once active_connection_id_limit is set to 1 by peer + let last_cid = match ids.last() { + Some(cid) => cid, + None => return, + }; + self.issued += ids.len() as u64; + // Record the timestamp of CID with the largest seq number + let sequence = last_cid.sequence; + ids.iter().for_each(|frame| { + self.active_seq.insert(frame.sequence); + }); + self.track_lifetime(sequence, now); + } + + /// Update CidState for receipt of a `RETIRE_CONNECTION_ID` frame + /// + /// Returns whether a new CID can be issued, or an error if the frame was illegal. + pub(crate) fn on_cid_retirement( + &mut self, + sequence: u64, + limit: u64, + ) -> Result { + if self.cid_len == 0 { + return Err(TransportError::PROTOCOL_VIOLATION( + "RETIRE_CONNECTION_ID when CIDs aren't in use", + )); + } + if sequence > self.issued { + debug!( + sequence, + "got RETIRE_CONNECTION_ID for unissued sequence number" + ); + return Err(TransportError::PROTOCOL_VIOLATION( + "RETIRE_CONNECTION_ID for unissued sequence number", + )); + } + self.active_seq.remove(&sequence); + // Consider a scenario where peer A has active remote cid 0,1,2. + // Peer B first send a NEW_CONNECTION_ID with cid 3 and retire_prior_to set to 1. + // Peer A processes this NEW_CONNECTION_ID frame; update remote cid to 1,2,3 + // and meanwhile send a RETIRE_CONNECTION_ID to retire cid 0 to peer B. + // If peer B doesn't check the cid limit here and send a new cid again, peer A will then face CONNECTION_ID_LIMIT_ERROR + Ok(limit > self.active_seq.len() as u64) + } + + /// Length of local Connection IDs + pub(crate) fn cid_len(&self) -> usize { + self.cid_len + } + + /// The value for `retire_prior_to` field in `NEW_CONNECTION_ID` frame + pub(crate) fn retire_prior_to(&self) -> u64 { + self.retire_seq + } + + #[cfg(test)] + pub(crate) fn active_seq(&self) -> (u64, u64) { + let mut min = u64::MAX; + let mut max = u64::MIN; + for n in self.active_seq.iter() { + if n < &min { + min = *n; + } + if n > &max { + max = *n; + } + } + (min, max) + } + + #[cfg(test)] + pub(crate) fn assign_retire_seq(&mut self, v: u64) -> u64 { + // Cannot retire more CIDs than what have been issued + debug_assert!(v <= *self.active_seq.iter().max().unwrap() + 1); + let n = v.checked_sub(self.retire_seq).unwrap(); + self.retire_seq = v; + n + } +} + +/// Data structure that records when issued cids should be retired +#[derive(Copy, Clone, Eq, PartialEq)] +struct CidTimestamp { + /// Highest cid sequence number created in a batch + sequence: u64, + /// Timestamp when cid needs to be retired + timestamp: Instant, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/datagrams.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/datagrams.rs new file mode 100644 index 0000000..c22e8d7 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/datagrams.rs @@ -0,0 +1,211 @@ +use std::collections::VecDeque; + +use bytes::Bytes; +use thiserror::Error; +use tracing::{debug, trace}; + +use super::Connection; +use crate::{ + TransportError, + frame::{Datagram, FrameStruct}, +}; + +/// API to control datagram traffic +pub struct Datagrams<'a> { + pub(super) conn: &'a mut Connection, +} + +impl Datagrams<'_> { + /// Queue an unreliable, unordered datagram for immediate transmission + /// + /// If `drop` is true, previously queued datagrams which are still unsent may be discarded to + /// make space for this datagram, in order of oldest to newest. If `drop` is false, and there + /// isn't enough space due to previously queued datagrams, this function will return + /// `SendDatagramError::Blocked`. `Event::DatagramsUnblocked` will be emitted once datagrams + /// have been sent. + /// + /// Returns `Err` iff a `len`-byte datagram cannot currently be sent. + pub fn send(&mut self, data: Bytes, drop: bool) -> Result<(), SendDatagramError> { + if self.conn.config.datagram_receive_buffer_size.is_none() { + return Err(SendDatagramError::Disabled); + } + let max = self + .max_size() + .ok_or(SendDatagramError::UnsupportedByPeer)?; + if data.len() > max { + return Err(SendDatagramError::TooLarge); + } + if drop { + while self.conn.datagrams.outgoing_total > self.conn.config.datagram_send_buffer_size { + let prev = self + .conn + .datagrams + .outgoing + .pop_front() + .expect("datagrams.outgoing_total desynchronized"); + trace!(len = prev.data.len(), "dropping outgoing datagram"); + self.conn.datagrams.outgoing_total -= prev.data.len(); + } + } else if self.conn.datagrams.outgoing_total + data.len() + > self.conn.config.datagram_send_buffer_size + { + self.conn.datagrams.send_blocked = true; + return Err(SendDatagramError::Blocked(data)); + } + self.conn.datagrams.outgoing_total += data.len(); + self.conn.datagrams.outgoing.push_back(Datagram { data }); + Ok(()) + } + + /// Compute the maximum size of datagrams that may passed to `send_datagram` + /// + /// Returns `None` if datagrams are unsupported by the peer or disabled locally. + /// + /// This may change over the lifetime of a connection according to variation in the path MTU + /// estimate. The peer can also enforce an arbitrarily small fixed limit, but if the peer's + /// limit is large this is guaranteed to be a little over a kilobyte at minimum. + /// + /// Not necessarily the maximum size of received datagrams. + pub fn max_size(&self) -> Option { + // We use the conservative overhead bound for any packet number, reducing the budget by at + // most 3 bytes, so that PN size fluctuations don't cause users sending maximum-size + // datagrams to suffer avoidable packet loss. + let max_size = self.conn.path.current_mtu() as usize + - self.conn.predict_1rtt_overhead(None) + - Datagram::SIZE_BOUND; + let limit = self + .conn + .peer_params + .max_datagram_frame_size? + .into_inner() + .saturating_sub(Datagram::SIZE_BOUND as u64); + Some(limit.min(max_size as u64) as usize) + } + + /// Receive an unreliable, unordered datagram + pub fn recv(&mut self) -> Option { + self.conn.datagrams.recv() + } + + /// Bytes available in the outgoing datagram buffer + /// + /// When greater than zero, [`send`](Self::send)ing a datagram of at most this size is + /// guaranteed not to cause older datagrams to be dropped. + pub fn send_buffer_space(&self) -> usize { + self.conn + .config + .datagram_send_buffer_size + .saturating_sub(self.conn.datagrams.outgoing_total) + } +} + +#[derive(Default)] +pub(super) struct DatagramState { + /// Number of bytes of datagrams that have been received by the local transport but not + /// delivered to the application + pub(super) recv_buffered: usize, + pub(super) incoming: VecDeque, + pub(super) outgoing: VecDeque, + pub(super) outgoing_total: usize, + pub(super) send_blocked: bool, +} + +impl DatagramState { + pub(super) fn received( + &mut self, + datagram: Datagram, + window: &Option, + ) -> Result { + let window = match window { + None => { + return Err(TransportError::PROTOCOL_VIOLATION( + "unexpected DATAGRAM frame", + )); + } + Some(x) => *x, + }; + + if datagram.data.len() > window { + return Err(TransportError::PROTOCOL_VIOLATION("oversized datagram")); + } + + let was_empty = self.recv_buffered == 0; + while datagram.data.len() + self.recv_buffered > window { + debug!("dropping stale datagram"); + self.recv(); + } + + self.recv_buffered += datagram.data.len(); + self.incoming.push_back(datagram); + Ok(was_empty) + } + + /// Discard outgoing datagrams with a payload larger than `max_payload` bytes + /// + /// Used to ensure that reductions in MTU don't get us stuck in a state where we have a datagram + /// queued but can't send it. + pub(super) fn drop_oversized(&mut self, max_payload: usize) { + self.outgoing.retain(|datagram| { + let result = datagram.data.len() < max_payload; + if !result { + trace!( + "dropping {} byte datagram violating {} byte limit", + datagram.data.len(), + max_payload + ); + self.outgoing_total -= datagram.data.len(); + } + result + }); + } + + /// Attempt to write a datagram frame into `buf`, consuming it from `self.outgoing` + /// + /// Returns whether a frame was written. At most `max_size` bytes will be written, including + /// framing. + pub(super) fn write(&mut self, buf: &mut Vec, max_size: usize) -> bool { + let datagram = match self.outgoing.pop_front() { + Some(x) => x, + None => return false, + }; + + if buf.len() + datagram.size(true) > max_size { + // Future work: we could be more clever about cramming small datagrams into + // mostly-full packets when a larger one is queued first + self.outgoing.push_front(datagram); + return false; + } + + trace!(len = datagram.data.len(), "DATAGRAM"); + + self.outgoing_total -= datagram.data.len(); + datagram.encode(true, buf); + true + } + + pub(super) fn recv(&mut self) -> Option { + let x = self.incoming.pop_front()?.data; + self.recv_buffered -= x.len(); + Some(x) + } +} + +/// Errors that can arise when sending a datagram +#[derive(Debug, Error, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum SendDatagramError { + /// The peer does not support receiving datagram frames + #[error("datagrams not supported by peer")] + UnsupportedByPeer, + /// Datagram support is disabled locally + #[error("datagram support disabled")] + Disabled, + /// The datagram is larger than the connection can currently accommodate + /// + /// Indicates that the path MTU minus overhead or the limit advertised by the peer has been + /// exceeded. + #[error("datagram too large")] + TooLarge, + /// Send would block + #[error("datagram send blocked")] + Blocked(Bytes), +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/mod.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/mod.rs new file mode 100644 index 0000000..74906f2 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/mod.rs @@ -0,0 +1,4123 @@ +use std::{ + cmp, + collections::VecDeque, + convert::TryFrom, + fmt, io, mem, + net::{IpAddr, SocketAddr}, + sync::Arc, +}; + +use bytes::{Bytes, BytesMut}; +use frame::StreamMetaVec; + +use rand::{Rng, SeedableRng, rngs::StdRng}; +use thiserror::Error; +use tracing::{debug, error, trace, trace_span, warn}; + +use crate::{ + Dir, Duration, EndpointConfig, Frame, INITIAL_MTU, Instant, MAX_CID_SIZE, MAX_STREAM_COUNT, + MIN_INITIAL_SIZE, Side, StreamId, TIMER_GRANULARITY, TokenStore, Transmit, TransportError, + TransportErrorCode, VarInt, + cid_generator::ConnectionIdGenerator, + cid_queue::CidQueue, + coding::BufMutExt, + config::{ServerConfig, TransportConfig}, + crypto::{self, KeyPair, Keys, PacketKey}, + frame::{self, Close, Datagram, FrameStruct, NewConnectionId, NewToken}, + packet::{ + FixedLengthConnectionIdParser, Header, InitialHeader, InitialPacket, LongType, Packet, + PacketNumber, PartialDecode, SpaceId, + }, + range_set::ArrayRangeSet, + shared::{ + ConnectionEvent, ConnectionEventInner, ConnectionId, DatagramConnectionEvent, EcnCodepoint, + EndpointEvent, EndpointEventInner, + }, + token::{ResetToken, Token, TokenPayload}, + transport_parameters::TransportParameters, +}; + +mod ack_frequency; +use ack_frequency::AckFrequencyState; + +mod assembler; +pub use assembler::Chunk; + +mod cid_state; +use cid_state::CidState; + +mod datagrams; +use datagrams::DatagramState; +pub use datagrams::{Datagrams, SendDatagramError}; + +mod mtud; +mod pacing; + +mod packet_builder; +use packet_builder::PacketBuilder; + +mod packet_crypto; +use packet_crypto::{PrevCrypto, ZeroRttCrypto}; + +mod paths; +pub use paths::RttEstimator; +use paths::{PathData, PathResponses}; + +pub(crate) mod qlog; + +mod send_buffer; + +mod spaces; +#[cfg(fuzzing)] +pub use spaces::Retransmits; +#[cfg(not(fuzzing))] +use spaces::Retransmits; +use spaces::{PacketNumberFilter, PacketSpace, SendableFrames, SentPacket, ThinRetransmits}; + +mod stats; +pub use stats::{ConnectionStats, FrameStats, PathStats, UdpStats}; + +mod streams; +#[cfg(fuzzing)] +pub use streams::StreamsState; +#[cfg(not(fuzzing))] +use streams::StreamsState; +pub use streams::{ + Chunks, ClosedStream, FinishError, ReadError, ReadableError, RecvStream, SendStream, + ShouldTransmit, StreamEvent, Streams, WriteError, Written, +}; + +mod timer; +use crate::congestion::Controller; +use timer::{Timer, TimerTable}; + +/// Protocol state and logic for a single QUIC connection +/// +/// Objects of this type receive [`ConnectionEvent`]s and emit [`EndpointEvent`]s and application +/// [`Event`]s to make progress. To handle timeouts, a `Connection` returns timer updates and +/// expects timeouts through various methods. A number of simple getter methods are exposed +/// to allow callers to inspect some of the connection state. +/// +/// `Connection` has roughly 4 types of methods: +/// +/// - A. Simple getters, taking `&self` +/// - B. Handlers for incoming events from the network or system, named `handle_*`. +/// - C. State machine mutators, for incoming commands from the application. For convenience we +/// refer to this as "performing I/O" below, however as per the design of this library none of the +/// functions actually perform system-level I/O. For example, [`read`](RecvStream::read) and +/// [`write`](SendStream::write), but also things like [`reset`](SendStream::reset). +/// - D. Polling functions for outgoing events or actions for the caller to +/// take, named `poll_*`. +/// +/// The simplest way to use this API correctly is to call (B) and (C) whenever +/// appropriate, then after each of those calls, as soon as feasible call all +/// polling methods (D) and deal with their outputs appropriately, e.g. by +/// passing it to the application or by making a system-level I/O call. You +/// should call the polling functions in this order: +/// +/// 1. [`poll_transmit`](Self::poll_transmit) +/// 2. [`poll_timeout`](Self::poll_timeout) +/// 3. [`poll_endpoint_events`](Self::poll_endpoint_events) +/// 4. [`poll`](Self::poll) +/// +/// Currently the only actual dependency is from (2) to (1), however additional +/// dependencies may be added in future, so the above order is recommended. +/// +/// (A) may be called whenever desired. +/// +/// Care should be made to ensure that the input events represent monotonically +/// increasing time. Specifically, calling [`handle_timeout`](Self::handle_timeout) +/// with events of the same [`Instant`] may be interleaved in any order with a +/// call to [`handle_event`](Self::handle_event) at that same instant; however +/// events or timeouts with different instants must not be interleaved. +pub struct Connection { + endpoint_config: Arc, + config: Arc, + rng: StdRng, + crypto: Box, + /// The CID we initially chose, for use during the handshake + handshake_cid: ConnectionId, + /// The CID the peer initially chose, for use during the handshake + rem_handshake_cid: ConnectionId, + /// The "real" local IP address which was was used to receive the initial packet. + /// This is only populated for the server case, and if known + local_ip: Option, + path: PathData, + /// Incremented every time we see a new path + /// + /// Stored separately from `path.generation` to account for aborted migrations + path_counter: u64, + /// Whether MTU detection is supported in this environment + allow_mtud: bool, + prev_path: Option<(ConnectionId, PathData)>, + state: State, + side: ConnectionSide, + /// Whether or not 0-RTT was enabled during the handshake. Does not imply acceptance. + zero_rtt_enabled: bool, + /// Set if 0-RTT is supported, then cleared when no longer needed. + zero_rtt_crypto: Option, + key_phase: bool, + /// How many packets are in the current key phase. Used only for `Data` space. + key_phase_size: u64, + /// Transport parameters set by the peer + peer_params: TransportParameters, + /// Source ConnectionId of the first packet received from the peer + orig_rem_cid: ConnectionId, + /// Destination ConnectionId sent by the client on the first Initial + initial_dst_cid: ConnectionId, + /// The value that the server included in the Source Connection ID field of a Retry packet, if + /// one was received + retry_src_cid: Option, + events: VecDeque, + endpoint_events: VecDeque, + /// Whether the spin bit is in use for this connection + spin_enabled: bool, + /// Outgoing spin bit state + spin: bool, + /// Packet number spaces: initial, handshake, 1-RTT + spaces: [PacketSpace; 3], + /// Highest usable packet number space + highest_space: SpaceId, + /// 1-RTT keys used prior to a key update + prev_crypto: Option, + /// 1-RTT keys to be used for the next key update + /// + /// These are generated in advance to prevent timing attacks and/or DoS by third-party attackers + /// spoofing key updates. + next_crypto: Option>>, + accepted_0rtt: bool, + /// Whether the idle timer should be reset the next time an ack-eliciting packet is transmitted. + permit_idle_reset: bool, + /// Negotiated idle timeout + idle_timeout: Option, + timers: TimerTable, + /// Number of packets received which could not be authenticated + authentication_failures: u64, + /// Why the connection was lost, if it has been + error: Option, + /// Identifies Data-space packet numbers to skip. Not used in earlier spaces. + packet_number_filter: PacketNumberFilter, + + // + // Queued non-retransmittable 1-RTT data + // + /// Responses to PATH_CHALLENGE frames + path_responses: PathResponses, + close: bool, + + // + // ACK frequency + // + ack_frequency: AckFrequencyState, + + // + // Loss Detection + // + /// The number of times a PTO has been sent without receiving an ack. + pto_count: u32, + + // + // Congestion Control + // + /// Whether the most recently received packet had an ECN codepoint set + receiving_ecn: bool, + /// Number of packets authenticated + total_authed_packets: u64, + /// Whether the last `poll_transmit` call yielded no data because there was + /// no outgoing application data. + app_limited: bool, + + streams: StreamsState, + /// Surplus remote CIDs for future use on new paths + rem_cids: CidQueue, + // Attributes of CIDs generated by local peer + local_cid_state: CidState, + /// State of the unreliable datagram extension + datagrams: DatagramState, + /// Connection level statistics + stats: ConnectionStats, + /// QUIC version used for the connection. + version: u32, +} + +impl Connection { + pub(crate) fn new( + endpoint_config: Arc, + config: Arc, + init_cid: ConnectionId, + loc_cid: ConnectionId, + rem_cid: ConnectionId, + remote: SocketAddr, + local_ip: Option, + crypto: Box, + cid_gen: &dyn ConnectionIdGenerator, + now: Instant, + version: u32, + allow_mtud: bool, + rng_seed: [u8; 32], + side_args: SideArgs, + ) -> Self { + let pref_addr_cid = side_args.pref_addr_cid(); + let path_validated = side_args.path_validated(); + let connection_side = ConnectionSide::from(side_args); + let side = connection_side.side(); + let initial_space = PacketSpace { + crypto: Some(crypto.initial_keys(&init_cid, side)), + ..PacketSpace::new(now) + }; + let state = State::Handshake(state::Handshake { + rem_cid_set: side.is_server(), + expected_token: Bytes::new(), + client_hello: None, + }); + let mut rng = StdRng::from_seed(rng_seed); + let mut this = Self { + endpoint_config, + crypto, + handshake_cid: loc_cid, + rem_handshake_cid: rem_cid, + local_cid_state: CidState::new( + cid_gen.cid_len(), + cid_gen.cid_lifetime(), + now, + if pref_addr_cid.is_some() { 2 } else { 1 }, + ), + path: PathData::new(remote, allow_mtud, None, 0, now, &config), + path_counter: 0, + allow_mtud, + local_ip, + prev_path: None, + state, + side: connection_side, + zero_rtt_enabled: false, + zero_rtt_crypto: None, + key_phase: false, + // A small initial key phase size ensures peers that don't handle key updates correctly + // fail sooner rather than later. It's okay for both peers to do this, as the first one + // to perform an update will reset the other's key phase size in `update_keys`, and a + // simultaneous key update by both is just like a regular key update with a really fast + // response. Inspired by quic-go's similar behavior of performing the first key update + // at the 100th short-header packet. + key_phase_size: rng.random_range(10..1000), + peer_params: TransportParameters::default(), + orig_rem_cid: rem_cid, + initial_dst_cid: init_cid, + retry_src_cid: None, + events: VecDeque::new(), + endpoint_events: VecDeque::new(), + spin_enabled: config.allow_spin && rng.random_ratio(7, 8), + spin: false, + spaces: [initial_space, PacketSpace::new(now), PacketSpace::new(now)], + highest_space: SpaceId::Initial, + prev_crypto: None, + next_crypto: None, + accepted_0rtt: false, + permit_idle_reset: true, + idle_timeout: match config.max_idle_timeout { + None | Some(VarInt(0)) => None, + Some(dur) => Some(Duration::from_millis(dur.0)), + }, + timers: TimerTable::default(), + authentication_failures: 0, + error: None, + #[cfg(test)] + packet_number_filter: match config.deterministic_packet_numbers { + false => PacketNumberFilter::new(&mut rng), + true => PacketNumberFilter::disabled(), + }, + #[cfg(not(test))] + packet_number_filter: PacketNumberFilter::new(&mut rng), + + path_responses: PathResponses::default(), + close: false, + + ack_frequency: AckFrequencyState::new(get_max_ack_delay( + &TransportParameters::default(), + )), + + pto_count: 0, + + app_limited: false, + receiving_ecn: false, + total_authed_packets: 0, + + streams: StreamsState::new( + side, + config.max_concurrent_uni_streams, + config.max_concurrent_bidi_streams, + config.send_window, + config.receive_window, + config.stream_receive_window, + ), + datagrams: DatagramState::default(), + config, + rem_cids: CidQueue::new(rem_cid), + rng, + stats: ConnectionStats::default(), + version, + }; + if path_validated { + this.on_path_validated(); + } + if side.is_client() { + // Kick off the connection + this.write_crypto(); + this.init_0rtt(); + } + this + } + + /// Returns the next time at which `handle_timeout` should be called + /// + /// The value returned may change after: + /// - the application performed some I/O on the connection + /// - a call was made to `handle_event` + /// - a call to `poll_transmit` returned `Some` + /// - a call was made to `handle_timeout` + #[must_use] + pub fn poll_timeout(&mut self) -> Option { + self.timers.next_timeout() + } + + /// Returns application-facing events + /// + /// Connections should be polled for events after: + /// - a call was made to `handle_event` + /// - a call was made to `handle_timeout` + #[must_use] + pub fn poll(&mut self) -> Option { + if let Some(x) = self.events.pop_front() { + return Some(x); + } + + if let Some(event) = self.streams.poll() { + return Some(Event::Stream(event)); + } + + if let Some(err) = self.error.take() { + return Some(Event::ConnectionLost { reason: err }); + } + + None + } + + /// Return endpoint-facing events + #[must_use] + pub fn poll_endpoint_events(&mut self) -> Option { + self.endpoint_events.pop_front().map(EndpointEvent) + } + + /// Provide control over streams + #[must_use] + pub fn streams(&mut self) -> Streams<'_> { + Streams { + state: &mut self.streams, + conn_state: &self.state, + } + } + + /// Provide control over streams + #[must_use] + pub fn recv_stream(&mut self, id: StreamId) -> RecvStream<'_> { + assert!(id.dir() == Dir::Bi || id.initiator() != self.side.side()); + RecvStream { + id, + state: &mut self.streams, + pending: &mut self.spaces[SpaceId::Data].pending, + } + } + + /// Provide control over streams + #[must_use] + pub fn send_stream(&mut self, id: StreamId) -> SendStream<'_> { + assert!(id.dir() == Dir::Bi || id.initiator() == self.side.side()); + SendStream { + id, + state: &mut self.streams, + pending: &mut self.spaces[SpaceId::Data].pending, + conn_state: &self.state, + } + } + + /// Returns packets to transmit + /// + /// Connections should be polled for transmit after: + /// - the application performed some I/O on the connection + /// - a call was made to `handle_event` + /// - a call was made to `handle_timeout` + /// + /// `max_datagrams` specifies how many datagrams can be returned inside a + /// single Transmit using GSO. This must be at least 1. + #[must_use] + pub fn poll_transmit( + &mut self, + now: Instant, + max_datagrams: usize, + buf: &mut Vec, + ) -> Option { + assert!(max_datagrams != 0); + let max_datagrams = match self.config.enable_segmentation_offload { + false => 1, + true => max_datagrams, + }; + + let mut num_datagrams = 0; + // Position in `buf` of the first byte of the current UDP datagram. When coalescing QUIC + // packets, this can be earlier than the start of the current QUIC packet. + let mut datagram_start = 0; + let mut segment_size = usize::from(self.path.current_mtu()); + + if let Some(challenge) = self.send_path_challenge(now, buf) { + return Some(challenge); + } + + // If we need to send a probe, make sure we have something to send. + for space in SpaceId::iter() { + let request_immediate_ack = + space == SpaceId::Data && self.peer_supports_ack_frequency(); + self.spaces[space].maybe_queue_probe(request_immediate_ack, &self.streams); + } + + // Check whether we need to send a close message + let close = match self.state { + State::Drained => { + self.app_limited = true; + return None; + } + State::Draining | State::Closed(_) => { + // self.close is only reset once the associated packet had been + // encoded successfully + if !self.close { + self.app_limited = true; + return None; + } + true + } + _ => false, + }; + + // Check whether we need to send an ACK_FREQUENCY frame + if let Some(config) = &self.config.ack_frequency_config { + self.spaces[SpaceId::Data].pending.ack_frequency = self + .ack_frequency + .should_send_ack_frequency(self.path.rtt.get(), config, &self.peer_params) + && self.highest_space == SpaceId::Data + && self.peer_supports_ack_frequency(); + } + + // Reserving capacity can provide more capacity than we asked for. However, we are not + // allowed to write more than `segment_size`. Therefore the maximum capacity is tracked + // separately. + let mut buf_capacity = 0; + + let mut coalesce = true; + let mut builder_storage: Option = None; + let mut sent_frames = None; + let mut pad_datagram = false; + let mut pad_datagram_to_mtu = false; + let mut congestion_blocked = false; + + // Iterate over all spaces and find data to send + let mut space_idx = 0; + let spaces = [SpaceId::Initial, SpaceId::Handshake, SpaceId::Data]; + // This loop will potentially spend multiple iterations in the same `SpaceId`, + // so we cannot trivially rewrite it to take advantage of `SpaceId::iter()`. + while space_idx < spaces.len() { + let space_id = spaces[space_idx]; + // Number of bytes available for frames if this is a 1-RTT packet. We're guaranteed to + // be able to send an individual frame at least this large in the next 1-RTT + // packet. This could be generalized to support every space, but it's only needed to + // handle large fixed-size frames, which only exist in 1-RTT (application datagrams). We + // don't account for coalesced packets potentially occupying space because frames can + // always spill into the next datagram. + let pn = self.packet_number_filter.peek(&self.spaces[SpaceId::Data]); + let frame_space_1rtt = + segment_size.saturating_sub(self.predict_1rtt_overhead(Some(pn))); + + // Is there data or a close message to send in this space? + let can_send = self.space_can_send(space_id, frame_space_1rtt); + if can_send.is_empty() && (!close || self.spaces[space_id].crypto.is_none()) { + space_idx += 1; + continue; + } + + let mut ack_eliciting = !self.spaces[space_id].pending.is_empty(&self.streams) + || self.spaces[space_id].ping_pending + || self.spaces[space_id].immediate_ack_pending; + if space_id == SpaceId::Data { + ack_eliciting |= self.can_send_1rtt(frame_space_1rtt); + } + + pad_datagram_to_mtu |= space_id == SpaceId::Data && self.config.pad_to_mtu; + + // Can we append more data into the current buffer? + // It is not safe to assume that `buf.len()` is the end of the data, + // since the last packet might not have been finished. + let buf_end = if let Some(builder) = &builder_storage { + buf.len().max(builder.min_size) + builder.tag_len + } else { + buf.len() + }; + + let tag_len = if let Some(ref crypto) = self.spaces[space_id].crypto { + crypto.packet.local.tag_len() + } else if space_id == SpaceId::Data { + self.zero_rtt_crypto.as_ref().expect( + "sending packets in the application data space requires known 0-RTT or 1-RTT keys", + ).packet.tag_len() + } else { + unreachable!("tried to send {:?} packet without keys", space_id) + }; + if !coalesce || buf_capacity - buf_end < MIN_PACKET_SPACE + tag_len { + // We need to send 1 more datagram and extend the buffer for that. + + // Is 1 more datagram allowed? + if num_datagrams >= max_datagrams { + // No more datagrams allowed + break; + } + + // Anti-amplification is only based on `total_sent`, which gets + // updated at the end of this method. Therefore we pass the amount + // of bytes for datagrams that are already created, as well as 1 byte + // for starting another datagram. If there is any anti-amplification + // budget left, we always allow a full MTU to be sent + // (see https://github.com/quinn-rs/quinn/issues/1082) + if self + .path + .anti_amplification_blocked(segment_size as u64 * (num_datagrams as u64) + 1) + { + trace!("blocked by anti-amplification"); + break; + } + + // Congestion control and pacing checks + // Tail loss probes must not be blocked by congestion, or a deadlock could arise + if ack_eliciting && self.spaces[space_id].loss_probes == 0 { + // Assume the current packet will get padded to fill the segment + let untracked_bytes = if let Some(builder) = &builder_storage { + buf_capacity - builder.partial_encode.start + } else { + 0 + } as u64; + debug_assert!(untracked_bytes <= segment_size as u64); + + let bytes_to_send = segment_size as u64 + untracked_bytes; + if self.path.in_flight.bytes + bytes_to_send >= self.path.congestion.window() { + space_idx += 1; + congestion_blocked = true; + // We continue instead of breaking here in order to avoid + // blocking loss probes queued for higher spaces. + trace!("blocked by congestion control"); + continue; + } + + // Check whether the next datagram is blocked by pacing + let smoothed_rtt = self.path.rtt.get(); + if let Some(delay) = self.path.pacing.delay( + smoothed_rtt, + bytes_to_send, + self.path.current_mtu(), + self.path.congestion.window(), + now, + ) { + self.timers.set(Timer::Pacing, delay); + congestion_blocked = true; + // Loss probes should be subject to pacing, even though + // they are not congestion controlled. + trace!("blocked by pacing"); + break; + } + } + + // Finish current packet + if let Some(mut builder) = builder_storage.take() { + if pad_datagram { + builder.pad_to(MIN_INITIAL_SIZE); + } + + if num_datagrams > 1 || pad_datagram_to_mtu { + // If too many padding bytes would be required to continue the GSO batch + // after this packet, end the GSO batch here. Ensures that fixed-size frames + // with heterogeneous sizes (e.g. application datagrams) won't inadvertently + // waste large amounts of bandwidth. The exact threshold is a bit arbitrary + // and might benefit from further tuning, though there's no universally + // optimal value. + // + // Additionally, if this datagram is a loss probe and `segment_size` is + // larger than `INITIAL_MTU`, then padding it to `segment_size` to continue + // the GSO batch would risk failure to recover from a reduction in path + // MTU. Loss probes are the only packets for which we might grow + // `buf_capacity` by less than `segment_size`. + const MAX_PADDING: usize = 16; + let packet_len_unpadded = cmp::max(builder.min_size, buf.len()) + - datagram_start + + builder.tag_len; + if (packet_len_unpadded + MAX_PADDING < segment_size + && !pad_datagram_to_mtu) + || datagram_start + segment_size > buf_capacity + { + trace!( + "GSO truncated by demand for {} padding bytes or loss probe", + segment_size - packet_len_unpadded + ); + builder_storage = Some(builder); + break; + } + + // Pad the current datagram to GSO segment size so it can be included in the + // GSO batch. + builder.pad_to(segment_size as u16); + } + + builder.finish_and_track(now, self, sent_frames.take(), buf); + + if num_datagrams == 1 { + // Set the segment size for this GSO batch to the size of the first UDP + // datagram in the batch. Larger data that cannot be fragmented + // (e.g. application datagrams) will be included in a future batch. When + // sending large enough volumes of data for GSO to be useful, we expect + // packet sizes to usually be consistent, e.g. populated by max-size STREAM + // frames or uniformly sized datagrams. + segment_size = buf.len(); + // Clip the unused capacity out of the buffer so future packets don't + // overrun + buf_capacity = buf.len(); + + // Check whether the data we planned to send will fit in the reduced segment + // size. If not, bail out and leave it for the next GSO batch so we don't + // end up trying to send an empty packet. We can't easily compute the right + // segment size before the original call to `space_can_send`, because at + // that time we haven't determined whether we're going to coalesce with the + // first datagram or potentially pad it to `MIN_INITIAL_SIZE`. + if space_id == SpaceId::Data { + let frame_space_1rtt = + segment_size.saturating_sub(self.predict_1rtt_overhead(Some(pn))); + if self.space_can_send(space_id, frame_space_1rtt).is_empty() { + break; + } + } + } + } + + // Allocate space for another datagram + let next_datagram_size_limit = match self.spaces[space_id].loss_probes { + 0 => segment_size, + _ => { + self.spaces[space_id].loss_probes -= 1; + // Clamp the datagram to at most the minimum MTU to ensure that loss probes + // can get through and enable recovery even if the path MTU has shrank + // unexpectedly. + std::cmp::min(segment_size, usize::from(INITIAL_MTU)) + } + }; + buf_capacity += next_datagram_size_limit; + if buf.capacity() < buf_capacity { + // We reserve the maximum space for sending `max_datagrams` upfront + // to avoid any reallocations if more datagrams have to be appended later on. + // Benchmarks have shown shown a 5-10% throughput improvement + // compared to continuously resizing the datagram buffer. + // While this will lead to over-allocation for small transmits + // (e.g. purely containing ACKs), modern memory allocators + // (e.g. mimalloc and jemalloc) will pool certain allocation sizes + // and therefore this is still rather efficient. + buf.reserve(max_datagrams * segment_size); + } + num_datagrams += 1; + coalesce = true; + pad_datagram = false; + datagram_start = buf.len(); + + debug_assert_eq!( + datagram_start % segment_size, + 0, + "datagrams in a GSO batch must be aligned to the segment size" + ); + } else { + // We can append/coalesce the next packet into the current + // datagram. + // Finish current packet without adding extra padding + if let Some(builder) = builder_storage.take() { + builder.finish_and_track(now, self, sent_frames.take(), buf); + } + } + + debug_assert!(buf_capacity - buf.len() >= MIN_PACKET_SPACE); + + // + // From here on, we've determined that a packet will definitely be sent. + // + + if self.spaces[SpaceId::Initial].crypto.is_some() + && space_id == SpaceId::Handshake + && self.side.is_client() + { + // A client stops both sending and processing Initial packets when it + // sends its first Handshake packet. + self.discard_space(now, SpaceId::Initial); + } + if let Some(ref mut prev) = self.prev_crypto { + prev.update_unacked = false; + } + + debug_assert!( + builder_storage.is_none() && sent_frames.is_none(), + "Previous packet must have been finished" + ); + + let builder = builder_storage.insert(PacketBuilder::new( + now, + space_id, + self.rem_cids.active(), + buf, + buf_capacity, + datagram_start, + ack_eliciting, + self, + )?); + coalesce = coalesce && !builder.short_header; + + // https://tools.ietf.org/html/draft-ietf-quic-transport-34#section-14.1 + pad_datagram |= + space_id == SpaceId::Initial && (self.side.is_client() || ack_eliciting); + + if close { + trace!("sending CONNECTION_CLOSE"); + // Encode ACKs before the ConnectionClose message, to give the receiver + // a better approximate on what data has been processed. This is + // especially important with ack delay, since the peer might not + // have gotten any other ACK for the data earlier on. + if !self.spaces[space_id].pending_acks.ranges().is_empty() { + Self::populate_acks( + now, + self.receiving_ecn, + &mut SentFrames::default(), + &mut self.spaces[space_id], + buf, + &mut self.stats, + ); + } + + // Since there only 64 ACK frames there will always be enough space + // to encode the ConnectionClose frame too. However we still have the + // check here to prevent crashes if something changes. + debug_assert!( + buf.len() + frame::ConnectionClose::SIZE_BOUND < builder.max_size, + "ACKs should leave space for ConnectionClose" + ); + if buf.len() + frame::ConnectionClose::SIZE_BOUND < builder.max_size { + let max_frame_size = builder.max_size - buf.len(); + match self.state { + State::Closed(state::Closed { ref reason }) => { + if space_id == SpaceId::Data || reason.is_transport_layer() { + reason.encode(buf, max_frame_size) + } else { + frame::ConnectionClose { + error_code: TransportErrorCode::APPLICATION_ERROR, + frame_type: None, + reason: Bytes::new(), + } + .encode(buf, max_frame_size) + } + } + State::Draining => frame::ConnectionClose { + error_code: TransportErrorCode::NO_ERROR, + frame_type: None, + reason: Bytes::new(), + } + .encode(buf, max_frame_size), + _ => unreachable!( + "tried to make a close packet when the connection wasn't closed" + ), + } + } + if space_id == self.highest_space { + // Don't send another close packet + self.close = false; + // `CONNECTION_CLOSE` is the final packet + break; + } else { + // Send a close frame in every possible space for robustness, per RFC9000 + // "Immediate Close during the Handshake". Don't bother trying to send anything + // else. + space_idx += 1; + continue; + } + } + + // Send an off-path PATH_RESPONSE. Prioritized over on-path data to ensure that path + // validation can occur while the link is saturated. + if space_id == SpaceId::Data && num_datagrams == 1 { + if let Some((token, remote)) = self.path_responses.pop_off_path(self.path.remote) { + // `unwrap` guaranteed to succeed because `builder_storage` was populated just + // above. + let mut builder = builder_storage.take().unwrap(); + trace!("PATH_RESPONSE {:08x} (off-path)", token); + buf.write(frame::FrameType::PATH_RESPONSE); + buf.write(token); + self.stats.frame_tx.path_response += 1; + builder.pad_to(MIN_INITIAL_SIZE); + builder.finish_and_track( + now, + self, + Some(SentFrames { + non_retransmits: true, + ..SentFrames::default() + }), + buf, + ); + self.stats.udp_tx.on_sent(1, buf.len()); + return Some(Transmit { + destination: remote, + size: buf.len(), + ecn: None, + segment_size: None, + src_ip: self.local_ip, + }); + } + } + + let sent = + self.populate_packet(now, space_id, buf, builder.max_size, builder.exact_number); + + // ACK-only packets should only be sent when explicitly allowed. If we write them due to + // any other reason, there is a bug which leads to one component announcing write + // readiness while not writing any data. This degrades performance. The condition is + // only checked if the full MTU is available and when potentially large fixed-size + // frames aren't queued, so that lack of space in the datagram isn't the reason for just + // writing ACKs. + debug_assert!( + !(sent.is_ack_only(&self.streams) + && !can_send.acks + && can_send.other + && (buf_capacity - builder.datagram_start) == self.path.current_mtu() as usize + && self.datagrams.outgoing.is_empty()), + "SendableFrames was {can_send:?}, but only ACKs have been written" + ); + pad_datagram |= sent.requires_padding; + + if sent.largest_acked.is_some() { + self.spaces[space_id].pending_acks.acks_sent(); + self.timers.stop(Timer::MaxAckDelay); + } + + // Keep information about the packet around until it gets finalized + sent_frames = Some(sent); + + // Don't increment space_idx. + // We stay in the current space and check if there is more data to send. + } + + // Finish the last packet + if let Some(mut builder) = builder_storage { + if pad_datagram { + builder.pad_to(MIN_INITIAL_SIZE); + } + + // If this datagram is a loss probe and `segment_size` is larger than `INITIAL_MTU`, + // then padding it to `segment_size` would risk failure to recover from a reduction in + // path MTU. + // Loss probes are the only packets for which we might grow `buf_capacity` + // by less than `segment_size`. + if pad_datagram_to_mtu && buf_capacity >= datagram_start + segment_size { + builder.pad_to(segment_size as u16); + } + + let last_packet_number = builder.exact_number; + builder.finish_and_track(now, self, sent_frames, buf); + self.path + .congestion + .on_sent(now, buf.len() as u64, last_packet_number); + + self.config.qlog_sink.emit_recovery_metrics( + self.pto_count, + &mut self.path, + now, + self.orig_rem_cid, + ); + } + + self.app_limited = buf.is_empty() && !congestion_blocked; + + // Send MTU probe if necessary + if buf.is_empty() && self.state.is_established() { + let space_id = SpaceId::Data; + let probe_size = self + .path + .mtud + .poll_transmit(now, self.packet_number_filter.peek(&self.spaces[space_id]))?; + + let buf_capacity = probe_size as usize; + buf.reserve(buf_capacity); + + let mut builder = PacketBuilder::new( + now, + space_id, + self.rem_cids.active(), + buf, + buf_capacity, + 0, + true, + self, + )?; + + // We implement MTU probes as ping packets padded up to the probe size + buf.write(frame::FrameType::PING); + self.stats.frame_tx.ping += 1; + + // If supported by the peer, we want no delays to the probe's ACK + if self.peer_supports_ack_frequency() { + buf.write(frame::FrameType::IMMEDIATE_ACK); + self.stats.frame_tx.immediate_ack += 1; + } + + builder.pad_to(probe_size); + let sent_frames = SentFrames { + non_retransmits: true, + ..Default::default() + }; + builder.finish_and_track(now, self, Some(sent_frames), buf); + + self.stats.path.sent_plpmtud_probes += 1; + num_datagrams = 1; + + trace!(?probe_size, "writing MTUD probe"); + } + + if buf.is_empty() { + return None; + } + + trace!("sending {} bytes in {} datagrams", buf.len(), num_datagrams); + self.path.total_sent = self.path.total_sent.saturating_add(buf.len() as u64); + + self.stats.udp_tx.on_sent(num_datagrams as u64, buf.len()); + + Some(Transmit { + destination: self.path.remote, + size: buf.len(), + ecn: if self.path.sending_ecn { + Some(EcnCodepoint::Ect0) + } else { + None + }, + segment_size: match num_datagrams { + 1 => None, + _ => Some(segment_size), + }, + src_ip: self.local_ip, + }) + } + + /// Send PATH_CHALLENGE for a previous path if necessary + fn send_path_challenge(&mut self, now: Instant, buf: &mut Vec) -> Option { + let (prev_cid, prev_path) = self.prev_path.as_mut()?; + if !prev_path.challenge_pending { + return None; + } + prev_path.challenge_pending = false; + let token = prev_path + .challenge + .expect("previous path challenge pending without token"); + let destination = prev_path.remote; + debug_assert_eq!( + self.highest_space, + SpaceId::Data, + "PATH_CHALLENGE queued without 1-RTT keys" + ); + buf.reserve(MIN_INITIAL_SIZE as usize); + + let buf_capacity = buf.capacity(); + + // Use the previous CID to avoid linking the new path with the previous path. We + // don't bother accounting for possible retirement of that prev_cid because this is + // sent once, immediately after migration, when the CID is known to be valid. Even + // if a post-migration packet caused the CID to be retired, it's fair to pretend + // this is sent first. + let mut builder = PacketBuilder::new( + now, + SpaceId::Data, + *prev_cid, + buf, + buf_capacity, + 0, + false, + self, + )?; + trace!("validating previous path with PATH_CHALLENGE {:08x}", token); + buf.write(frame::FrameType::PATH_CHALLENGE); + buf.write(token); + self.stats.frame_tx.path_challenge += 1; + + // An endpoint MUST expand datagrams that contain a PATH_CHALLENGE frame + // to at least the smallest allowed maximum datagram size of 1200 bytes, + // unless the anti-amplification limit for the path does not permit + // sending a datagram of this size + builder.pad_to(MIN_INITIAL_SIZE); + + builder.finish(self, now, buf); + self.stats.udp_tx.on_sent(1, buf.len()); + + Some(Transmit { + destination, + size: buf.len(), + ecn: None, + segment_size: None, + src_ip: self.local_ip, + }) + } + + /// Indicate what types of frames are ready to send for the given space + fn space_can_send(&self, space_id: SpaceId, frame_space_1rtt: usize) -> SendableFrames { + if self.spaces[space_id].crypto.is_none() + && (space_id != SpaceId::Data + || self.zero_rtt_crypto.is_none() + || self.side.is_server()) + { + // No keys available for this space + return SendableFrames::empty(); + } + let mut can_send = self.spaces[space_id].can_send(&self.streams); + if space_id == SpaceId::Data { + can_send.other |= self.can_send_1rtt(frame_space_1rtt); + } + can_send + } + + /// Process `ConnectionEvent`s generated by the associated `Endpoint` + /// + /// Will execute protocol logic upon receipt of a connection event, in turn preparing signals + /// (including application `Event`s, `EndpointEvent`s and outgoing datagrams) that should be + /// extracted through the relevant methods. + pub fn handle_event(&mut self, event: ConnectionEvent) { + use ConnectionEventInner::*; + match event.0 { + Datagram(DatagramConnectionEvent { + now, + remote, + ecn, + first_decode, + remaining, + }) => { + // If this packet could initiate a migration and we're a client or a server that + // forbids migration, drop the datagram. This could be relaxed to heuristically + // permit NAT-rebinding-like migration. + if remote != self.path.remote && !self.side.remote_may_migrate() { + trace!("discarding packet from unrecognized peer {}", remote); + return; + } + + let was_anti_amplification_blocked = self.path.anti_amplification_blocked(1); + + self.stats.udp_rx.datagrams += 1; + self.stats.udp_rx.bytes += first_decode.len() as u64; + let data_len = first_decode.len(); + + self.handle_decode(now, remote, ecn, first_decode); + // The current `path` might have changed inside `handle_decode`, + // since the packet could have triggered a migration. Make sure + // the data received is accounted for the most recent path by accessing + // `path` after `handle_decode`. + self.path.total_recvd = self.path.total_recvd.saturating_add(data_len as u64); + + if let Some(data) = remaining { + self.stats.udp_rx.bytes += data.len() as u64; + self.handle_coalesced(now, remote, ecn, data); + } + + self.config.qlog_sink.emit_recovery_metrics( + self.pto_count, + &mut self.path, + now, + self.orig_rem_cid, + ); + + if was_anti_amplification_blocked { + // A prior attempt to set the loss detection timer may have failed due to + // anti-amplification, so ensure it's set now. Prevents a handshake deadlock if + // the server's first flight is lost. + self.set_loss_detection_timer(now); + } + } + NewIdentifiers(ids, now) => { + self.local_cid_state.new_cids(&ids, now); + ids.into_iter().rev().for_each(|frame| { + self.spaces[SpaceId::Data].pending.new_cids.push(frame); + }); + // Update Timer::PushNewCid + if self + .timers + .get(Timer::PushNewCid) + .map_or(true, |x| x <= now) + { + self.reset_cid_retirement(); + } + } + } + } + + /// Process timer expirations + /// + /// Executes protocol logic, potentially preparing signals (including application `Event`s, + /// `EndpointEvent`s and outgoing datagrams) that should be extracted through the relevant + /// methods. + /// + /// It is most efficient to call this immediately after the system clock reaches the latest + /// `Instant` that was output by `poll_timeout`; however spurious extra calls will simply + /// no-op and therefore are safe. + pub fn handle_timeout(&mut self, now: Instant) { + for &timer in &Timer::VALUES { + if !self.timers.is_expired(timer, now) { + continue; + } + self.timers.stop(timer); + trace!(timer = ?timer, "timeout"); + match timer { + Timer::Close => { + self.state = State::Drained; + self.endpoint_events.push_back(EndpointEventInner::Drained); + } + Timer::Idle => { + self.kill(ConnectionError::TimedOut); + } + Timer::KeepAlive => { + trace!("sending keep-alive"); + self.ping(); + } + Timer::LossDetection => { + self.on_loss_detection_timeout(now); + + self.config.qlog_sink.emit_recovery_metrics( + self.pto_count, + &mut self.path, + now, + self.orig_rem_cid, + ); + } + Timer::KeyDiscard => { + self.zero_rtt_crypto = None; + self.prev_crypto = None; + } + Timer::PathValidation => { + debug!("path validation failed"); + if let Some((_, prev)) = self.prev_path.take() { + self.path = prev; + } + self.path.challenge = None; + self.path.challenge_pending = false; + } + Timer::Pacing => trace!("pacing timer expired"), + Timer::PushNewCid => { + // Update `retire_prior_to` field in NEW_CONNECTION_ID frame + let num_new_cid = self.local_cid_state.on_cid_timeout().into(); + if !self.state.is_closed() { + trace!( + "push a new cid to peer RETIRE_PRIOR_TO field {}", + self.local_cid_state.retire_prior_to() + ); + self.endpoint_events + .push_back(EndpointEventInner::NeedIdentifiers(now, num_new_cid)); + } + } + Timer::MaxAckDelay => { + trace!("max ack delay reached"); + // This timer is only armed in the Data space + self.spaces[SpaceId::Data] + .pending_acks + .on_max_ack_delay_timeout() + } + } + } + } + + /// Close a connection immediately + /// + /// This does not ensure delivery of outstanding data. It is the application's responsibility to + /// call this only when all important communications have been completed, e.g. by calling + /// [`SendStream::finish`] on outstanding streams and waiting for the corresponding + /// [`StreamEvent::Finished`] event. + /// + /// If [`Streams::send_streams`] returns 0, all outstanding stream data has been + /// delivered. There may still be data from the peer that has not been received. + /// + /// [`StreamEvent::Finished`]: crate::StreamEvent::Finished + pub fn close(&mut self, now: Instant, error_code: VarInt, reason: Bytes) { + self.close_inner( + now, + Close::Application(frame::ApplicationClose { error_code, reason }), + ) + } + + fn close_inner(&mut self, now: Instant, reason: Close) { + let was_closed = self.state.is_closed(); + if !was_closed { + self.close_common(); + self.set_close_timer(now); + self.close = true; + self.state = State::Closed(state::Closed { reason }); + } + } + + /// Control datagrams + pub fn datagrams(&mut self) -> Datagrams<'_> { + Datagrams { conn: self } + } + + /// Returns connection statistics + pub fn stats(&self) -> ConnectionStats { + let mut stats = self.stats; + stats.path.rtt = self.path.rtt.get(); + stats.path.cwnd = self.path.congestion.window(); + stats.path.current_mtu = self.path.mtud.current_mtu(); + + stats + } + + /// Ping the remote endpoint + /// + /// Causes an ACK-eliciting packet to be transmitted. + pub fn ping(&mut self) { + self.spaces[self.highest_space].ping_pending = true; + } + + /// Update traffic keys spontaneously + /// + /// This can be useful for testing key updates, as they otherwise only happen infrequently. + pub fn force_key_update(&mut self) { + if !self.state.is_established() { + debug!("ignoring forced key update in illegal state"); + return; + } + if self.prev_crypto.is_some() { + // We already just updated, or are currently updating, the keys. Concurrent key updates + // are illegal. + debug!("ignoring redundant forced key update"); + return; + } + self.update_keys(None, false); + } + + // Compatibility wrapper for quinn < 0.11.7. Remove for 0.12. + #[doc(hidden)] + #[deprecated] + pub fn initiate_key_update(&mut self) { + self.force_key_update(); + } + + /// Get a session reference + pub fn crypto_session(&self) -> &dyn crypto::Session { + &*self.crypto + } + + /// Whether the connection is in the process of being established + /// + /// If this returns `false`, the connection may be either established or closed, signaled by the + /// emission of a `Connected` or `ConnectionLost` message respectively. + pub fn is_handshaking(&self) -> bool { + self.state.is_handshake() + } + + /// Whether the connection is closed + /// + /// Closed connections cannot transport any further data. A connection becomes closed when + /// either peer application intentionally closes it, or when either transport layer detects an + /// error such as a time-out or certificate validation failure. + /// + /// A `ConnectionLost` event is emitted with details when the connection becomes closed. + pub fn is_closed(&self) -> bool { + self.state.is_closed() + } + + /// Whether there is no longer any need to keep the connection around + /// + /// Closed connections become drained after a brief timeout to absorb any remaining in-flight + /// packets from the peer. All drained connections have been closed. + pub fn is_drained(&self) -> bool { + self.state.is_drained() + } + + /// For clients, if the peer accepted the 0-RTT data packets + /// + /// The value is meaningless until after the handshake completes. + pub fn accepted_0rtt(&self) -> bool { + self.accepted_0rtt + } + + /// Whether 0-RTT is/was possible during the handshake + pub fn has_0rtt(&self) -> bool { + self.zero_rtt_enabled + } + + /// Whether there are any pending retransmits + pub fn has_pending_retransmits(&self) -> bool { + !self.spaces[SpaceId::Data].pending.is_empty(&self.streams) + } + + /// Look up whether we're the client or server of this Connection + pub fn side(&self) -> Side { + self.side.side() + } + + /// The latest socket address for this connection's peer + pub fn remote_address(&self) -> SocketAddr { + self.path.remote + } + + /// The local IP address which was used when the peer established + /// the connection + /// + /// This can be different from the address the endpoint is bound to, in case + /// the endpoint is bound to a wildcard address like `0.0.0.0` or `::`. + /// + /// This will return `None` for clients, or when no `local_ip` was passed to + /// [`Endpoint::handle()`](crate::Endpoint::handle) for the datagrams establishing this + /// connection. + pub fn local_ip(&self) -> Option { + self.local_ip + } + + /// Current best estimate of this connection's latency (round-trip-time) + pub fn rtt(&self) -> Duration { + self.path.rtt.get() + } + + /// Current state of this connection's congestion controller, for debugging purposes + pub fn congestion_state(&self) -> &dyn Controller { + self.path.congestion.as_ref() + } + + /// Resets path-specific settings. + /// + /// This will force-reset several subsystems related to a specific network path. + /// Currently this is the congestion controller, round-trip estimator, and the MTU + /// discovery. + /// + /// This is useful when it is known the underlying network path has changed and the old + /// state of these subsystems is no longer valid or optimal. In this case it might be + /// faster or reduce loss to settle on optimal values by restarting from the initial + /// configuration in the [`TransportConfig`]. + pub fn path_changed(&mut self, now: Instant) { + self.path.reset(now, &self.config); + } + + /// Modify the number of remotely initiated streams that may be concurrently open + /// + /// No streams may be opened by the peer unless fewer than `count` are already open. Large + /// `count`s increase both minimum and worst-case memory consumption. + pub fn set_max_concurrent_streams(&mut self, dir: Dir, count: VarInt) { + self.streams.set_max_concurrent(dir, count); + // If the limit was reduced, then a flow control update previously deemed insignificant may + // now be significant. + let pending = &mut self.spaces[SpaceId::Data].pending; + self.streams.queue_max_stream_id(pending); + } + + /// Current number of remotely initiated streams that may be concurrently open + /// + /// If the target for this limit is reduced using [`set_max_concurrent_streams`](Self::set_max_concurrent_streams), + /// it will not change immediately, even if fewer streams are open. Instead, it will + /// decrement by one for each time a remotely initiated stream of matching directionality is closed. + pub fn max_concurrent_streams(&self, dir: Dir) -> u64 { + self.streams.max_concurrent(dir) + } + + /// The peer's advertised `initial_max_streams_bidi` transport parameter. + /// + /// This is the total number of client-initiated bidirectional streams the + /// peer will allow us to open before it must send a `MAX_STREAMS` frame to + /// raise the limit. Useful for diagnosing credit-starvation hangs on + /// `open_bi()` where the peer advertises a low initial limit and never + /// raises it at runtime. + pub fn peer_params_initial_max_streams_bidi(&self) -> u64 { + self.peer_params.initial_max_streams_bidi.into_inner() + } + + /// The peer's advertised `initial_max_streams_uni` transport parameter. + pub fn peer_params_initial_max_streams_uni(&self) -> u64 { + self.peer_params.initial_max_streams_uni.into_inner() + } + + /// The peer's advertised `initial_max_data` transport parameter. + pub fn peer_params_initial_max_data(&self) -> u64 { + self.peer_params.initial_max_data.into_inner() + } + + /// See [`TransportConfig::send_window()`] + pub fn set_send_window(&mut self, send_window: u64) { + self.streams.set_send_window(send_window); + } + + /// See [`TransportConfig::receive_window()`] + pub fn set_receive_window(&mut self, receive_window: VarInt) { + if self.streams.set_receive_window(receive_window) { + self.spaces[SpaceId::Data].pending.max_data = true; + } + } + + fn on_ack_received( + &mut self, + now: Instant, + space: SpaceId, + ack: frame::Ack, + ) -> Result<(), TransportError> { + if ack.largest >= self.spaces[space].next_packet_number { + return Err(TransportError::PROTOCOL_VIOLATION("unsent packet acked")); + } + let new_largest = { + let space = &mut self.spaces[space]; + if space + .largest_acked_packet + .map_or(true, |pn| ack.largest > pn) + { + space.largest_acked_packet = Some(ack.largest); + if let Some(info) = space.sent_packets.get(&ack.largest) { + // This should always succeed, but a misbehaving peer might ACK a packet we + // haven't sent. At worst, that will result in us spuriously reducing the + // congestion window. + space.largest_acked_packet_sent = info.time_sent; + } + true + } else { + false + } + }; + + // Avoid DoS from unreasonably huge ack ranges by filtering out just the new acks. + let mut newly_acked = ArrayRangeSet::new(); + for range in ack.iter() { + self.packet_number_filter.check_ack(space, range.clone())?; + for (&pn, _) in self.spaces[space].sent_packets.range(range) { + newly_acked.insert_one(pn); + } + } + + if newly_acked.is_empty() { + return Ok(()); + } + + let mut ack_eliciting_acked = false; + for packet in newly_acked.elts() { + if let Some(info) = self.spaces[space].take(packet) { + if let Some(acked) = info.largest_acked { + // Assume ACKs for all packets below the largest acknowledged in `packet` have + // been received. This can cause the peer to spuriously retransmit if some of + // our earlier ACKs were lost, but allows for simpler state tracking. See + // discussion at + // https://www.rfc-editor.org/rfc/rfc9000.html#name-limiting-ranges-by-tracking + self.spaces[space].pending_acks.subtract_below(acked); + } + ack_eliciting_acked |= info.ack_eliciting; + + // Notify MTU discovery that a packet was acked, because it might be an MTU probe + let mtu_updated = self.path.mtud.on_acked(space, packet, info.size); + if mtu_updated { + self.path + .congestion + .on_mtu_update(self.path.mtud.current_mtu()); + } + + // Notify ack frequency that a packet was acked, because it might contain an ACK_FREQUENCY frame + self.ack_frequency.on_acked(packet); + + self.on_packet_acked(now, info); + } + } + + self.path.congestion.on_end_acks( + now, + self.path.in_flight.bytes, + self.app_limited, + self.spaces[space].largest_acked_packet, + ); + + if new_largest && ack_eliciting_acked { + let ack_delay = if space != SpaceId::Data { + Duration::from_micros(0) + } else { + cmp::min( + self.ack_frequency.peer_max_ack_delay, + Duration::from_micros(ack.delay << self.peer_params.ack_delay_exponent.0), + ) + }; + let rtt = instant_saturating_sub(now, self.spaces[space].largest_acked_packet_sent); + self.path.rtt.update(ack_delay, rtt); + if self.path.first_packet_after_rtt_sample.is_none() { + self.path.first_packet_after_rtt_sample = + Some((space, self.spaces[space].next_packet_number)); + } + } + + // Must be called before crypto/pto_count are clobbered + self.detect_lost_packets(now, space, true); + + if self.peer_completed_address_validation() { + self.pto_count = 0; + } + + // Explicit congestion notification + if self.path.sending_ecn { + if let Some(ecn) = ack.ecn { + // We only examine ECN counters from ACKs that we are certain we received in transmit + // order, allowing us to compute an increase in ECN counts to compare against the number + // of newly acked packets that remains well-defined in the presence of arbitrary packet + // reordering. + if new_largest { + let sent = self.spaces[space].largest_acked_packet_sent; + self.process_ecn(now, space, newly_acked.len() as u64, ecn, sent); + } + } else { + // We always start out sending ECN, so any ack that doesn't acknowledge it disables it. + debug!("ECN not acknowledged by peer"); + self.path.sending_ecn = false; + } + } + + self.set_loss_detection_timer(now); + Ok(()) + } + + /// Process a new ECN block from an in-order ACK + fn process_ecn( + &mut self, + now: Instant, + space: SpaceId, + newly_acked: u64, + ecn: frame::EcnCounts, + largest_sent_time: Instant, + ) { + match self.spaces[space].detect_ecn(newly_acked, ecn) { + Err(e) => { + debug!("halting ECN due to verification failure: {}", e); + self.path.sending_ecn = false; + // Wipe out the existing value because it might be garbage and could interfere with + // future attempts to use ECN on new paths. + self.spaces[space].ecn_feedback = frame::EcnCounts::ZERO; + } + Ok(false) => {} + Ok(true) => { + self.stats.path.congestion_events += 1; + self.path + .congestion + .on_congestion_event(now, largest_sent_time, false, 0); + } + } + } + + // Not timing-aware, so it's safe to call this for inferred acks, such as arise from + // high-latency handshakes + fn on_packet_acked(&mut self, now: Instant, info: SentPacket) { + self.remove_in_flight(&info); + if info.ack_eliciting && self.path.challenge.is_none() { + // Only pass ACKs to the congestion controller if we are not validating the current + // path, so as to ignore any ACKs from older paths still coming in. + self.path.congestion.on_ack( + now, + info.time_sent, + info.size.into(), + self.app_limited, + &self.path.rtt, + ); + } + + // Update state for confirmed delivery of frames + if let Some(retransmits) = info.retransmits.get() { + for (id, _) in retransmits.reset_stream.iter() { + self.streams.reset_acked(*id); + } + } + + for frame in info.stream_frames { + self.streams.received_ack_of(frame); + } + } + + fn set_key_discard_timer(&mut self, now: Instant, space: SpaceId) { + let start = if self.zero_rtt_crypto.is_some() { + now + } else { + self.prev_crypto + .as_ref() + .expect("no previous keys") + .end_packet + .as_ref() + .expect("update not acknowledged yet") + .1 + }; + self.timers + .set(Timer::KeyDiscard, start + self.pto(space) * 3); + } + + fn on_loss_detection_timeout(&mut self, now: Instant) { + if let Some((_, pn_space)) = self.loss_time_and_space() { + // Time threshold loss Detection + self.detect_lost_packets(now, pn_space, false); + self.set_loss_detection_timer(now); + return; + } + + let (_, space) = match self.pto_time_and_space(now) { + Some(x) => x, + None => { + error!("PTO expired while unset"); + return; + } + }; + trace!( + in_flight = self.path.in_flight.bytes, + count = self.pto_count, + ?space, + "PTO fired" + ); + + let count = match self.path.in_flight.ack_eliciting { + // A PTO when we're not expecting any ACKs must be due to handshake anti-amplification + // deadlock preventions + 0 => { + debug_assert!(!self.peer_completed_address_validation()); + 1 + } + // Conventional loss probe + _ => 2, + }; + self.spaces[space].loss_probes = self.spaces[space].loss_probes.saturating_add(count); + self.pto_count = self.pto_count.saturating_add(1); + self.set_loss_detection_timer(now); + } + + fn detect_lost_packets(&mut self, now: Instant, pn_space: SpaceId, due_to_ack: bool) { + let mut lost_packets = Vec::::new(); + let mut lost_mtu_probe = None; + let in_flight_mtu_probe = self.path.mtud.in_flight_mtu_probe(); + let rtt = self.path.rtt.conservative(); + let loss_delay = cmp::max(rtt.mul_f32(self.config.time_threshold), TIMER_GRANULARITY); + + // Packets sent before this time are deemed lost. + let lost_send_time = now.checked_sub(loss_delay).unwrap(); + let largest_acked_packet = self.spaces[pn_space].largest_acked_packet.unwrap(); + let packet_threshold = self.config.packet_threshold as u64; + let mut size_of_lost_packets = 0u64; + + // InPersistentCongestion: Determine if all packets in the time period before the newest + // lost packet, including the edges, are marked lost. PTO computation must always + // include max ACK delay, i.e. operate as if in Data space (see RFC9001 §7.6.1). + let congestion_period = + self.pto(SpaceId::Data) * self.config.persistent_congestion_threshold; + let mut persistent_congestion_start: Option = None; + let mut prev_packet = None; + let mut in_persistent_congestion = false; + + let space = &mut self.spaces[pn_space]; + space.loss_time = None; + + for (&packet, info) in space.sent_packets.range(0..largest_acked_packet) { + if prev_packet != Some(packet.wrapping_sub(1)) { + // An intervening packet was acknowledged + persistent_congestion_start = None; + } + + if info.time_sent <= lost_send_time || largest_acked_packet >= packet + packet_threshold + { + if Some(packet) == in_flight_mtu_probe { + // Lost MTU probes are not included in `lost_packets`, because they should not + // trigger a congestion control response + lost_mtu_probe = in_flight_mtu_probe; + } else { + lost_packets.push(packet); + size_of_lost_packets += info.size as u64; + if info.ack_eliciting && due_to_ack { + match persistent_congestion_start { + // Two ACK-eliciting packets lost more than congestion_period apart, with no + // ACKed packets in between + Some(start) if info.time_sent - start > congestion_period => { + in_persistent_congestion = true; + } + // Persistent congestion must start after the first RTT sample + None if self + .path + .first_packet_after_rtt_sample + .is_some_and(|x| x < (pn_space, packet)) => + { + persistent_congestion_start = Some(info.time_sent); + } + _ => {} + } + } + } + } else { + let next_loss_time = info.time_sent + loss_delay; + space.loss_time = Some( + space + .loss_time + .map_or(next_loss_time, |x| cmp::min(x, next_loss_time)), + ); + persistent_congestion_start = None; + } + + prev_packet = Some(packet); + } + + // OnPacketsLost + if let Some(largest_lost) = lost_packets.last().cloned() { + let old_bytes_in_flight = self.path.in_flight.bytes; + let largest_lost_sent = self.spaces[pn_space].sent_packets[&largest_lost].time_sent; + self.stats.path.lost_packets += lost_packets.len() as u64; + self.stats.path.lost_bytes += size_of_lost_packets; + trace!( + "packets lost: {:?}, bytes lost: {}", + lost_packets, size_of_lost_packets + ); + + for &packet in &lost_packets { + let info = self.spaces[pn_space].take(packet).unwrap(); // safe: lost_packets is populated just above + self.config.qlog_sink.emit_packet_lost( + packet, + &info, + lost_send_time, + pn_space, + now, + self.orig_rem_cid, + ); + self.remove_in_flight(&info); + for frame in info.stream_frames { + self.streams.retransmit(frame); + } + self.spaces[pn_space].pending |= info.retransmits; + self.path.mtud.on_non_probe_lost(packet, info.size); + } + + if self.path.mtud.black_hole_detected(now) { + self.stats.path.black_holes_detected += 1; + self.path + .congestion + .on_mtu_update(self.path.mtud.current_mtu()); + if let Some(max_datagram_size) = self.datagrams().max_size() { + self.datagrams.drop_oversized(max_datagram_size); + } + } + + // Don't apply congestion penalty for lost ack-only packets + let lost_ack_eliciting = old_bytes_in_flight != self.path.in_flight.bytes; + + if lost_ack_eliciting { + self.stats.path.congestion_events += 1; + self.path.congestion.on_congestion_event( + now, + largest_lost_sent, + in_persistent_congestion, + size_of_lost_packets, + ); + } + } + + // Handle a lost MTU probe + if let Some(packet) = lost_mtu_probe { + let info = self.spaces[SpaceId::Data].take(packet).unwrap(); // safe: lost_mtu_probe is omitted from lost_packets, and therefore must not have been removed yet + self.remove_in_flight(&info); + self.path.mtud.on_probe_lost(); + self.stats.path.lost_plpmtud_probes += 1; + } + } + + fn loss_time_and_space(&self) -> Option<(Instant, SpaceId)> { + SpaceId::iter() + .filter_map(|id| Some((self.spaces[id].loss_time?, id))) + .min_by_key(|&(time, _)| time) + } + + fn pto_time_and_space(&self, now: Instant) -> Option<(Instant, SpaceId)> { + let backoff = 2u32.pow(self.pto_count.min(MAX_BACKOFF_EXPONENT)); + let mut duration = self.path.rtt.pto_base() * backoff; + + if self.path.in_flight.ack_eliciting == 0 { + debug_assert!(!self.peer_completed_address_validation()); + let space = match self.highest_space { + SpaceId::Handshake => SpaceId::Handshake, + _ => SpaceId::Initial, + }; + return Some((now + duration, space)); + } + + let mut result = None; + for space in SpaceId::iter() { + if !self.spaces[space].has_in_flight() { + continue; + } + if space == SpaceId::Data { + // Skip ApplicationData until handshake completes. + if self.is_handshaking() { + return result; + } + // Include max_ack_delay and backoff for ApplicationData. + duration += self.ack_frequency.max_ack_delay_for_pto() * backoff; + } + let last_ack_eliciting = match self.spaces[space].time_of_last_ack_eliciting_packet { + Some(time) => time, + None => continue, + }; + let pto = last_ack_eliciting + duration; + if result.map_or(true, |(earliest_pto, _)| pto < earliest_pto) { + result = Some((pto, space)); + } + } + result + } + + fn peer_completed_address_validation(&self) -> bool { + if self.side.is_server() || self.state.is_closed() { + return true; + } + // The server is guaranteed to have validated our address if any of our handshake or 1-RTT + // packets are acknowledged or we've seen HANDSHAKE_DONE and discarded handshake keys. + self.spaces[SpaceId::Handshake] + .largest_acked_packet + .is_some() + || self.spaces[SpaceId::Data].largest_acked_packet.is_some() + || (self.spaces[SpaceId::Data].crypto.is_some() + && self.spaces[SpaceId::Handshake].crypto.is_none()) + } + + fn set_loss_detection_timer(&mut self, now: Instant) { + if self.state.is_closed() { + // No loss detection takes place on closed connections, and `close_common` already + // stopped time timer. Ensure we don't restart it inadvertently, e.g. in response to a + // reordered packet being handled by state-insensitive code. + return; + } + + if let Some((loss_time, _)) = self.loss_time_and_space() { + // Time threshold loss detection. + self.timers.set(Timer::LossDetection, loss_time); + return; + } + + if self.path.anti_amplification_blocked(1) { + // We wouldn't be able to send anything, so don't bother. + self.timers.stop(Timer::LossDetection); + return; + } + + if self.path.in_flight.ack_eliciting == 0 && self.peer_completed_address_validation() { + // There is nothing to detect lost, so no timer is set. However, the client needs to arm + // the timer if the server might be blocked by the anti-amplification limit. + self.timers.stop(Timer::LossDetection); + return; + } + + // Determine which PN space to arm PTO for. + // Calculate PTO duration + if let Some((timeout, _)) = self.pto_time_and_space(now) { + self.timers.set(Timer::LossDetection, timeout); + } else { + self.timers.stop(Timer::LossDetection); + } + } + + /// Probe Timeout + fn pto(&self, space: SpaceId) -> Duration { + let max_ack_delay = match space { + SpaceId::Initial | SpaceId::Handshake => Duration::ZERO, + SpaceId::Data => self.ack_frequency.max_ack_delay_for_pto(), + }; + self.path.rtt.pto_base() + max_ack_delay + } + + fn on_packet_authenticated( + &mut self, + now: Instant, + space_id: SpaceId, + ecn: Option, + packet: Option, + spin: bool, + is_1rtt: bool, + ) { + self.total_authed_packets += 1; + self.reset_keep_alive(now); + self.reset_idle_timeout(now, space_id); + self.permit_idle_reset = true; + self.receiving_ecn |= ecn.is_some(); + if let Some(x) = ecn { + let space = &mut self.spaces[space_id]; + space.ecn_counters += x; + + if x.is_ce() { + space.pending_acks.set_immediate_ack_required(); + } + } + + let packet = match packet { + Some(x) => x, + None => return, + }; + if self.side.is_server() { + if self.spaces[SpaceId::Initial].crypto.is_some() && space_id == SpaceId::Handshake { + // A server stops sending and processing Initial packets when it receives its first Handshake packet. + self.discard_space(now, SpaceId::Initial); + } + if self.zero_rtt_crypto.is_some() && is_1rtt { + // Discard 0-RTT keys soon after receiving a 1-RTT packet + self.set_key_discard_timer(now, space_id) + } + } + let space = &mut self.spaces[space_id]; + space.pending_acks.insert_one(packet, now); + if packet >= space.rx_packet { + space.rx_packet = packet; + // Update outgoing spin bit, inverting iff we're the client + self.spin = self.side.is_client() ^ spin; + } + + self.config.qlog_sink.emit_packet_received( + packet, + space_id, + !is_1rtt, + now, + self.orig_rem_cid, + ); + } + + fn reset_idle_timeout(&mut self, now: Instant, space: SpaceId) { + let timeout = match self.idle_timeout { + None => return, + Some(dur) => dur, + }; + if self.state.is_closed() { + self.timers.stop(Timer::Idle); + return; + } + let dt = cmp::max(timeout, 3 * self.pto(space)); + self.timers.set(Timer::Idle, now + dt); + } + + fn reset_keep_alive(&mut self, now: Instant) { + let interval = match self.config.keep_alive_interval { + Some(x) if self.state.is_established() => x, + _ => return, + }; + self.timers.set(Timer::KeepAlive, now + interval); + } + + fn reset_cid_retirement(&mut self) { + if let Some(t) = self.local_cid_state.next_timeout() { + self.timers.set(Timer::PushNewCid, t); + } + } + + /// Handle the already-decrypted first packet from the client + /// + /// Decrypting the first packet in the `Endpoint` allows stateless packet handling to be more + /// efficient. + pub(crate) fn handle_first_packet( + &mut self, + now: Instant, + remote: SocketAddr, + ecn: Option, + packet_number: u64, + packet: InitialPacket, + remaining: Option, + ) -> Result<(), ConnectionError> { + let span = trace_span!("first recv"); + let _guard = span.enter(); + debug_assert!(self.side.is_server()); + let len = packet.header_data.len() + packet.payload.len(); + self.path.total_recvd = len as u64; + + match self.state { + State::Handshake(ref mut state) => { + state.expected_token = packet.header.token.clone(); + } + _ => unreachable!("first packet must be delivered in Handshake state"), + } + + self.on_packet_authenticated( + now, + SpaceId::Initial, + ecn, + Some(packet_number), + false, + false, + ); + + self.process_decrypted_packet(now, remote, Some(packet_number), packet.into())?; + if let Some(data) = remaining { + self.handle_coalesced(now, remote, ecn, data); + } + + self.config.qlog_sink.emit_recovery_metrics( + self.pto_count, + &mut self.path, + now, + self.orig_rem_cid, + ); + + Ok(()) + } + + fn init_0rtt(&mut self) { + let (header, packet) = match self.crypto.early_crypto() { + Some(x) => x, + None => return, + }; + if self.side.is_client() { + match self.crypto.transport_parameters() { + Ok(params) => { + let params = params + .expect("crypto layer didn't supply transport parameters with ticket"); + // Certain values must not be cached + let params = TransportParameters { + initial_src_cid: None, + original_dst_cid: None, + preferred_address: None, + retry_src_cid: None, + stateless_reset_token: None, + min_ack_delay: None, + ack_delay_exponent: TransportParameters::default().ack_delay_exponent, + max_ack_delay: TransportParameters::default().max_ack_delay, + ..params + }; + self.set_peer_params(params); + } + Err(e) => { + error!("session ticket has malformed transport parameters: {}", e); + return; + } + } + } + trace!("0-RTT enabled"); + self.zero_rtt_enabled = true; + self.zero_rtt_crypto = Some(ZeroRttCrypto { header, packet }); + } + + fn read_crypto( + &mut self, + space: SpaceId, + crypto: &frame::Crypto, + payload_len: usize, + ) -> Result<(), TransportError> { + let expected = if !self.state.is_handshake() { + SpaceId::Data + } else if self.highest_space == SpaceId::Initial { + SpaceId::Initial + } else { + // On the server, self.highest_space can be Data after receiving the client's first + // flight, but we expect Handshake CRYPTO until the handshake is complete. + SpaceId::Handshake + }; + // We can't decrypt Handshake packets when highest_space is Initial, CRYPTO frames in 0-RTT + // packets are illegal, and we don't process 1-RTT packets until the handshake is + // complete. Therefore, we will never see CRYPTO data from a later-than-expected space. + debug_assert!(space <= expected, "received out-of-order CRYPTO data"); + + let end = crypto.offset + crypto.data.len() as u64; + if space < expected && end > self.spaces[space].crypto_stream.bytes_read() { + warn!( + "received new {:?} CRYPTO data when expecting {:?}", + space, expected + ); + return Err(TransportError::PROTOCOL_VIOLATION( + "new data at unexpected encryption level", + )); + } + + let space = &mut self.spaces[space]; + let max = end.saturating_sub(space.crypto_stream.bytes_read()); + if max > self.config.crypto_buffer_size as u64 { + return Err(TransportError::CRYPTO_BUFFER_EXCEEDED("")); + } + + space + .crypto_stream + .insert(crypto.offset, crypto.data.clone(), payload_len); + while let Some(chunk) = space.crypto_stream.read(usize::MAX, true) { + trace!("consumed {} CRYPTO bytes", chunk.bytes.len()); + if self.crypto.read_handshake(&chunk.bytes)? { + self.events.push_back(Event::HandshakeDataReady); + } + } + + Ok(()) + } + + fn write_crypto(&mut self) { + loop { + let space = self.highest_space; + let mut outgoing = Vec::new(); + if let Some(crypto) = self.crypto.write_handshake(&mut outgoing) { + match space { + SpaceId::Initial => { + self.upgrade_crypto(SpaceId::Handshake, crypto); + } + SpaceId::Handshake => { + self.upgrade_crypto(SpaceId::Data, crypto); + } + _ => unreachable!("got updated secrets during 1-RTT"), + } + } + if outgoing.is_empty() { + if space == self.highest_space { + break; + } else { + // Keys updated, check for more data to send + continue; + } + } + let offset = self.spaces[space].crypto_offset; + let outgoing = Bytes::from(outgoing); + if let State::Handshake(ref mut state) = self.state { + if space == SpaceId::Initial && offset == 0 && self.side.is_client() { + state.client_hello = Some(outgoing.clone()); + } + } + self.spaces[space].crypto_offset += outgoing.len() as u64; + trace!("wrote {} {:?} CRYPTO bytes", outgoing.len(), space); + self.spaces[space].pending.crypto.push_back(frame::Crypto { + offset, + data: outgoing, + }); + } + } + + /// Switch to stronger cryptography during handshake + fn upgrade_crypto(&mut self, space: SpaceId, crypto: Keys) { + debug_assert!( + self.spaces[space].crypto.is_none(), + "already reached packet space {space:?}" + ); + trace!("{:?} keys ready", space); + if space == SpaceId::Data { + // Precompute the first key update + self.next_crypto = Some( + self.crypto + .next_1rtt_keys() + .expect("handshake should be complete"), + ); + } + + self.spaces[space].crypto = Some(crypto); + debug_assert!(space as usize > self.highest_space as usize); + self.highest_space = space; + if space == SpaceId::Data && self.side.is_client() { + // Discard 0-RTT keys because 1-RTT keys are available. + self.zero_rtt_crypto = None; + } + } + + fn discard_space(&mut self, now: Instant, space_id: SpaceId) { + debug_assert!(space_id != SpaceId::Data); + trace!("discarding {:?} keys", space_id); + if space_id == SpaceId::Initial { + // No longer needed + if let ConnectionSide::Client { token, .. } = &mut self.side { + *token = Bytes::new(); + } + } + let space = &mut self.spaces[space_id]; + space.crypto = None; + space.time_of_last_ack_eliciting_packet = None; + space.loss_time = None; + let sent_packets = mem::take(&mut space.sent_packets); + for packet in sent_packets.into_values() { + self.remove_in_flight(&packet); + } + self.set_loss_detection_timer(now) + } + + fn handle_coalesced( + &mut self, + now: Instant, + remote: SocketAddr, + ecn: Option, + data: BytesMut, + ) { + self.path.total_recvd = self.path.total_recvd.saturating_add(data.len() as u64); + let mut remaining = Some(data); + while let Some(data) = remaining { + match PartialDecode::new( + data, + &FixedLengthConnectionIdParser::new(self.local_cid_state.cid_len()), + &[self.version], + self.endpoint_config.grease_quic_bit, + ) { + Ok((partial_decode, rest)) => { + remaining = rest; + self.handle_decode(now, remote, ecn, partial_decode); + } + Err(e) => { + trace!("malformed header: {}", e); + return; + } + } + } + } + + fn handle_decode( + &mut self, + now: Instant, + remote: SocketAddr, + ecn: Option, + partial_decode: PartialDecode, + ) { + if let Some(decoded) = packet_crypto::unprotect_header( + partial_decode, + &self.spaces, + self.zero_rtt_crypto.as_ref(), + self.peer_params.stateless_reset_token, + ) { + self.handle_packet(now, remote, ecn, decoded.packet, decoded.stateless_reset); + } + } + + fn handle_packet( + &mut self, + now: Instant, + remote: SocketAddr, + ecn: Option, + packet: Option, + stateless_reset: bool, + ) { + self.stats.udp_rx.ios += 1; + if let Some(ref packet) = packet { + trace!( + "got {:?} packet ({} bytes) from {} using id {}", + packet.header.space(), + packet.payload.len() + packet.header_data.len(), + remote, + packet.header.dst_cid(), + ); + } + + if self.is_handshaking() && remote != self.path.remote { + debug!("discarding packet with unexpected remote during handshake"); + return; + } + + let was_closed = self.state.is_closed(); + let was_drained = self.state.is_drained(); + + let decrypted = match packet { + None => Err(None), + Some(mut packet) => self + .decrypt_packet(now, &mut packet) + .map(move |number| (packet, number)), + }; + let result = match decrypted { + _ if stateless_reset => { + debug!("got stateless reset"); + Err(ConnectionError::Reset) + } + Err(Some(e)) => { + warn!("illegal packet: {}", e); + Err(e.into()) + } + Err(None) => { + debug!("failed to authenticate packet"); + self.authentication_failures += 1; + let integrity_limit = self.spaces[self.highest_space] + .crypto + .as_ref() + .unwrap() + .packet + .local + .integrity_limit(); + if self.authentication_failures > integrity_limit { + Err(TransportError::AEAD_LIMIT_REACHED("integrity limit violated").into()) + } else { + return; + } + } + Ok((packet, number)) => { + let span = match number { + Some(pn) => trace_span!("recv", space = ?packet.header.space(), pn), + None => trace_span!("recv", space = ?packet.header.space()), + }; + let _guard = span.enter(); + + let is_duplicate = |n| self.spaces[packet.header.space()].dedup.insert(n); + if number.is_some_and(is_duplicate) { + debug!("discarding possible duplicate packet"); + return; + } else if self.state.is_handshake() && packet.header.is_short() { + // TODO: SHOULD buffer these to improve reordering tolerance. + trace!("dropping short packet during handshake"); + return; + } else { + if let Header::Initial(InitialHeader { ref token, .. }) = packet.header { + if let State::Handshake(ref hs) = self.state { + if self.side.is_server() && token != &hs.expected_token { + // Clients must send the same retry token in every Initial. Initial + // packets can be spoofed, so we discard rather than killing the + // connection. + warn!("discarding Initial with invalid retry token"); + return; + } + } + } + + if !self.state.is_closed() { + let spin = match packet.header { + Header::Short { spin, .. } => spin, + _ => false, + }; + self.on_packet_authenticated( + now, + packet.header.space(), + ecn, + number, + spin, + packet.header.is_1rtt(), + ); + } + + self.process_decrypted_packet(now, remote, number, packet) + } + } + }; + + // State transitions for error cases + if let Err(conn_err) = result { + self.error = Some(conn_err.clone()); + self.state = match conn_err { + ConnectionError::ApplicationClosed(reason) => State::closed(reason), + ConnectionError::ConnectionClosed(reason) => State::closed(reason), + ConnectionError::Reset + | ConnectionError::TransportError(TransportError { + code: TransportErrorCode::AEAD_LIMIT_REACHED, + .. + }) => State::Drained, + ConnectionError::TimedOut => { + unreachable!("timeouts aren't generated by packet processing"); + } + ConnectionError::TransportError(err) => { + debug!("closing connection due to transport error: {}", err); + State::closed(err) + } + ConnectionError::VersionMismatch => State::Draining, + ConnectionError::LocallyClosed => { + unreachable!("LocallyClosed isn't generated by packet processing"); + } + ConnectionError::CidsExhausted => { + unreachable!("CidsExhausted isn't generated by packet processing"); + } + }; + } + + if !was_closed && self.state.is_closed() { + self.close_common(); + if !self.state.is_drained() { + self.set_close_timer(now); + } + } + if !was_drained && self.state.is_drained() { + self.endpoint_events.push_back(EndpointEventInner::Drained); + // Close timer may have been started previously, e.g. if we sent a close and got a + // stateless reset in response + self.timers.stop(Timer::Close); + } + + // Transmit CONNECTION_CLOSE if necessary + if let State::Closed(_) = self.state { + self.close = remote == self.path.remote; + } + } + + fn process_decrypted_packet( + &mut self, + now: Instant, + remote: SocketAddr, + number: Option, + packet: Packet, + ) -> Result<(), ConnectionError> { + let state = match self.state { + State::Established => { + match packet.header.space() { + SpaceId::Data => self.process_payload(now, remote, number.unwrap(), packet)?, + _ if packet.header.has_frames() => self.process_early_payload(now, packet)?, + _ => { + trace!("discarding unexpected pre-handshake packet"); + } + } + return Ok(()); + } + State::Closed(_) => { + for result in frame::Iter::new(packet.payload.freeze())? { + let frame = match result { + Ok(frame) => frame, + Err(err) => { + debug!("frame decoding error: {err:?}"); + continue; + } + }; + + if let Frame::Padding = frame { + continue; + }; + + self.stats.frame_rx.record(&frame); + + if let Frame::Close(_) = frame { + trace!("draining"); + self.state = State::Draining; + break; + } + } + return Ok(()); + } + State::Draining | State::Drained => return Ok(()), + State::Handshake(ref mut state) => state, + }; + + match packet.header { + Header::Retry { + src_cid: rem_cid, .. + } => { + if self.side.is_server() { + return Err(TransportError::PROTOCOL_VIOLATION("client sent Retry").into()); + } + + if self.total_authed_packets > 1 + || packet.payload.len() <= 16 // token + 16 byte tag + || !self.crypto.is_valid_retry( + &self.rem_cids.active(), + &packet.header_data, + &packet.payload, + ) + { + trace!("discarding invalid Retry"); + // - After the client has received and processed an Initial or Retry + // packet from the server, it MUST discard any subsequent Retry + // packets that it receives. + // - A client MUST discard a Retry packet with a zero-length Retry Token + // field. + // - Clients MUST discard Retry packets that have a Retry Integrity Tag + // that cannot be validated + return Ok(()); + } + + trace!("retrying with CID {}", rem_cid); + let client_hello = state.client_hello.take().unwrap(); + self.retry_src_cid = Some(rem_cid); + self.rem_cids.update_initial_cid(rem_cid); + self.rem_handshake_cid = rem_cid; + + let space = &mut self.spaces[SpaceId::Initial]; + if let Some(info) = space.take(0) { + self.on_packet_acked(now, info); + }; + + self.discard_space(now, SpaceId::Initial); // Make sure we clean up after any retransmitted Initials + self.spaces[SpaceId::Initial] = PacketSpace { + crypto: Some(self.crypto.initial_keys(&rem_cid, self.side.side())), + next_packet_number: self.spaces[SpaceId::Initial].next_packet_number, + crypto_offset: client_hello.len() as u64, + ..PacketSpace::new(now) + }; + self.spaces[SpaceId::Initial] + .pending + .crypto + .push_back(frame::Crypto { + offset: 0, + data: client_hello, + }); + + // Retransmit all 0-RTT data + let zero_rtt = mem::take(&mut self.spaces[SpaceId::Data].sent_packets); + for info in zero_rtt.into_values() { + self.remove_in_flight(&info); + self.spaces[SpaceId::Data].pending |= info.retransmits; + } + self.streams.retransmit_all_for_0rtt(); + + let token_len = packet.payload.len() - 16; + let ConnectionSide::Client { ref mut token, .. } = self.side else { + unreachable!("we already short-circuited if we're server"); + }; + *token = packet.payload.freeze().split_to(token_len); + self.state = State::Handshake(state::Handshake { + expected_token: Bytes::new(), + rem_cid_set: false, + client_hello: None, + }); + Ok(()) + } + Header::Long { + ty: LongType::Handshake, + src_cid: rem_cid, + .. + } => { + if rem_cid != self.rem_handshake_cid { + debug!( + "discarding packet with mismatched remote CID: {} != {}", + self.rem_handshake_cid, rem_cid + ); + return Ok(()); + } + self.on_path_validated(); + + self.process_early_payload(now, packet)?; + if self.state.is_closed() { + return Ok(()); + } + + if self.crypto.is_handshaking() { + trace!("handshake ongoing"); + return Ok(()); + } + + if self.side.is_client() { + // Client-only because server params were set from the client's Initial + let params = + self.crypto + .transport_parameters()? + .ok_or_else(|| TransportError { + code: TransportErrorCode::crypto(0x6d), + frame: None, + reason: "transport parameters missing".into(), + })?; + + if self.has_0rtt() { + if !self.crypto.early_data_accepted().unwrap() { + debug_assert!(self.side.is_client()); + debug!("0-RTT rejected"); + self.accepted_0rtt = false; + self.streams.zero_rtt_rejected(); + + // Discard already-queued frames + self.spaces[SpaceId::Data].pending = Retransmits::default(); + + // Discard 0-RTT packets + let sent_packets = + mem::take(&mut self.spaces[SpaceId::Data].sent_packets); + for packet in sent_packets.into_values() { + self.remove_in_flight(&packet); + } + } else { + self.accepted_0rtt = true; + params.validate_resumption_from(&self.peer_params)?; + } + } + if let Some(token) = params.stateless_reset_token { + self.endpoint_events + .push_back(EndpointEventInner::ResetToken(self.path.remote, token)); + } + self.handle_peer_params(params)?; + self.issue_first_cids(now); + } else { + // Server-only + self.spaces[SpaceId::Data].pending.handshake_done = true; + self.discard_space(now, SpaceId::Handshake); + } + + self.events.push_back(Event::Connected); + self.state = State::Established; + trace!("established"); + Ok(()) + } + Header::Initial(InitialHeader { + src_cid: rem_cid, .. + }) => { + if !state.rem_cid_set { + trace!("switching remote CID to {}", rem_cid); + let mut state = state.clone(); + self.rem_cids.update_initial_cid(rem_cid); + self.rem_handshake_cid = rem_cid; + self.orig_rem_cid = rem_cid; + state.rem_cid_set = true; + self.state = State::Handshake(state); + } else if rem_cid != self.rem_handshake_cid { + debug!( + "discarding packet with mismatched remote CID: {} != {}", + self.rem_handshake_cid, rem_cid + ); + return Ok(()); + } + + let starting_space = self.highest_space; + self.process_early_payload(now, packet)?; + + if self.side.is_server() + && starting_space == SpaceId::Initial + && self.highest_space != SpaceId::Initial + { + let params = + self.crypto + .transport_parameters()? + .ok_or_else(|| TransportError { + code: TransportErrorCode::crypto(0x6d), + frame: None, + reason: "transport parameters missing".into(), + })?; + self.handle_peer_params(params)?; + self.issue_first_cids(now); + self.init_0rtt(); + } + Ok(()) + } + Header::Long { + ty: LongType::ZeroRtt, + .. + } => { + self.process_payload(now, remote, number.unwrap(), packet)?; + Ok(()) + } + Header::VersionNegotiate { .. } => { + if self.total_authed_packets > 1 { + return Ok(()); + } + let supported = packet + .payload + .chunks(4) + .any(|x| match <[u8; 4]>::try_from(x) { + Ok(version) => self.version == u32::from_be_bytes(version), + Err(_) => false, + }); + if supported { + return Ok(()); + } + debug!("remote doesn't support our version"); + Err(ConnectionError::VersionMismatch) + } + Header::Short { .. } => unreachable!( + "short packets received during handshake are discarded in handle_packet" + ), + } + } + + /// Process an Initial or Handshake packet payload + fn process_early_payload( + &mut self, + now: Instant, + packet: Packet, + ) -> Result<(), TransportError> { + debug_assert_ne!(packet.header.space(), SpaceId::Data); + let payload_len = packet.payload.len(); + let mut ack_eliciting = false; + for result in frame::Iter::new(packet.payload.freeze())? { + let frame = result?; + let span = match frame { + Frame::Padding => continue, + _ => Some(trace_span!("frame", ty = %frame.ty())), + }; + + self.stats.frame_rx.record(&frame); + + let _guard = span.as_ref().map(|x| x.enter()); + ack_eliciting |= frame.is_ack_eliciting(); + + // Process frames + match frame { + Frame::Padding | Frame::Ping => {} + Frame::Crypto(frame) => { + self.read_crypto(packet.header.space(), &frame, payload_len)?; + } + Frame::Ack(ack) => { + self.on_ack_received(now, packet.header.space(), ack)?; + } + Frame::Close(reason) => { + self.error = Some(reason.into()); + self.state = State::Draining; + return Ok(()); + } + _ => { + let mut err = + TransportError::PROTOCOL_VIOLATION("illegal frame type in handshake"); + err.frame = Some(frame.ty()); + return Err(err); + } + } + } + + if ack_eliciting { + // In the initial and handshake spaces, ACKs must be sent immediately + self.spaces[packet.header.space()] + .pending_acks + .set_immediate_ack_required(); + } + + self.write_crypto(); + Ok(()) + } + + fn process_payload( + &mut self, + now: Instant, + remote: SocketAddr, + number: u64, + packet: Packet, + ) -> Result<(), TransportError> { + let payload = packet.payload.freeze(); + let mut is_probing_packet = true; + let mut close = None; + let payload_len = payload.len(); + let mut ack_eliciting = false; + for result in frame::Iter::new(payload)? { + let frame = result?; + let span = match frame { + Frame::Padding => continue, + _ => Some(trace_span!("frame", ty = %frame.ty())), + }; + + self.stats.frame_rx.record(&frame); + // Crypto, Stream and Datagram frames are special cased in order no pollute + // the log with payload data + match &frame { + Frame::Crypto(f) => { + trace!(offset = f.offset, len = f.data.len(), "got crypto frame"); + } + Frame::Stream(f) => { + trace!(id = %f.id, offset = f.offset, len = f.data.len(), fin = f.fin, "got stream frame"); + } + Frame::Datagram(f) => { + trace!(len = f.data.len(), "got datagram frame"); + } + f => { + trace!("got frame {:?}", f); + } + } + + let _guard = span.as_ref().map(|x| x.enter()); + if packet.header.is_0rtt() { + match frame { + Frame::Crypto(_) | Frame::Close(Close::Application(_)) => { + return Err(TransportError::PROTOCOL_VIOLATION( + "illegal frame type in 0-RTT", + )); + } + _ => {} + } + } + ack_eliciting |= frame.is_ack_eliciting(); + + // Check whether this could be a probing packet + match frame { + Frame::Padding + | Frame::PathChallenge(_) + | Frame::PathResponse(_) + | Frame::NewConnectionId(_) => {} + _ => { + is_probing_packet = false; + } + } + match frame { + Frame::Crypto(frame) => { + self.read_crypto(SpaceId::Data, &frame, payload_len)?; + } + Frame::Stream(frame) => { + if self.streams.received(frame, payload_len)?.should_transmit() { + self.spaces[SpaceId::Data].pending.max_data = true; + } + } + Frame::Ack(ack) => { + self.on_ack_received(now, SpaceId::Data, ack)?; + } + Frame::Padding | Frame::Ping => {} + Frame::Close(reason) => { + close = Some(reason); + } + Frame::PathChallenge(token) => { + self.path_responses.push(number, token, remote); + if remote == self.path.remote { + // PATH_CHALLENGE on active path, possible off-path packet forwarding + // attack. Send a non-probing packet to recover the active path. + match self.peer_supports_ack_frequency() { + true => self.immediate_ack(), + false => self.ping(), + } + } + } + Frame::PathResponse(token) => { + if self.path.challenge == Some(token) && remote == self.path.remote { + trace!("new path validated"); + self.timers.stop(Timer::PathValidation); + self.path.challenge = None; + self.path.validated = true; + if let Some((_, ref mut prev_path)) = self.prev_path { + prev_path.challenge = None; + prev_path.challenge_pending = false; + } + } else { + debug!(token, "ignoring invalid PATH_RESPONSE"); + } + } + Frame::MaxData(bytes) => { + self.streams.received_max_data(bytes); + } + Frame::MaxStreamData { id, offset } => { + self.streams.received_max_stream_data(id, offset)?; + } + Frame::MaxStreams { dir, count } => { + self.streams.received_max_streams(dir, count)?; + } + Frame::ResetStream(frame) => { + if self.streams.received_reset(frame)?.should_transmit() { + self.spaces[SpaceId::Data].pending.max_data = true; + } + } + Frame::DataBlocked { offset } => { + debug!(offset, "peer claims to be blocked at connection level"); + } + Frame::StreamDataBlocked { id, offset } => { + if id.initiator() == self.side.side() && id.dir() == Dir::Uni { + debug!("got STREAM_DATA_BLOCKED on send-only {}", id); + return Err(TransportError::STREAM_STATE_ERROR( + "STREAM_DATA_BLOCKED on send-only stream", + )); + } + debug!( + stream = %id, + offset, "peer claims to be blocked at stream level" + ); + } + Frame::StreamsBlocked { dir, limit } => { + if limit > MAX_STREAM_COUNT { + return Err(TransportError::FRAME_ENCODING_ERROR( + "unrepresentable stream limit", + )); + } + debug!( + "peer claims to be blocked opening more than {} {} streams", + limit, dir + ); + } + Frame::StopSending(frame::StopSending { id, error_code }) => { + if id.initiator() != self.side.side() { + if id.dir() == Dir::Uni { + debug!("got STOP_SENDING on recv-only {}", id); + return Err(TransportError::STREAM_STATE_ERROR( + "STOP_SENDING on recv-only stream", + )); + } + } else if self.streams.is_local_unopened(id) { + return Err(TransportError::STREAM_STATE_ERROR( + "STOP_SENDING on unopened stream", + )); + } + self.streams.received_stop_sending(id, error_code); + } + Frame::RetireConnectionId { sequence } => { + let allow_more_cids = self + .local_cid_state + .on_cid_retirement(sequence, self.peer_params.issue_cids_limit())?; + self.endpoint_events + .push_back(EndpointEventInner::RetireConnectionId( + now, + sequence, + allow_more_cids, + )); + } + Frame::NewConnectionId(frame) => { + trace!( + sequence = frame.sequence, + id = %frame.id, + retire_prior_to = frame.retire_prior_to, + ); + if self.rem_cids.active().is_empty() { + return Err(TransportError::PROTOCOL_VIOLATION( + "NEW_CONNECTION_ID when CIDs aren't in use", + )); + } + if frame.retire_prior_to > frame.sequence { + return Err(TransportError::PROTOCOL_VIOLATION( + "NEW_CONNECTION_ID retiring unissued CIDs", + )); + } + + use crate::cid_queue::InsertError; + match self.rem_cids.insert(frame) { + Ok(None) => {} + Ok(Some((retired, reset_token))) => { + let pending_retired = + &mut self.spaces[SpaceId::Data].pending.retire_cids; + /// Ensure `pending_retired` cannot grow without bound. Limit is + /// somewhat arbitrary but very permissive. + const MAX_PENDING_RETIRED_CIDS: u64 = CidQueue::LEN as u64 * 10; + // We don't bother counting in-flight frames because those are bounded + // by congestion control. + if (pending_retired.len() as u64) + .saturating_add(retired.end.saturating_sub(retired.start)) + > MAX_PENDING_RETIRED_CIDS + { + return Err(TransportError::CONNECTION_ID_LIMIT_ERROR( + "queued too many retired CIDs", + )); + } + pending_retired.extend(retired); + self.set_reset_token(reset_token); + } + Err(InsertError::ExceedsLimit) => { + return Err(TransportError::CONNECTION_ID_LIMIT_ERROR("")); + } + Err(InsertError::Retired) => { + trace!("discarding already-retired"); + // RETIRE_CONNECTION_ID might not have been previously sent if e.g. a + // range of connection IDs larger than the active connection ID limit + // was retired all at once via retire_prior_to. + self.spaces[SpaceId::Data] + .pending + .retire_cids + .push(frame.sequence); + continue; + } + }; + + if self.side.is_server() && self.rem_cids.active_seq() == 0 { + // We're a server still using the initial remote CID for the client, so + // let's switch immediately to enable clientside stateless resets. + self.update_rem_cid(); + } + } + Frame::NewToken(NewToken { token }) => { + let ConnectionSide::Client { + token_store, + server_name, + .. + } = &self.side + else { + return Err(TransportError::PROTOCOL_VIOLATION("client sent NEW_TOKEN")); + }; + if token.is_empty() { + return Err(TransportError::FRAME_ENCODING_ERROR("empty token")); + } + trace!("got new token"); + token_store.insert(server_name, token); + } + Frame::Datagram(datagram) => { + if self + .datagrams + .received(datagram, &self.config.datagram_receive_buffer_size)? + { + self.events.push_back(Event::DatagramReceived); + } + } + Frame::AckFrequency(ack_frequency) => { + // This frame can only be sent in the Data space + let space = &mut self.spaces[SpaceId::Data]; + + if !self + .ack_frequency + .ack_frequency_received(&ack_frequency, &mut space.pending_acks)? + { + // The AckFrequency frame is stale (we have already received a more recent one) + continue; + } + + // Our `max_ack_delay` has been updated, so we may need to adjust its associated + // timeout + if let Some(timeout) = space + .pending_acks + .max_ack_delay_timeout(self.ack_frequency.max_ack_delay) + { + self.timers.set(Timer::MaxAckDelay, timeout); + } + } + Frame::ImmediateAck => { + // This frame can only be sent in the Data space + self.spaces[SpaceId::Data] + .pending_acks + .set_immediate_ack_required(); + } + Frame::HandshakeDone => { + if self.side.is_server() { + return Err(TransportError::PROTOCOL_VIOLATION( + "client sent HANDSHAKE_DONE", + )); + } + if self.spaces[SpaceId::Handshake].crypto.is_some() { + self.discard_space(now, SpaceId::Handshake); + } + } + } + } + + let space = &mut self.spaces[SpaceId::Data]; + if space + .pending_acks + .packet_received(now, number, ack_eliciting, &space.dedup) + { + self.timers + .set(Timer::MaxAckDelay, now + self.ack_frequency.max_ack_delay); + } + + // Issue stream ID credit due to ACKs of outgoing finish/resets and incoming finish/resets + // on stopped streams. Incoming finishes/resets on open streams are not handled here as they + // are only freed, and hence only issue credit, once the application has been notified + // during a read on the stream. + let pending = &mut self.spaces[SpaceId::Data].pending; + self.streams.queue_max_stream_id(pending); + + if let Some(reason) = close { + self.error = Some(reason.into()); + self.state = State::Draining; + self.close = true; + } + + if remote != self.path.remote + && !is_probing_packet + && number == self.spaces[SpaceId::Data].rx_packet + { + let ConnectionSide::Server { ref server_config } = self.side else { + panic!("packets from unknown remote should be dropped by clients"); + }; + debug_assert!( + server_config.migration, + "migration-initiating packets should have been dropped immediately" + ); + self.migrate(now, remote); + // Break linkability, if possible + self.update_rem_cid(); + self.spin = false; + } + + Ok(()) + } + + fn migrate(&mut self, now: Instant, remote: SocketAddr) { + trace!(%remote, "migration initiated"); + self.path_counter = self.path_counter.wrapping_add(1); + // Reset rtt/congestion state for new path unless it looks like a NAT rebinding. + // Note that the congestion window will not grow until validation terminates. Helps mitigate + // amplification attacks performed by spoofing source addresses. + let mut new_path = if remote.is_ipv4() && remote.ip() == self.path.remote.ip() { + PathData::from_previous(remote, &self.path, self.path_counter, now) + } else { + let peer_max_udp_payload_size = + u16::try_from(self.peer_params.max_udp_payload_size.into_inner()) + .unwrap_or(u16::MAX); + PathData::new( + remote, + self.allow_mtud, + Some(peer_max_udp_payload_size), + self.path_counter, + now, + &self.config, + ) + }; + new_path.challenge = Some(self.rng.random()); + new_path.challenge_pending = true; + let prev_pto = self.pto(SpaceId::Data); + + let mut prev = mem::replace(&mut self.path, new_path); + // Don't clobber the original path if the previous one hasn't been validated yet + if prev.challenge.is_none() { + prev.challenge = Some(self.rng.random()); + prev.challenge_pending = true; + // We haven't updated the remote CID yet, this captures the remote CID we were using on + // the previous path. + self.prev_path = Some((self.rem_cids.active(), prev)); + } + + self.timers.set( + Timer::PathValidation, + now + 3 * cmp::max(self.pto(SpaceId::Data), prev_pto), + ); + } + + /// Handle a change in the local address, i.e. an active migration + pub fn local_address_changed(&mut self) { + self.update_rem_cid(); + self.ping(); + } + + /// Switch to a previously unused remote connection ID, if possible + fn update_rem_cid(&mut self) { + let (reset_token, retired) = match self.rem_cids.next() { + Some(x) => x, + None => return, + }; + + // Retire the current remote CID and any CIDs we had to skip. + self.spaces[SpaceId::Data] + .pending + .retire_cids + .extend(retired); + self.set_reset_token(reset_token); + } + + fn set_reset_token(&mut self, reset_token: ResetToken) { + self.endpoint_events + .push_back(EndpointEventInner::ResetToken( + self.path.remote, + reset_token, + )); + self.peer_params.stateless_reset_token = Some(reset_token); + } + + /// Issue an initial set of connection IDs to the peer upon connection + fn issue_first_cids(&mut self, now: Instant) { + if self.local_cid_state.cid_len() == 0 { + return; + } + + // Subtract 1 to account for the CID we supplied while handshaking + let mut n = self.peer_params.issue_cids_limit() - 1; + if let ConnectionSide::Server { server_config } = &self.side { + if server_config.has_preferred_address() { + // We also sent a CID in the transport parameters + n -= 1; + } + } + self.endpoint_events + .push_back(EndpointEventInner::NeedIdentifiers(now, n)); + } + + fn populate_packet( + &mut self, + now: Instant, + space_id: SpaceId, + buf: &mut Vec, + max_size: usize, + pn: u64, + ) -> SentFrames { + let mut sent = SentFrames::default(); + let space = &mut self.spaces[space_id]; + let is_0rtt = space_id == SpaceId::Data && space.crypto.is_none(); + space.pending_acks.maybe_ack_non_eliciting(); + + // HANDSHAKE_DONE + if !is_0rtt && mem::replace(&mut space.pending.handshake_done, false) { + buf.write(frame::FrameType::HANDSHAKE_DONE); + sent.retransmits.get_or_create().handshake_done = true; + // This is just a u8 counter and the frame is typically just sent once + self.stats.frame_tx.handshake_done = + self.stats.frame_tx.handshake_done.saturating_add(1); + } + + // PING + if mem::replace(&mut space.ping_pending, false) { + trace!("PING"); + buf.write(frame::FrameType::PING); + sent.non_retransmits = true; + self.stats.frame_tx.ping += 1; + } + + // IMMEDIATE_ACK + if mem::replace(&mut space.immediate_ack_pending, false) { + trace!("IMMEDIATE_ACK"); + buf.write(frame::FrameType::IMMEDIATE_ACK); + sent.non_retransmits = true; + self.stats.frame_tx.immediate_ack += 1; + } + + // ACK + if space.pending_acks.can_send() { + Self::populate_acks( + now, + self.receiving_ecn, + &mut sent, + space, + buf, + &mut self.stats, + ); + } + + // ACK_FREQUENCY + if mem::replace(&mut space.pending.ack_frequency, false) { + let sequence_number = self.ack_frequency.next_sequence_number(); + + // Safe to unwrap because this is always provided when ACK frequency is enabled + let config = self.config.ack_frequency_config.as_ref().unwrap(); + + // Ensure the delay is within bounds to avoid a PROTOCOL_VIOLATION error + let max_ack_delay = self.ack_frequency.candidate_max_ack_delay( + self.path.rtt.get(), + config, + &self.peer_params, + ); + + trace!(?max_ack_delay, "ACK_FREQUENCY"); + + frame::AckFrequency { + sequence: sequence_number, + ack_eliciting_threshold: config.ack_eliciting_threshold, + request_max_ack_delay: max_ack_delay.as_micros().try_into().unwrap_or(VarInt::MAX), + reordering_threshold: config.reordering_threshold, + } + .encode(buf); + + sent.retransmits.get_or_create().ack_frequency = true; + + self.ack_frequency.ack_frequency_sent(pn, max_ack_delay); + self.stats.frame_tx.ack_frequency += 1; + } + + // PATH_CHALLENGE + if buf.len() + 9 < max_size && space_id == SpaceId::Data { + // Transmit challenges with every outgoing frame on an unvalidated path + if let Some(token) = self.path.challenge { + // But only send a packet solely for that purpose at most once + self.path.challenge_pending = false; + sent.non_retransmits = true; + sent.requires_padding = true; + trace!("PATH_CHALLENGE {:08x}", token); + buf.write(frame::FrameType::PATH_CHALLENGE); + buf.write(token); + self.stats.frame_tx.path_challenge += 1; + } + } + + // PATH_RESPONSE + if buf.len() + 9 < max_size && space_id == SpaceId::Data { + if let Some(token) = self.path_responses.pop_on_path(self.path.remote) { + sent.non_retransmits = true; + sent.requires_padding = true; + trace!("PATH_RESPONSE {:08x}", token); + buf.write(frame::FrameType::PATH_RESPONSE); + buf.write(token); + self.stats.frame_tx.path_response += 1; + } + } + + // CRYPTO + while buf.len() + frame::Crypto::SIZE_BOUND < max_size && !is_0rtt { + let mut frame = match space.pending.crypto.pop_front() { + Some(x) => x, + None => break, + }; + + // Calculate the maximum amount of crypto data we can store in the buffer. + // Since the offset is known, we can reserve the exact size required to encode it. + // For length we reserve 2bytes which allows to encode up to 2^14, + // which is more than what fits into normally sized QUIC frames. + let max_crypto_data_size = max_size + - buf.len() + - 1 // Frame Type + - VarInt::size(unsafe { VarInt::from_u64_unchecked(frame.offset) }) + - 2; // Maximum encoded length for frame size, given we send less than 2^14 bytes + + let len = frame + .data + .len() + .min(2usize.pow(14) - 1) + .min(max_crypto_data_size); + + let data = frame.data.split_to(len); + let truncated = frame::Crypto { + offset: frame.offset, + data, + }; + trace!( + "CRYPTO: off {} len {}", + truncated.offset, + truncated.data.len() + ); + truncated.encode(buf); + self.stats.frame_tx.crypto += 1; + sent.retransmits.get_or_create().crypto.push_back(truncated); + if !frame.data.is_empty() { + frame.offset += len as u64; + space.pending.crypto.push_front(frame); + } + } + + if space_id == SpaceId::Data { + self.streams.write_control_frames( + buf, + &mut space.pending, + &mut sent.retransmits, + &mut self.stats.frame_tx, + max_size, + ); + } + + // NEW_CONNECTION_ID + while buf.len() + NewConnectionId::SIZE_BOUND < max_size { + let issued = match space.pending.new_cids.pop() { + Some(x) => x, + None => break, + }; + trace!( + sequence = issued.sequence, + id = %issued.id, + "NEW_CONNECTION_ID" + ); + frame::NewConnectionId { + sequence: issued.sequence, + retire_prior_to: self.local_cid_state.retire_prior_to(), + id: issued.id, + reset_token: issued.reset_token, + } + .encode(buf); + sent.retransmits.get_or_create().new_cids.push(issued); + self.stats.frame_tx.new_connection_id += 1; + } + + // RETIRE_CONNECTION_ID + while buf.len() + frame::RETIRE_CONNECTION_ID_SIZE_BOUND < max_size { + let seq = match space.pending.retire_cids.pop() { + Some(x) => x, + None => break, + }; + trace!(sequence = seq, "RETIRE_CONNECTION_ID"); + buf.write(frame::FrameType::RETIRE_CONNECTION_ID); + buf.write_var(seq); + sent.retransmits.get_or_create().retire_cids.push(seq); + self.stats.frame_tx.retire_connection_id += 1; + } + + // DATAGRAM + let mut sent_datagrams = false; + while buf.len() + Datagram::SIZE_BOUND < max_size && space_id == SpaceId::Data { + match self.datagrams.write(buf, max_size) { + true => { + sent_datagrams = true; + sent.non_retransmits = true; + self.stats.frame_tx.datagram += 1; + } + false => break, + } + } + if self.datagrams.send_blocked && sent_datagrams { + self.events.push_back(Event::DatagramsUnblocked); + self.datagrams.send_blocked = false; + } + + // NEW_TOKEN + while let Some(remote_addr) = space.pending.new_tokens.pop() { + debug_assert_eq!(space_id, SpaceId::Data); + let ConnectionSide::Server { server_config } = &self.side else { + panic!("NEW_TOKEN frames should not be enqueued by clients"); + }; + + if remote_addr != self.path.remote { + // NEW_TOKEN frames contain tokens bound to a client's IP address, and are only + // useful if used from the same IP address. Thus, we abandon enqueued NEW_TOKEN + // frames upon an path change. Instead, when the new path becomes validated, + // NEW_TOKEN frames may be enqueued for the new path instead. + continue; + } + + let token = Token::new( + TokenPayload::Validation { + ip: remote_addr.ip(), + issued: server_config.time_source.now(), + }, + &mut self.rng, + ); + let new_token = NewToken { + token: token.encode(&*server_config.token_key).into(), + }; + + if buf.len() + new_token.size() >= max_size { + space.pending.new_tokens.push(remote_addr); + break; + } + + new_token.encode(buf); + sent.retransmits + .get_or_create() + .new_tokens + .push(remote_addr); + self.stats.frame_tx.new_token += 1; + } + + // STREAM + if space_id == SpaceId::Data { + sent.stream_frames = + self.streams + .write_stream_frames(buf, max_size, self.config.send_fairness); + self.stats.frame_tx.stream += sent.stream_frames.len() as u64; + } + + sent + } + + /// Write pending ACKs into a buffer + /// + /// This method assumes ACKs are pending, and should only be called if + /// `!PendingAcks::ranges().is_empty()` returns `true`. + fn populate_acks( + now: Instant, + receiving_ecn: bool, + sent: &mut SentFrames, + space: &mut PacketSpace, + buf: &mut Vec, + stats: &mut ConnectionStats, + ) { + debug_assert!(!space.pending_acks.ranges().is_empty()); + + // 0-RTT packets must never carry acks (which would have to be of handshake packets) + debug_assert!(space.crypto.is_some(), "tried to send ACK in 0-RTT"); + let ecn = if receiving_ecn { + Some(&space.ecn_counters) + } else { + None + }; + sent.largest_acked = space.pending_acks.ranges().max(); + + let delay_micros = space.pending_acks.ack_delay(now).as_micros() as u64; + + // TODO: This should come from `TransportConfig` if that gets configurable. + let ack_delay_exp = TransportParameters::default().ack_delay_exponent; + let delay = delay_micros >> ack_delay_exp.into_inner(); + + trace!( + "ACK {:?}, Delay = {}us", + space.pending_acks.ranges(), + delay_micros + ); + + frame::Ack::encode(delay as _, space.pending_acks.ranges(), ecn, buf); + stats.frame_tx.acks += 1; + } + + fn close_common(&mut self) { + trace!("connection closed"); + for &timer in &Timer::VALUES { + self.timers.stop(timer); + } + } + + fn set_close_timer(&mut self, now: Instant) { + self.timers + .set(Timer::Close, now + 3 * self.pto(self.highest_space)); + } + + /// Handle transport parameters received from the peer + fn handle_peer_params(&mut self, params: TransportParameters) -> Result<(), TransportError> { + if Some(self.orig_rem_cid) != params.initial_src_cid + || (self.side.is_client() + && (Some(self.initial_dst_cid) != params.original_dst_cid + || self.retry_src_cid != params.retry_src_cid)) + { + return Err(TransportError::TRANSPORT_PARAMETER_ERROR( + "CID authentication failure", + )); + } + + self.set_peer_params(params); + + Ok(()) + } + + fn set_peer_params(&mut self, params: TransportParameters) { + self.streams.set_params(¶ms); + self.idle_timeout = + negotiate_max_idle_timeout(self.config.max_idle_timeout, Some(params.max_idle_timeout)); + trace!("negotiated max idle timeout {:?}", self.idle_timeout); + if let Some(ref info) = params.preferred_address { + self.rem_cids.insert(frame::NewConnectionId { + sequence: 1, + id: info.connection_id, + reset_token: info.stateless_reset_token, + retire_prior_to: 0, + }).expect("preferred address CID is the first received, and hence is guaranteed to be legal"); + } + self.ack_frequency.peer_max_ack_delay = get_max_ack_delay(¶ms); + self.peer_params = params; + self.path.mtud.on_peer_max_udp_payload_size_received( + u16::try_from(self.peer_params.max_udp_payload_size.into_inner()).unwrap_or(u16::MAX), + ); + } + + fn decrypt_packet( + &mut self, + now: Instant, + packet: &mut Packet, + ) -> Result, Option> { + let result = packet_crypto::decrypt_packet_body( + packet, + &self.spaces, + self.zero_rtt_crypto.as_ref(), + self.key_phase, + self.prev_crypto.as_ref(), + self.next_crypto.as_ref(), + )?; + + let result = match result { + Some(r) => r, + None => return Ok(None), + }; + + if result.outgoing_key_update_acked { + if let Some(prev) = self.prev_crypto.as_mut() { + prev.end_packet = Some((result.number, now)); + self.set_key_discard_timer(now, packet.header.space()); + } + } + + if result.incoming_key_update { + trace!("key update authenticated"); + self.update_keys(Some((result.number, now)), true); + self.set_key_discard_timer(now, packet.header.space()); + } + + Ok(Some(result.number)) + } + + fn update_keys(&mut self, end_packet: Option<(u64, Instant)>, remote: bool) { + trace!("executing key update"); + // Generate keys for the key phase after the one we're switching to, store them in + // `next_crypto`, make the contents of `next_crypto` current, and move the current keys into + // `prev_crypto`. + let new = self + .crypto + .next_1rtt_keys() + .expect("only called for `Data` packets"); + self.key_phase_size = new + .local + .confidentiality_limit() + .saturating_sub(KEY_UPDATE_MARGIN); + let old = mem::replace( + &mut self.spaces[SpaceId::Data] + .crypto + .as_mut() + .unwrap() // safe because update_keys() can only be triggered by short packets + .packet, + mem::replace(self.next_crypto.as_mut().unwrap(), new), + ); + self.spaces[SpaceId::Data].sent_with_keys = 0; + self.prev_crypto = Some(PrevCrypto { + crypto: old, + end_packet, + update_unacked: remote, + }); + self.key_phase = !self.key_phase; + } + + fn peer_supports_ack_frequency(&self) -> bool { + self.peer_params.min_ack_delay.is_some() + } + + /// Send an IMMEDIATE_ACK frame to the remote endpoint + /// + /// According to the spec, this will result in an error if the remote endpoint does not support + /// the Acknowledgement Frequency extension + pub(crate) fn immediate_ack(&mut self) { + self.spaces[self.highest_space].immediate_ack_pending = true; + } + + /// Decodes a packet, returning its decrypted payload, so it can be inspected in tests + #[cfg(test)] + pub(crate) fn decode_packet(&self, event: &ConnectionEvent) -> Option> { + let (first_decode, remaining) = match &event.0 { + ConnectionEventInner::Datagram(DatagramConnectionEvent { + first_decode, + remaining, + .. + }) => (first_decode, remaining), + _ => return None, + }; + + if remaining.is_some() { + panic!("Packets should never be coalesced in tests"); + } + + let decrypted_header = packet_crypto::unprotect_header( + first_decode.clone(), + &self.spaces, + self.zero_rtt_crypto.as_ref(), + self.peer_params.stateless_reset_token, + )?; + + let mut packet = decrypted_header.packet?; + packet_crypto::decrypt_packet_body( + &mut packet, + &self.spaces, + self.zero_rtt_crypto.as_ref(), + self.key_phase, + self.prev_crypto.as_ref(), + self.next_crypto.as_ref(), + ) + .ok()?; + + Some(packet.payload.to_vec()) + } + + /// The number of bytes of packets containing retransmittable frames that have not been + /// acknowledged or declared lost. + #[cfg(test)] + pub(crate) fn bytes_in_flight(&self) -> u64 { + self.path.in_flight.bytes + } + + /// Number of bytes worth of non-ack-only packets that may be sent + #[cfg(test)] + pub(crate) fn congestion_window(&self) -> u64 { + self.path + .congestion + .window() + .saturating_sub(self.path.in_flight.bytes) + } + + /// Whether no timers but keepalive, idle, rtt, pushnewcid, and key discard are running + #[cfg(test)] + pub(crate) fn is_idle(&self) -> bool { + Timer::VALUES + .iter() + .filter(|&&t| !matches!(t, Timer::KeepAlive | Timer::PushNewCid | Timer::KeyDiscard)) + .filter_map(|&t| Some((t, self.timers.get(t)?))) + .min_by_key(|&(_, time)| time) + .map_or(true, |(timer, _)| timer == Timer::Idle) + } + + /// Whether explicit congestion notification is in use on outgoing packets. + #[cfg(test)] + pub(crate) fn using_ecn(&self) -> bool { + self.path.sending_ecn + } + + /// The number of received bytes in the current path + #[cfg(test)] + pub(crate) fn total_recvd(&self) -> u64 { + self.path.total_recvd + } + + #[cfg(test)] + pub(crate) fn active_local_cid_seq(&self) -> (u64, u64) { + self.local_cid_state.active_seq() + } + + /// Instruct the peer to replace previously issued CIDs by sending a NEW_CONNECTION_ID frame + /// with updated `retire_prior_to` field set to `v` + #[cfg(test)] + pub(crate) fn rotate_local_cid(&mut self, v: u64, now: Instant) { + let n = self.local_cid_state.assign_retire_seq(v); + self.endpoint_events + .push_back(EndpointEventInner::NeedIdentifiers(now, n)); + } + + /// Check the current active remote CID sequence + #[cfg(test)] + pub(crate) fn active_rem_cid_seq(&self) -> u64 { + self.rem_cids.active_seq() + } + + /// Returns the detected maximum udp payload size for the current path + #[cfg(test)] + pub(crate) fn path_mtu(&self) -> u16 { + self.path.current_mtu() + } + + /// Whether we have 1-RTT data to send + /// + /// See also `self.space(SpaceId::Data).can_send()` + fn can_send_1rtt(&self, max_size: usize) -> bool { + self.streams.can_send_stream_data() + || self.path.challenge_pending + || self + .prev_path + .as_ref() + .is_some_and(|(_, x)| x.challenge_pending) + || !self.path_responses.is_empty() + || self + .datagrams + .outgoing + .front() + .is_some_and(|x| x.size(true) <= max_size) + } + + /// Update counters to account for a packet becoming acknowledged, lost, or abandoned + fn remove_in_flight(&mut self, packet: &SentPacket) { + // Visit known paths from newest to oldest to find the one `packet` was sent on + for path in [&mut self.path] + .into_iter() + .chain(self.prev_path.as_mut().map(|(_, data)| data)) + { + if path.remove_in_flight(packet) { + return; + } + } + } + + /// Terminate the connection instantly, without sending a close packet + fn kill(&mut self, reason: ConnectionError) { + self.close_common(); + self.error = Some(reason); + self.state = State::Drained; + self.endpoint_events.push_back(EndpointEventInner::Drained); + } + + /// Storage size required for the largest packet known to be supported by the current path + /// + /// Buffers passed to [`Connection::poll_transmit`] should be at least this large. + pub fn current_mtu(&self) -> u16 { + self.path.current_mtu() + } + + /// Size of non-frame data for a 1-RTT packet + /// + /// Quantifies space consumed by the QUIC header and AEAD tag. All other bytes in a packet are + /// frames. Changes if the length of the remote connection ID changes, which is expected to be + /// rare. If `pn` is specified, may additionally change unpredictably due to variations in + /// latency and packet loss. + fn predict_1rtt_overhead(&self, pn: Option) -> usize { + let pn_len = match pn { + Some(pn) => PacketNumber::new( + pn, + self.spaces[SpaceId::Data].largest_acked_packet.unwrap_or(0), + ) + .len(), + // Upper bound + None => 4, + }; + + // 1 byte for flags + 1 + self.rem_cids.active().len() + pn_len + self.tag_len_1rtt() + } + + fn tag_len_1rtt(&self) -> usize { + let key = match self.spaces[SpaceId::Data].crypto.as_ref() { + Some(crypto) => Some(&*crypto.packet.local), + None => self.zero_rtt_crypto.as_ref().map(|x| &*x.packet), + }; + // If neither Data nor 0-RTT keys are available, make a reasonable tag length guess. As of + // this writing, all QUIC cipher suites use 16-byte tags. We could return `None` instead, + // but that would needlessly prevent sending datagrams during 0-RTT. + key.map_or(16, |x| x.tag_len()) + } + + /// Mark the path as validated, and enqueue NEW_TOKEN frames to be sent as appropriate + fn on_path_validated(&mut self) { + self.path.validated = true; + let ConnectionSide::Server { server_config } = &self.side else { + return; + }; + let new_tokens = &mut self.spaces[SpaceId::Data as usize].pending.new_tokens; + new_tokens.clear(); + for _ in 0..server_config.validation_token.sent { + new_tokens.push(self.path.remote); + } + } +} + +impl fmt::Debug for Connection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Connection") + .field("handshake_cid", &self.handshake_cid) + .finish() + } +} + +/// Fields of `Connection` specific to it being client-side or server-side +enum ConnectionSide { + Client { + /// Sent in every outgoing Initial packet. Always empty after Initial keys are discarded + token: Bytes, + token_store: Arc, + server_name: String, + }, + Server { + server_config: Arc, + }, +} + +impl ConnectionSide { + fn remote_may_migrate(&self) -> bool { + match self { + Self::Server { server_config } => server_config.migration, + Self::Client { .. } => false, + } + } + + fn is_client(&self) -> bool { + self.side().is_client() + } + + fn is_server(&self) -> bool { + self.side().is_server() + } + + fn side(&self) -> Side { + match *self { + Self::Client { .. } => Side::Client, + Self::Server { .. } => Side::Server, + } + } +} + +impl From for ConnectionSide { + fn from(side: SideArgs) -> Self { + match side { + SideArgs::Client { + token_store, + server_name, + } => Self::Client { + token: token_store.take(&server_name).unwrap_or_default(), + token_store, + server_name, + }, + SideArgs::Server { + server_config, + pref_addr_cid: _, + path_validated: _, + } => Self::Server { server_config }, + } + } +} + +/// Parameters to `Connection::new` specific to it being client-side or server-side +pub(crate) enum SideArgs { + Client { + token_store: Arc, + server_name: String, + }, + Server { + server_config: Arc, + pref_addr_cid: Option, + path_validated: bool, + }, +} + +impl SideArgs { + pub(crate) fn pref_addr_cid(&self) -> Option { + match *self { + Self::Client { .. } => None, + Self::Server { pref_addr_cid, .. } => pref_addr_cid, + } + } + + pub(crate) fn path_validated(&self) -> bool { + match *self { + Self::Client { .. } => true, + Self::Server { path_validated, .. } => path_validated, + } + } + + pub(crate) fn side(&self) -> Side { + match *self { + Self::Client { .. } => Side::Client, + Self::Server { .. } => Side::Server, + } + } +} + +/// Reasons why a connection might be lost +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ConnectionError { + /// The peer doesn't implement any supported version + #[error("peer doesn't implement any supported version")] + VersionMismatch, + /// The peer violated the QUIC specification as understood by this implementation + #[error(transparent)] + TransportError(#[from] TransportError), + /// The peer's QUIC stack aborted the connection automatically + #[error("aborted by peer: {0}")] + ConnectionClosed(frame::ConnectionClose), + /// The peer closed the connection + #[error("closed by peer: {0}")] + ApplicationClosed(frame::ApplicationClose), + /// The peer is unable to continue processing this connection, usually due to having restarted + #[error("reset by peer")] + Reset, + /// Communication with the peer has lapsed for longer than the negotiated idle timeout + /// + /// If neither side is sending keep-alives, a connection will time out after a long enough idle + /// period even if the peer is still reachable. See also [`TransportConfig::max_idle_timeout()`] + /// and [`TransportConfig::keep_alive_interval()`]. + #[error("timed out")] + TimedOut, + /// The local application closed the connection + #[error("closed")] + LocallyClosed, + /// The connection could not be created because not enough of the CID space is available + /// + /// Try using longer connection IDs. + #[error("CIDs exhausted")] + CidsExhausted, +} + +impl From for ConnectionError { + fn from(x: Close) -> Self { + match x { + Close::Connection(reason) => Self::ConnectionClosed(reason), + Close::Application(reason) => Self::ApplicationClosed(reason), + } + } +} + +// For compatibility with API consumers +impl From for io::Error { + fn from(x: ConnectionError) -> Self { + use ConnectionError::*; + let kind = match x { + TimedOut => io::ErrorKind::TimedOut, + Reset => io::ErrorKind::ConnectionReset, + ApplicationClosed(_) | ConnectionClosed(_) => io::ErrorKind::ConnectionAborted, + TransportError(_) | VersionMismatch | LocallyClosed | CidsExhausted => { + io::ErrorKind::Other + } + }; + Self::new(kind, x) + } +} + +#[allow(unreachable_pub)] // fuzzing only +#[derive(Clone)] +pub enum State { + Handshake(state::Handshake), + Established, + Closed(state::Closed), + Draining, + /// Waiting for application to call close so we can dispose of the resources + Drained, +} + +impl State { + fn closed>(reason: R) -> Self { + Self::Closed(state::Closed { + reason: reason.into(), + }) + } + + fn is_handshake(&self) -> bool { + matches!(*self, Self::Handshake(_)) + } + + fn is_established(&self) -> bool { + matches!(*self, Self::Established) + } + + fn is_closed(&self) -> bool { + matches!(*self, Self::Closed(_) | Self::Draining | Self::Drained) + } + + fn is_drained(&self) -> bool { + matches!(*self, Self::Drained) + } +} + +mod state { + use super::*; + + #[allow(unreachable_pub)] // fuzzing only + #[derive(Clone)] + pub struct Handshake { + /// Whether the remote CID has been set by the peer yet + /// + /// Always set for servers + pub(super) rem_cid_set: bool, + /// Stateless retry token received in the first Initial by a server. + /// + /// Must be present in every Initial. Always empty for clients. + pub(super) expected_token: Bytes, + /// First cryptographic message + /// + /// Only set for clients + pub(super) client_hello: Option, + } + + #[allow(unreachable_pub)] // fuzzing only + #[derive(Clone)] + pub struct Closed { + pub(super) reason: Close, + } +} + +/// Events of interest to the application +#[derive(Debug)] +pub enum Event { + /// The connection's handshake data is ready + HandshakeDataReady, + /// The connection was successfully established + Connected, + /// The connection was lost + /// + /// Emitted if the peer closes the connection or an error is encountered. + ConnectionLost { + /// Reason that the connection was closed + reason: ConnectionError, + }, + /// Stream events + Stream(StreamEvent), + /// One or more application datagrams have been received + DatagramReceived, + /// One or more application datagrams have been sent after blocking + DatagramsUnblocked, +} + +fn instant_saturating_sub(x: Instant, y: Instant) -> Duration { + if x > y { x - y } else { Duration::ZERO } +} + +fn get_max_ack_delay(params: &TransportParameters) -> Duration { + Duration::from_micros(params.max_ack_delay.0 * 1000) +} + +// Prevents overflow and improves behavior in extreme circumstances +const MAX_BACKOFF_EXPONENT: u32 = 16; + +/// Minimal remaining size to allow packet coalescing, excluding cryptographic tag +/// +/// This must be at least as large as the header for a well-formed empty packet to be coalesced, +/// plus some space for frames. We only care about handshake headers because short header packets +/// necessarily have smaller headers, and initial packets are only ever the first packet in a +/// datagram (because we coalesce in ascending packet space order and the only reason to split a +/// packet is when packet space changes). +const MIN_PACKET_SPACE: usize = MAX_HANDSHAKE_OR_0RTT_HEADER_SIZE + 32; + +/// Largest amount of space that could be occupied by a Handshake or 0-RTT packet's header +/// +/// Excludes packet-type-specific fields such as packet number or Initial token +// https://www.rfc-editor.org/rfc/rfc9000.html#name-0-rtt: flags + version + dcid len + dcid + +// scid len + scid + length + pn +const MAX_HANDSHAKE_OR_0RTT_HEADER_SIZE: usize = + 1 + 4 + 1 + MAX_CID_SIZE + 1 + MAX_CID_SIZE + VarInt::from_u32(u16::MAX as u32).size() + 4; + +/// Perform key updates this many packets before the AEAD confidentiality limit. +/// +/// Chosen arbitrarily, intended to be large enough to prevent spurious connection loss. +const KEY_UPDATE_MARGIN: u64 = 10_000; + +#[derive(Default)] +struct SentFrames { + retransmits: ThinRetransmits, + largest_acked: Option, + stream_frames: StreamMetaVec, + /// Whether the packet contains non-retransmittable frames (like datagrams) + non_retransmits: bool, + requires_padding: bool, +} + +impl SentFrames { + /// Returns whether the packet contains only ACKs + fn is_ack_only(&self, streams: &StreamsState) -> bool { + self.largest_acked.is_some() + && !self.non_retransmits + && self.stream_frames.is_empty() + && self.retransmits.is_empty(streams) + } +} + +/// Compute the negotiated idle timeout based on local and remote max_idle_timeout transport parameters. +/// +/// According to the definition of max_idle_timeout, a value of `0` means the timeout is disabled; see +/// +/// According to the negotiation procedure, either the minimum of the timeouts or one specified is used as the negotiated value; see +/// +/// Returns the negotiated idle timeout as a `Duration`, or `None` when both endpoints have opted out of idle timeout. +fn negotiate_max_idle_timeout(x: Option, y: Option) -> Option { + match (x, y) { + (Some(VarInt(0)) | None, Some(VarInt(0)) | None) => None, + (Some(VarInt(0)) | None, Some(y)) => Some(Duration::from_millis(y.0)), + (Some(x), Some(VarInt(0)) | None) => Some(Duration::from_millis(x.0)), + (Some(x), Some(y)) => Some(Duration::from_millis(cmp::min(x, y).0)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn negotiate_max_idle_timeout_commutative() { + let test_params = [ + (None, None, None), + (None, Some(VarInt(0)), None), + (None, Some(VarInt(2)), Some(Duration::from_millis(2))), + (Some(VarInt(0)), Some(VarInt(0)), None), + ( + Some(VarInt(2)), + Some(VarInt(0)), + Some(Duration::from_millis(2)), + ), + ( + Some(VarInt(1)), + Some(VarInt(4)), + Some(Duration::from_millis(1)), + ), + ]; + + for (left, right, result) in test_params { + assert_eq!(negotiate_max_idle_timeout(left, right), result); + assert_eq!(negotiate_max_idle_timeout(right, left), result); + } + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/mtud.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/mtud.rs new file mode 100644 index 0000000..b690731 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/mtud.rs @@ -0,0 +1,970 @@ +use crate::{Instant, MAX_UDP_PAYLOAD, MtuDiscoveryConfig, packet::SpaceId}; +use std::cmp; +use tracing::trace; + +/// Implements Datagram Packetization Layer Path Maximum Transmission Unit Discovery +/// +/// See [`MtuDiscoveryConfig`] for details +#[derive(Clone)] +pub(crate) struct MtuDiscovery { + /// Detected MTU for the path + current_mtu: u16, + /// The state of the MTU discovery, if enabled + state: Option, + /// The state of the black hole detector + black_hole_detector: BlackHoleDetector, +} + +impl MtuDiscovery { + pub(crate) fn new( + initial_plpmtu: u16, + min_mtu: u16, + peer_max_udp_payload_size: Option, + config: MtuDiscoveryConfig, + ) -> Self { + debug_assert!( + initial_plpmtu >= min_mtu, + "initial_max_udp_payload_size must be at least {min_mtu}" + ); + + let mut mtud = Self::with_state( + initial_plpmtu, + min_mtu, + Some(EnabledMtuDiscovery::new(config)), + ); + + // We might be migrating an existing connection to a new path, in which case the transport + // parameters have already been transmitted, and we already know the value of + // `peer_max_udp_payload_size` + if let Some(peer_max_udp_payload_size) = peer_max_udp_payload_size { + mtud.on_peer_max_udp_payload_size_received(peer_max_udp_payload_size); + } + + mtud + } + + /// MTU discovery will be disabled and the current MTU will be fixed to the provided value + pub(crate) fn disabled(plpmtu: u16, min_mtu: u16) -> Self { + Self::with_state(plpmtu, min_mtu, None) + } + + fn with_state(current_mtu: u16, min_mtu: u16, state: Option) -> Self { + Self { + current_mtu, + state, + black_hole_detector: BlackHoleDetector::new(min_mtu), + } + } + + pub(super) fn reset(&mut self, current_mtu: u16, min_mtu: u16) { + self.current_mtu = current_mtu; + if let Some(state) = self.state.take() { + self.state = Some(EnabledMtuDiscovery::new(state.config)); + self.on_peer_max_udp_payload_size_received(state.peer_max_udp_payload_size); + } + self.black_hole_detector = BlackHoleDetector::new(min_mtu); + } + + /// Returns the current MTU + pub(crate) fn current_mtu(&self) -> u16 { + self.current_mtu + } + + /// Returns the amount of bytes that should be sent as an MTU probe, if any + pub(crate) fn poll_transmit(&mut self, now: Instant, next_pn: u64) -> Option { + self.state + .as_mut() + .and_then(|state| state.poll_transmit(now, self.current_mtu, next_pn)) + } + + /// Notifies the [`MtuDiscovery`] that the peer's `max_udp_payload_size` transport parameter has + /// been received + pub(crate) fn on_peer_max_udp_payload_size_received(&mut self, peer_max_udp_payload_size: u16) { + self.current_mtu = self.current_mtu.min(peer_max_udp_payload_size); + + if let Some(state) = self.state.as_mut() { + // MTUD is only active after the connection has been fully established, so it is + // guaranteed we will receive the peer's transport parameters before we start probing + debug_assert!(matches!(state.phase, Phase::Initial)); + state.peer_max_udp_payload_size = peer_max_udp_payload_size; + } + } + + /// Notifies the [`MtuDiscovery`] that a packet has been ACKed + /// + /// Returns true if the packet was an MTU probe + pub(crate) fn on_acked(&mut self, space: SpaceId, pn: u64, len: u16) -> bool { + // MTU probes are only sent in application data space + if space != SpaceId::Data { + return false; + } + + // Update the state of the MTU search + if let Some(new_mtu) = self + .state + .as_mut() + .and_then(|state| state.on_probe_acked(pn)) + { + self.current_mtu = new_mtu; + trace!(current_mtu = self.current_mtu, "new MTU detected"); + + self.black_hole_detector.on_probe_acked(pn, len); + true + } else { + self.black_hole_detector.on_non_probe_acked(pn, len); + false + } + } + + /// Returns the packet number of the in-flight MTU probe, if any + pub(crate) fn in_flight_mtu_probe(&self) -> Option { + match &self.state { + Some(EnabledMtuDiscovery { + phase: Phase::Searching(search_state), + .. + }) => search_state.in_flight_probe, + _ => None, + } + } + + /// Notifies the [`MtuDiscovery`] that the in-flight MTU probe was lost + pub(crate) fn on_probe_lost(&mut self) { + if let Some(state) = &mut self.state { + state.on_probe_lost(); + } + } + + /// Notifies the [`MtuDiscovery`] that a non-probe packet was lost + /// + /// When done notifying of lost packets, [`MtuDiscovery::black_hole_detected`] must be called, to + /// ensure the last loss burst is properly processed and to trigger black hole recovery logic if + /// necessary. + pub(crate) fn on_non_probe_lost(&mut self, pn: u64, len: u16) { + self.black_hole_detector.on_non_probe_lost(pn, len); + } + + /// Returns true if a black hole was detected + /// + /// Calling this function will close the previous loss burst. If a black hole is detected, the + /// current MTU will be reset to `min_mtu`. + pub(crate) fn black_hole_detected(&mut self, now: Instant) -> bool { + if !self.black_hole_detector.black_hole_detected() { + return false; + } + + self.current_mtu = self.black_hole_detector.min_mtu; + + if let Some(state) = &mut self.state { + state.on_black_hole_detected(now); + } + + true + } +} + +/// Additional state for enabled MTU discovery +#[derive(Debug, Clone)] +struct EnabledMtuDiscovery { + phase: Phase, + peer_max_udp_payload_size: u16, + config: MtuDiscoveryConfig, +} + +impl EnabledMtuDiscovery { + fn new(config: MtuDiscoveryConfig) -> Self { + Self { + phase: Phase::Initial, + peer_max_udp_payload_size: MAX_UDP_PAYLOAD, + config, + } + } + + /// Returns the amount of bytes that should be sent as an MTU probe, if any + fn poll_transmit(&mut self, now: Instant, current_mtu: u16, next_pn: u64) -> Option { + if let Phase::Initial = &self.phase { + // Start the first search + self.phase = Phase::Searching(SearchState::new( + current_mtu, + self.peer_max_udp_payload_size, + &self.config, + )); + } else if let Phase::Complete(next_mtud_activation) = &self.phase { + if now < *next_mtud_activation { + return None; + } + + // Start a new search (we have reached the next activation time) + self.phase = Phase::Searching(SearchState::new( + current_mtu, + self.peer_max_udp_payload_size, + &self.config, + )); + } + + if let Phase::Searching(state) = &mut self.phase { + // Nothing to do while there is a probe in flight + if state.in_flight_probe.is_some() { + return None; + } + + // Retransmit lost probes, if any + if 0 < state.lost_probe_count && state.lost_probe_count < MAX_PROBE_RETRANSMITS { + state.in_flight_probe = Some(next_pn); + return Some(state.last_probed_mtu); + } + + let last_probe_succeeded = state.lost_probe_count == 0; + + // The probe is definitely lost (we reached the MAX_PROBE_RETRANSMITS threshold) + if !last_probe_succeeded { + state.lost_probe_count = 0; + state.in_flight_probe = None; + } + + if let Some(probe_udp_payload_size) = state.next_mtu_to_probe(last_probe_succeeded) { + state.in_flight_probe = Some(next_pn); + state.last_probed_mtu = probe_udp_payload_size; + return Some(probe_udp_payload_size); + } else { + let next_mtud_activation = now + self.config.interval; + self.phase = Phase::Complete(next_mtud_activation); + return None; + } + } + + None + } + + /// Called when a packet is acknowledged in [`SpaceId::Data`] + /// + /// Returns the new `current_mtu` if the packet number corresponds to the in-flight MTU probe + fn on_probe_acked(&mut self, pn: u64) -> Option { + match &mut self.phase { + Phase::Searching(state) if state.in_flight_probe == Some(pn) => { + state.in_flight_probe = None; + state.lost_probe_count = 0; + Some(state.last_probed_mtu) + } + _ => None, + } + } + + /// Called when the in-flight MTU probe was lost + fn on_probe_lost(&mut self) { + // We might no longer be searching, e.g. if a black hole was detected + if let Phase::Searching(state) = &mut self.phase { + state.in_flight_probe = None; + state.lost_probe_count += 1; + } + } + + /// Called when a black hole is detected + fn on_black_hole_detected(&mut self, now: Instant) { + // Stop searching, if applicable, and reset the timer + let next_mtud_activation = now + self.config.black_hole_cooldown; + self.phase = Phase::Complete(next_mtud_activation); + } +} + +#[derive(Debug, Clone, Copy)] +enum Phase { + /// We haven't started polling yet + Initial, + /// We are currently searching for a higher PMTU + Searching(SearchState), + /// Searching has completed and will be triggered again at the provided instant + Complete(Instant), +} + +#[derive(Debug, Clone, Copy)] +struct SearchState { + /// The lower bound for the current binary search + lower_bound: u16, + /// The upper bound for the current binary search + upper_bound: u16, + /// The minimum change to stop the current binary search + minimum_change: u16, + /// The UDP payload size we last sent a probe for + last_probed_mtu: u16, + /// Packet number of an in-flight probe (if any) + in_flight_probe: Option, + /// Lost probes at the current probe size + lost_probe_count: usize, +} + +impl SearchState { + /// Creates a new search state, with the specified lower bound (the upper bound is derived from + /// the config and the peer's `max_udp_payload_size` transport parameter) + fn new( + mut lower_bound: u16, + peer_max_udp_payload_size: u16, + config: &MtuDiscoveryConfig, + ) -> Self { + lower_bound = lower_bound.min(peer_max_udp_payload_size); + let upper_bound = config + .upper_bound + .clamp(lower_bound, peer_max_udp_payload_size); + + Self { + in_flight_probe: None, + lost_probe_count: 0, + lower_bound, + upper_bound, + minimum_change: config.minimum_change, + // During initialization, we consider the lower bound to have already been + // successfully probed + last_probed_mtu: lower_bound, + } + } + + /// Determines the next MTU to probe using binary search + fn next_mtu_to_probe(&mut self, last_probe_succeeded: bool) -> Option { + debug_assert_eq!(self.in_flight_probe, None); + + if last_probe_succeeded { + self.lower_bound = self.last_probed_mtu; + } else { + self.upper_bound = self.last_probed_mtu - 1; + } + + let next_mtu = (self.lower_bound as i32 + self.upper_bound as i32) / 2; + + // Binary search stopping condition + if ((next_mtu - self.last_probed_mtu as i32).unsigned_abs() as u16) < self.minimum_change { + // Special case: if the upper bound is far enough, we want to probe it as a last + // step (otherwise we will never achieve the upper bound) + if self.upper_bound.saturating_sub(self.last_probed_mtu) >= self.minimum_change { + return Some(self.upper_bound); + } + + return None; + } + + Some(next_mtu as u16) + } +} + +/// Judges whether packet loss might indicate a drop in MTU +/// +/// Our MTU black hole detection scheme is a heuristic based on the order in which packets were sent +/// (the packet number order), their sizes, and which are deemed lost. +/// +/// First, contiguous groups of lost packets ("loss bursts") are aggregated, because a group of +/// packets all lost together were probably lost for the same reason. +/// +/// A loss burst is deemed "suspicious" if it contains no packets that are (a) smaller than the +/// minimum MTU or (b) smaller than a more recent acknowledged packet, because such a burst could be +/// fully explained by a reduction in MTU. +/// +/// When the number of suspicious loss bursts exceeds [`BLACK_HOLE_THRESHOLD`], we judge the +/// evidence for an MTU black hole to be sufficient. +#[derive(Clone)] +struct BlackHoleDetector { + /// Packet loss bursts currently considered suspicious + suspicious_loss_bursts: Vec, + /// Loss burst currently being aggregated, if any + current_loss_burst: Option, + /// Packet number of the biggest packet larger than `min_mtu` which we've received + /// acknowledgment of more recently than any suspicious loss burst, if any + largest_post_loss_packet: u64, + /// The maximum of `min_mtu` and the size of `largest_post_loss_packet`, or exactly `min_mtu` if + /// no larger packets have been received since the most recent loss burst. + acked_mtu: u16, + /// The UDP payload size guaranteed to be supported by the network + min_mtu: u16, +} + +impl BlackHoleDetector { + fn new(min_mtu: u16) -> Self { + Self { + suspicious_loss_bursts: Vec::with_capacity(BLACK_HOLE_THRESHOLD + 1), + current_loss_burst: None, + largest_post_loss_packet: 0, + acked_mtu: min_mtu, + min_mtu, + } + } + + fn on_probe_acked(&mut self, pn: u64, len: u16) { + // MTU probes are always larger than the previous MTU, so no previous loss bursts are + // suspicious. At most one MTU probe is in flight at a time, so we don't need to worry about + // reordering between them. + self.suspicious_loss_bursts.clear(); + self.acked_mtu = len; + // This might go backwards, but that's okay: a successful ACK means we haven't yet judged a + // more recently sent packet lost, and we just want to track the largest packet that's been + // successfully delivered more recently than a loss. + self.largest_post_loss_packet = pn; + } + + fn on_non_probe_acked(&mut self, pn: u64, len: u16) { + if len <= self.acked_mtu { + // We've already seen a larger packet since the most recent suspicious loss burst; + // nothing to do. + return; + } + self.acked_mtu = len; + // This might go backwards, but that's okay as described in `on_probe_acked`. + self.largest_post_loss_packet = pn; + // Loss bursts packets smaller than this are retroactively deemed non-suspicious. + self.suspicious_loss_bursts + .retain(|burst| burst.smallest_packet_size > len); + } + + fn on_non_probe_lost(&mut self, pn: u64, len: u16) { + // A loss burst is a group of consecutive packets that are declared lost, so a distance + // greater than 1 indicates a new burst + let end_last_burst = self + .current_loss_burst + .as_ref() + .is_some_and(|current| pn - current.latest_non_probe != 1); + + if end_last_burst { + self.finish_loss_burst(); + } + + self.current_loss_burst = Some(CurrentLossBurst { + latest_non_probe: pn, + smallest_packet_size: self + .current_loss_burst + .map_or(len, |prev| cmp::min(prev.smallest_packet_size, len)), + }); + } + + fn black_hole_detected(&mut self) -> bool { + self.finish_loss_burst(); + + if self.suspicious_loss_bursts.len() <= BLACK_HOLE_THRESHOLD { + return false; + } + + self.suspicious_loss_bursts.clear(); + + true + } + + /// Marks the end of the current loss burst, checking whether it was suspicious + fn finish_loss_burst(&mut self) { + let Some(burst) = self.current_loss_burst.take() else { + return; + }; + // If a loss burst contains a packet smaller than the minimum MTU or a more recently + // transmitted packet, it is not suspicious. + if burst.smallest_packet_size < self.min_mtu + || (burst.latest_non_probe < self.largest_post_loss_packet + && burst.smallest_packet_size < self.acked_mtu) + { + return; + } + // The loss burst is now deemed suspicious. + + // A suspicious loss burst more recent than `largest_post_loss_packet` invalidates it. This + // makes `acked_mtu` a conservative approximation. Ideally we'd update `safe_mtu` and + // `largest_post_loss_packet` to describe the largest acknowledged packet sent later than + // this burst, but that would require tracking the size of an unpredictable number of + // recently acknowledged packets, and erring on the side of false positives is safe. + if burst.latest_non_probe > self.largest_post_loss_packet { + self.acked_mtu = self.min_mtu; + } + + let burst = LossBurst { + smallest_packet_size: burst.smallest_packet_size, + }; + + if self.suspicious_loss_bursts.len() <= BLACK_HOLE_THRESHOLD { + self.suspicious_loss_bursts.push(burst); + return; + } + + // To limit memory use, only track the most suspicious loss bursts. + let smallest = self + .suspicious_loss_bursts + .iter_mut() + .min_by_key(|prev| prev.smallest_packet_size) + .filter(|prev| prev.smallest_packet_size < burst.smallest_packet_size); + if let Some(smallest) = smallest { + *smallest = burst; + } + } + + #[cfg(test)] + fn suspicious_loss_burst_count(&self) -> usize { + self.suspicious_loss_bursts.len() + } + + #[cfg(test)] + fn largest_non_probe_lost(&self) -> Option { + self.current_loss_burst.as_ref().map(|x| x.latest_non_probe) + } +} + +#[derive(Copy, Clone)] +struct LossBurst { + smallest_packet_size: u16, +} + +#[derive(Copy, Clone)] +struct CurrentLossBurst { + smallest_packet_size: u16, + latest_non_probe: u64, +} + +// Corresponds to the RFC's `MAX_PROBES` constant (see +// https://www.rfc-editor.org/rfc/rfc8899#section-5.1.2) +const MAX_PROBE_RETRANSMITS: usize = 3; +/// Maximum number of suspicious loss bursts that will not trigger black hole detection +const BLACK_HOLE_THRESHOLD: usize = 3; + +#[cfg(test)] +mod tests { + use super::*; + use crate::Duration; + use crate::MAX_UDP_PAYLOAD; + use crate::packet::SpaceId; + use assert_matches::assert_matches; + + fn default_mtud() -> MtuDiscovery { + let config = MtuDiscoveryConfig::default(); + MtuDiscovery::new(1_200, 1_200, None, config) + } + + fn completed(mtud: &MtuDiscovery) -> bool { + matches!(mtud.state.as_ref().unwrap().phase, Phase::Complete(_)) + } + + /// Drives mtud until it reaches `Phase::Completed` + fn drive_to_completion( + mtud: &mut MtuDiscovery, + now: Instant, + link_payload_size_limit: u16, + ) -> Vec { + let mut probed_sizes = Vec::new(); + for probe_pn in 1..100 { + let result = mtud.poll_transmit(now, probe_pn); + + if completed(mtud) { + break; + } + + // "Send" next probe + assert!(result.is_some()); + let probe_size = result.unwrap(); + probed_sizes.push(probe_size); + + if probe_size <= link_payload_size_limit { + mtud.on_acked(SpaceId::Data, probe_pn, probe_size); + } else { + mtud.on_probe_lost(); + } + } + probed_sizes + } + + #[test] + fn black_hole_detector_ignores_burst_containing_non_suspicious_packet() { + let mut mtud = default_mtud(); + mtud.on_non_probe_lost(2, 1300); + mtud.on_non_probe_lost(3, 1300); + assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), Some(3)); + assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0); + + mtud.on_non_probe_lost(4, 800); + assert!(!mtud.black_hole_detected(Instant::now())); + assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), None); + assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0); + } + + #[test] + fn black_hole_detector_counts_burst_containing_only_suspicious_packets() { + let mut mtud = default_mtud(); + mtud.on_non_probe_lost(2, 1300); + mtud.on_non_probe_lost(3, 1300); + assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), Some(3)); + assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0); + + assert!(!mtud.black_hole_detected(Instant::now())); + assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), None); + assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 1); + } + + #[test] + fn black_hole_detector_ignores_empty_burst() { + let mut mtud = default_mtud(); + assert!(!mtud.black_hole_detected(Instant::now())); + assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0); + } + + #[test] + fn mtu_discovery_disabled_does_nothing() { + let mut mtud = MtuDiscovery::disabled(1_200, 1_200); + let probe_size = mtud.poll_transmit(Instant::now(), 0); + assert_eq!(probe_size, None); + } + + #[test] + fn mtu_discovery_disabled_lost_four_packet_bursts_triggers_black_hole_detection() { + let mut mtud = MtuDiscovery::disabled(1_400, 1_250); + let now = Instant::now(); + + for i in 0..4 { + // The packets are never contiguous, so each one has its own burst + mtud.on_non_probe_lost(i * 2, 1300); + } + + assert!(mtud.black_hole_detected(now)); + assert_eq!(mtud.current_mtu, 1250); + assert_matches!(mtud.state, None); + } + + #[test] + fn mtu_discovery_lost_two_packet_bursts_does_not_trigger_black_hole_detection() { + let mut mtud = default_mtud(); + let now = Instant::now(); + + for i in 0..2 { + mtud.on_non_probe_lost(i, 1300); + assert!(!mtud.black_hole_detected(now)); + } + } + + #[test] + fn mtu_discovery_lost_four_packet_bursts_triggers_black_hole_detection_and_resets_timer() { + let mut mtud = default_mtud(); + let now = Instant::now(); + + for i in 0..4 { + // The packets are never contiguous, so each one has its own burst + mtud.on_non_probe_lost(i * 2, 1300); + } + + assert!(mtud.black_hole_detected(now)); + assert_eq!(mtud.current_mtu, 1200); + if let Phase::Complete(next_mtud_activation) = mtud.state.unwrap().phase { + assert_eq!(next_mtud_activation, now + Duration::from_secs(60)); + } else { + panic!("Unexpected MTUD phase!"); + } + } + + #[test] + fn mtu_discovery_after_complete_reactivates_when_interval_elapsed() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(9_000); + let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config); + let now = Instant::now(); + drive_to_completion(&mut mtud, now, 1_500); + + // Polling right after completion does not cause new packets to be sent + assert_eq!(mtud.poll_transmit(now, 42), None); + assert!(completed(&mtud)); + assert_eq!(mtud.current_mtu, 1_471); + + // Polling after the interval has passed does (taking the current mtu as lower bound) + assert_eq!( + mtud.poll_transmit(now + Duration::from_secs(600), 43), + Some(5235) + ); + + match mtud.state.unwrap().phase { + Phase::Searching(state) => { + assert_eq!(state.lower_bound, 1_471); + assert_eq!(state.upper_bound, 9_000); + } + _ => { + panic!("Unexpected MTUD phase!") + } + } + } + + #[test] + fn mtu_discovery_lost_three_probes_lowers_probe_size() { + let mut mtud = default_mtud(); + + let mut probe_sizes = (0..4).map(|i| { + let probe_size = mtud.poll_transmit(Instant::now(), i); + assert!(probe_size.is_some(), "no probe returned for packet {i}"); + + mtud.on_probe_lost(); + probe_size.unwrap() + }); + + // After the first probe is lost, it gets retransmitted twice + let first_probe_size = probe_sizes.next().unwrap(); + for _ in 0..2 { + assert_eq!(probe_sizes.next().unwrap(), first_probe_size) + } + + // After the third probe is lost, we decrement our probe size + let fourth_probe_size = probe_sizes.next().unwrap(); + assert!(fourth_probe_size < first_probe_size); + assert_eq!( + fourth_probe_size, + first_probe_size - (first_probe_size - 1_200) / 2 - 1 + ); + } + + #[test] + fn mtu_discovery_with_peer_max_udp_payload_size_clamps_upper_bound() { + let mut mtud = default_mtud(); + + mtud.on_peer_max_udp_payload_size_received(1300); + let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500); + + assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1300); + assert_eq!(mtud.current_mtu, 1300); + let expected_probed_sizes = &[1250, 1275, 1300]; + assert_eq!(probed_sizes, expected_probed_sizes); + assert!(completed(&mtud)); + } + + #[test] + fn mtu_discovery_with_previous_peer_max_udp_payload_size_clamps_upper_bound() { + let mut mtud = MtuDiscovery::new(1500, 1_200, Some(1400), MtuDiscoveryConfig::default()); + + assert_eq!(mtud.current_mtu, 1400); + assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1400); + + let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500); + + assert_eq!(mtud.current_mtu, 1400); + assert!(probed_sizes.is_empty()); + assert!(completed(&mtud)); + } + + #[cfg(debug_assertions)] + #[test] + #[should_panic] + fn mtu_discovery_with_peer_max_udp_payload_size_after_search_panics() { + let mut mtud = default_mtud(); + drive_to_completion(&mut mtud, Instant::now(), 1500); + mtud.on_peer_max_udp_payload_size_received(1300); + } + + #[test] + fn mtu_discovery_with_1500_limit() { + let mut mtud = default_mtud(); + + let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500); + + let expected_probed_sizes = &[1326, 1389, 1420, 1452]; + assert_eq!(probed_sizes, expected_probed_sizes); + assert_eq!(mtud.current_mtu, 1452); + assert!(completed(&mtud)); + } + + #[test] + fn mtu_discovery_with_1500_limit_and_10000_upper_bound() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(10_000); + let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config); + + let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500); + + let expected_probed_sizes = &[ + 5600, 5600, 5600, 3399, 3399, 3399, 2299, 2299, 2299, 1749, 1749, 1749, 1474, 1611, + 1611, 1611, 1542, 1542, 1542, 1507, 1507, 1507, + ]; + assert_eq!(probed_sizes, expected_probed_sizes); + assert_eq!(mtud.current_mtu, 1474); + assert!(completed(&mtud)); + } + + #[test] + fn mtu_discovery_no_lost_probes_finds_maximum_udp_payload() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(MAX_UDP_PAYLOAD); + let mut mtud = MtuDiscovery::new(1200, 1200, None, config); + + drive_to_completion(&mut mtud, Instant::now(), u16::MAX); + + assert_eq!(mtud.current_mtu, 65527); + assert!(completed(&mtud)); + } + + #[test] + fn mtu_discovery_lost_half_of_probes_finds_maximum_udp_payload() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(MAX_UDP_PAYLOAD); + let mut mtud = MtuDiscovery::new(1200, 1200, None, config); + + let now = Instant::now(); + let mut iterations = 0; + for i in 1..100 { + iterations += 1; + + let probe_pn = i * 2 - 1; + let other_pn = i * 2; + + let result = mtud.poll_transmit(Instant::now(), probe_pn); + + if completed(&mtud) { + break; + } + + // "Send" next probe + assert!(result.is_some()); + assert!(mtud.in_flight_mtu_probe().is_some()); + + // Nothing else to send while the probe is in-flight + assert_matches!(mtud.poll_transmit(now, other_pn), None); + + if i % 2 == 0 { + // ACK probe and ensure it results in an increase of current_mtu + let previous_max_size = mtud.current_mtu; + mtud.on_acked(SpaceId::Data, probe_pn, result.unwrap()); + println!( + "ACK packet {}. Previous MTU = {previous_max_size}. New MTU = {}", + result.unwrap(), + mtud.current_mtu + ); + // assert!(mtud.current_mtu > previous_max_size); + } else { + mtud.on_probe_lost(); + } + } + + assert_eq!(iterations, 25); + assert_eq!(mtud.current_mtu, 65527); + assert!(completed(&mtud)); + } + + #[test] + fn search_state_lower_bound_higher_than_upper_bound_clamps_upper_bound() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(1400); + + let state = SearchState::new(1500, u16::MAX, &config); + assert_eq!(state.lower_bound, 1500); + assert_eq!(state.upper_bound, 1500); + } + + #[test] + fn search_state_lower_bound_higher_than_peer_max_udp_payload_size_clamps_lower_bound() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(9000); + + let state = SearchState::new(1500, 1300, &config); + assert_eq!(state.lower_bound, 1300); + assert_eq!(state.upper_bound, 1300); + } + + #[test] + fn search_state_upper_bound_higher_than_peer_max_udp_payload_size_clamps_upper_bound() { + let mut config = MtuDiscoveryConfig::default(); + config.upper_bound(9000); + + let state = SearchState::new(1200, 1450, &config); + assert_eq!(state.lower_bound, 1200); + assert_eq!(state.upper_bound, 1450); + } + + // Loss of packets larger than have been acknowledged should indicate a black hole + #[test] + fn simple_black_hole_detection() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1300); + for i in 0..BLACK_HOLE_THRESHOLD { + bhd.on_non_probe_lost(i as u64 * 2, 1400); + } + // But not before `BLACK_HOLE_THRESHOLD + 1` bursts + assert!(!bhd.black_hole_detected()); + bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 2, 1400); + assert!(bhd.black_hole_detected()); + } + + // Loss of packets followed in transmission order by confirmation of a larger packet should not + // indicate a black hole + #[test] + fn non_suspicious_bursts() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1500); + for i in 0..(BLACK_HOLE_THRESHOLD + 1) { + bhd.on_non_probe_lost(i as u64 * 2, 1400); + } + assert!(!bhd.black_hole_detected()); + } + + // Loss of packets smaller than have been acknowledged previously should still indicate a black + // hole + #[test] + fn dynamic_mtu_reduction() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked(0, 1500); + for i in 0..(BLACK_HOLE_THRESHOLD + 1) { + bhd.on_non_probe_lost(i as u64 * 2, 1400); + } + assert!(bhd.black_hole_detected()); + } + + // Bursts containing heterogeneous packets are judged based on the smallest + #[test] + fn mixed_non_suspicious_bursts() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 3, 1400); + for i in 0..(BLACK_HOLE_THRESHOLD + 1) { + bhd.on_non_probe_lost(i as u64 * 3, 1500); + bhd.on_non_probe_lost(i as u64 * 3 + 1, 1300); + } + assert!(!bhd.black_hole_detected()); + } + + // Multi-packet bursts are only counted once + #[test] + fn bursts_count_once() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 3, 1400); + for i in 0..(BLACK_HOLE_THRESHOLD) { + bhd.on_non_probe_lost(i as u64 * 3, 1500); + bhd.on_non_probe_lost(i as u64 * 3 + 1, 1500); + } + assert!(!bhd.black_hole_detected()); + bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 3, 1500); + assert!(bhd.black_hole_detected()); + } + + // Non-suspicious bursts don't interfere with detection of suspicious bursts + #[test] + fn interleaved_bursts() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 4, 1400); + for i in 0..(BLACK_HOLE_THRESHOLD + 1) { + bhd.on_non_probe_lost(i as u64 * 4, 1500); + bhd.on_non_probe_lost(i as u64 * 4 + 2, 1300); + } + assert!(bhd.black_hole_detected()); + } + + // Bursts that are non-suspicious before a delivered packet become suspicious past it + #[test] + fn suspicious_after_acked() { + let mut bhd = BlackHoleDetector::new(1200); + bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1400); + for i in 0..(BLACK_HOLE_THRESHOLD + 1) { + bhd.on_non_probe_lost(i as u64 * 2, 1300); + } + assert!( + !bhd.black_hole_detected(), + "1300 byte losses preceding a 1400 byte delivery are not suspicious" + ); + for i in 0..(BLACK_HOLE_THRESHOLD + 1) { + bhd.on_non_probe_lost((BLACK_HOLE_THRESHOLD as u64 + 1 + i as u64) * 2, 1300); + } + assert!( + bhd.black_hole_detected(), + "1300 byte losses following a 1400 byte delivery are suspicious" + ); + } + + // Acknowledgment of a packet marks prior loss bursts with the same packet size as + // non-suspicious + #[test] + fn retroactively_non_suspicious() { + let mut bhd = BlackHoleDetector::new(1200); + for i in 0..BLACK_HOLE_THRESHOLD { + bhd.on_non_probe_lost(i as u64 * 2, 1400); + } + bhd.on_non_probe_acked(BLACK_HOLE_THRESHOLD as u64 * 2, 1400); + bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 2 + 1, 1400); + assert!(!bhd.black_hole_detected()); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/pacing.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/pacing.rs new file mode 100644 index 0000000..2e46994 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/pacing.rs @@ -0,0 +1,308 @@ +//! Pacing of packet transmissions. + +use crate::{Duration, Instant}; + +use tracing::warn; + +/// A simple token-bucket pacer +/// +/// The pacer's capacity is derived on a fraction of the congestion window +/// which can be sent in regular intervals +/// Once the bucket is empty, further transmission is blocked. +/// The bucket refills at a rate slightly faster +/// than one congestion window per RTT, as recommended in +/// +pub(super) struct Pacer { + capacity: u64, + last_window: u64, + last_mtu: u16, + tokens: u64, + prev: Instant, +} + +impl Pacer { + /// Obtains a new [`Pacer`]. + pub(super) fn new(smoothed_rtt: Duration, window: u64, mtu: u16, now: Instant) -> Self { + let capacity = optimal_capacity(smoothed_rtt, window, mtu); + Self { + capacity, + last_window: window, + last_mtu: mtu, + tokens: capacity, + prev: now, + } + } + + /// Record that a packet has been transmitted. + pub(super) fn on_transmit(&mut self, packet_length: u16) { + self.tokens = self.tokens.saturating_sub(packet_length.into()) + } + + /// Return how long we need to wait before sending `bytes_to_send` + /// + /// If we can send a packet right away, this returns `None`. Otherwise, returns `Some(d)`, + /// where `d` is the time before this function should be called again. + /// + /// The 5/4 ratio used here comes from the suggestion that N = 1.25 in the draft IETF RFC for + /// QUIC. + pub(super) fn delay( + &mut self, + smoothed_rtt: Duration, + bytes_to_send: u64, + mtu: u16, + window: u64, + now: Instant, + ) -> Option { + debug_assert_ne!( + window, 0, + "zero-sized congestion control window is nonsense" + ); + + if window != self.last_window || mtu != self.last_mtu { + self.capacity = optimal_capacity(smoothed_rtt, window, mtu); + + // Clamp the tokens + self.tokens = self.capacity.min(self.tokens); + self.last_window = window; + self.last_mtu = mtu; + } + + // if we can already send a packet, there is no need for delay + if self.tokens >= bytes_to_send { + return None; + } + + // we disable pacing for extremely large windows + if window > u64::from(u32::MAX) { + return None; + } + + let window = window as u32; + + let time_elapsed = now.checked_duration_since(self.prev).unwrap_or_else(|| { + warn!("received a timestamp early than a previous recorded time, ignoring"); + Default::default() + }); + + if smoothed_rtt.as_nanos() == 0 { + return None; + } + + let elapsed_rtts = time_elapsed.as_secs_f64() / smoothed_rtt.as_secs_f64(); + let new_tokens = window as f64 * 1.25 * elapsed_rtts; + self.tokens = self + .tokens + .saturating_add(new_tokens as _) + .min(self.capacity); + + self.prev = now; + + // if we can already send a packet, there is no need for delay + if self.tokens >= bytes_to_send { + return None; + } + + let unscaled_delay = smoothed_rtt + .checked_mul((bytes_to_send.max(self.capacity) - self.tokens) as _) + .unwrap_or(Duration::MAX) + / window; + + // divisions come before multiplications to prevent overflow + // this is the time at which the pacing window becomes empty + Some(self.prev + (unscaled_delay / 5) * 4) + } +} + +/// Calculates a pacer capacity for a certain window and RTT +/// +/// The goal is to emit a burst (of size `capacity`) in timer intervals +/// which compromise between +/// - ideally distributing datagrams over time +/// - constantly waking up the connection to produce additional datagrams +/// +/// Too short burst intervals means we will never meet them since the timer +/// accuracy in user-space is not high enough. If we miss the interval by more +/// than 25%, we will lose that part of the congestion window since no additional +/// tokens for the extra-elapsed time can be stored. +/// +/// Too long burst intervals make pacing less effective. +fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 { + let rtt = smoothed_rtt.as_nanos().max(1); + + let capacity = ((window as u128 * BURST_INTERVAL_NANOS) / rtt) as u64; + + // Small bursts are less efficient (no GSO), could increase latency and don't effectively + // use the channel's buffer capacity. Large bursts might block the connection on sending. + capacity.clamp(MIN_BURST_SIZE * mtu as u64, MAX_BURST_SIZE * mtu as u64) +} + +/// The burst interval +/// +/// The capacity will we refilled in 4/5 of that time. +/// 2ms is chosen here since framework timers might have 1ms precision. +/// If kernel-level pacing is supported later a higher time here might be +/// more applicable. +const BURST_INTERVAL_NANOS: u128 = 2_000_000; // 2ms + +/// Allows some usage of GSO, and doesn't slow down the handshake. +const MIN_BURST_SIZE: u64 = 10; + +/// Creating 256 packets took 1ms in a benchmark, so larger bursts don't make sense. +const MAX_BURST_SIZE: u64 = 256; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn does_not_panic_on_bad_instant() { + let old_instant = Instant::now(); + let new_instant = old_instant + Duration::from_micros(15); + let rtt = Duration::from_micros(400); + + assert!( + Pacer::new(rtt, 30000, 1500, new_instant) + .delay(Duration::from_micros(0), 0, 1500, 1, old_instant) + .is_none() + ); + assert!( + Pacer::new(rtt, 30000, 1500, new_instant) + .delay(Duration::from_micros(0), 1600, 1500, 1, old_instant) + .is_none() + ); + assert!( + Pacer::new(rtt, 30000, 1500, new_instant) + .delay(Duration::from_micros(0), 1500, 1500, 3000, old_instant) + .is_none() + ); + } + + #[test] + fn derives_initial_capacity() { + let window = 2_000_000; + let mtu = 1500; + let rtt = Duration::from_millis(50); + let now = Instant::now(); + + let pacer = Pacer::new(rtt, window, mtu, now); + assert_eq!( + pacer.capacity, + (window as u128 * BURST_INTERVAL_NANOS / rtt.as_nanos()) as u64 + ); + assert_eq!(pacer.tokens, pacer.capacity); + + let pacer = Pacer::new(Duration::from_millis(0), window, mtu, now); + assert_eq!(pacer.capacity, MAX_BURST_SIZE * mtu as u64); + assert_eq!(pacer.tokens, pacer.capacity); + + let pacer = Pacer::new(rtt, 1, mtu, now); + assert_eq!(pacer.capacity, MIN_BURST_SIZE * mtu as u64); + assert_eq!(pacer.tokens, pacer.capacity); + } + + #[test] + fn adjusts_capacity() { + let window = 2_000_000; + let mtu = 1500; + let rtt = Duration::from_millis(50); + let now = Instant::now(); + + let mut pacer = Pacer::new(rtt, window, mtu, now); + assert_eq!( + pacer.capacity, + (window as u128 * BURST_INTERVAL_NANOS / rtt.as_nanos()) as u64 + ); + assert_eq!(pacer.tokens, pacer.capacity); + let initial_tokens = pacer.tokens; + + pacer.delay(rtt, mtu as u64, mtu, window * 2, now); + assert_eq!( + pacer.capacity, + (2 * window as u128 * BURST_INTERVAL_NANOS / rtt.as_nanos()) as u64 + ); + assert_eq!(pacer.tokens, initial_tokens); + + pacer.delay(rtt, mtu as u64, mtu, window / 2, now); + assert_eq!( + pacer.capacity, + (window as u128 / 2 * BURST_INTERVAL_NANOS / rtt.as_nanos()) as u64 + ); + assert_eq!(pacer.tokens, initial_tokens / 2); + + pacer.delay(rtt, mtu as u64, mtu * 2, window, now); + assert_eq!( + pacer.capacity, + (window as u128 * BURST_INTERVAL_NANOS / rtt.as_nanos()) as u64 + ); + + pacer.delay(rtt, mtu as u64, 20_000, window, now); + assert_eq!(pacer.capacity, 20_000_u64 * MIN_BURST_SIZE); + } + + #[test] + fn computes_pause_correctly() { + let window = 2_000_000u64; + let mtu = 1000; + let rtt = Duration::from_millis(50); + let old_instant = Instant::now(); + + let mut pacer = Pacer::new(rtt, window, mtu, old_instant); + let packet_capacity = pacer.capacity / mtu as u64; + + for _ in 0..packet_capacity { + assert_eq!( + pacer.delay(rtt, mtu as u64, mtu, window, old_instant), + None, + "When capacity is available packets should be sent immediately" + ); + + pacer.on_transmit(mtu); + } + + let pace_duration = Duration::from_nanos((BURST_INTERVAL_NANOS * 4 / 5) as u64); + + assert_eq!( + pacer + .delay(rtt, mtu as u64, mtu, window, old_instant) + .expect("Send must be delayed") + .duration_since(old_instant), + pace_duration + ); + + // Refill half of the tokens + assert_eq!( + pacer.delay( + rtt, + mtu as u64, + mtu, + window, + old_instant + pace_duration / 2 + ), + None + ); + assert_eq!(pacer.tokens, pacer.capacity / 2); + + for _ in 0..packet_capacity / 2 { + assert_eq!( + pacer.delay(rtt, mtu as u64, mtu, window, old_instant), + None, + "When capacity is available packets should be sent immediately" + ); + + pacer.on_transmit(mtu); + } + + // Refill all capacity by waiting more than the expected duration + assert_eq!( + pacer.delay( + rtt, + mtu as u64, + mtu, + window, + old_instant + pace_duration * 3 / 2 + ), + None + ); + assert_eq!(pacer.tokens, pacer.capacity); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/packet_builder.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/packet_builder.rs new file mode 100644 index 0000000..cc60177 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/packet_builder.rs @@ -0,0 +1,282 @@ +use bytes::Bytes; +use rand::Rng; +use tracing::{debug, trace, trace_span}; + +use super::{Connection, SentFrames, spaces::SentPacket}; +use crate::{ + ConnectionId, Instant, TransportError, TransportErrorCode, + connection::ConnectionSide, + frame::{self, Close}, + packet::{FIXED_BIT, Header, InitialHeader, LongType, PacketNumber, PartialEncode, SpaceId}, +}; + +pub(super) struct PacketBuilder { + pub(super) datagram_start: usize, + pub(super) space: SpaceId, + pub(super) partial_encode: PartialEncode, + pub(super) ack_eliciting: bool, + pub(super) exact_number: u64, + pub(super) short_header: bool, + /// Smallest absolute position in the associated buffer that must be occupied by this packet's + /// frames + pub(super) min_size: usize, + /// Largest absolute position in the associated buffer that may be occupied by this packet's + /// frames + pub(super) max_size: usize, + pub(super) tag_len: usize, + pub(super) _span: tracing::span::EnteredSpan, +} + +impl PacketBuilder { + /// Write a new packet header to `buffer` and determine the packet's properties + /// + /// Marks the connection drained and returns `None` if the confidentiality limit would be + /// violated. + pub(super) fn new( + now: Instant, + space_id: SpaceId, + dst_cid: ConnectionId, + buffer: &mut Vec, + buffer_capacity: usize, + datagram_start: usize, + ack_eliciting: bool, + conn: &mut Connection, + ) -> Option { + let version = conn.version; + // Initiate key update if we're approaching the confidentiality limit + let sent_with_keys = conn.spaces[space_id].sent_with_keys; + if space_id == SpaceId::Data { + if sent_with_keys >= conn.key_phase_size { + debug!("routine key update due to phase exhaustion"); + conn.force_key_update(); + } + } else { + let confidentiality_limit = conn.spaces[space_id] + .crypto + .as_ref() + .map_or_else( + || &conn.zero_rtt_crypto.as_ref().unwrap().packet, + |keys| &keys.packet.local, + ) + .confidentiality_limit(); + if sent_with_keys.saturating_add(1) == confidentiality_limit { + // We still have time to attempt a graceful close + conn.close_inner( + now, + Close::Connection(frame::ConnectionClose { + error_code: TransportErrorCode::AEAD_LIMIT_REACHED, + frame_type: None, + reason: Bytes::from_static(b"confidentiality limit reached"), + }), + ) + } else if sent_with_keys > confidentiality_limit { + // Confidentiality limited violated and there's nothing we can do + conn.kill( + TransportError::AEAD_LIMIT_REACHED("confidentiality limit reached").into(), + ); + return None; + } + } + + let space = &mut conn.spaces[space_id]; + let exact_number = match space_id { + SpaceId::Data => conn.packet_number_filter.allocate(&mut conn.rng, space), + _ => space.get_tx_number(), + }; + + let span = trace_span!("send", space = ?space_id, pn = exact_number).entered(); + + let number = PacketNumber::new(exact_number, space.largest_acked_packet.unwrap_or(0)); + let header = match space_id { + SpaceId::Data if space.crypto.is_some() => Header::Short { + dst_cid, + number, + spin: if conn.spin_enabled { + conn.spin + } else { + conn.rng.random() + }, + key_phase: conn.key_phase, + }, + SpaceId::Data => Header::Long { + ty: LongType::ZeroRtt, + src_cid: conn.handshake_cid, + dst_cid, + number, + version, + }, + SpaceId::Handshake => Header::Long { + ty: LongType::Handshake, + src_cid: conn.handshake_cid, + dst_cid, + number, + version, + }, + SpaceId::Initial => Header::Initial(InitialHeader { + src_cid: conn.handshake_cid, + dst_cid, + token: match &conn.side { + ConnectionSide::Client { token, .. } => token.clone(), + ConnectionSide::Server { .. } => Bytes::new(), + }, + number, + version, + }), + }; + let partial_encode = header.encode(buffer); + if conn.peer_params.grease_quic_bit && conn.rng.random() { + buffer[partial_encode.start] ^= FIXED_BIT; + } + + let (sample_size, tag_len) = if let Some(ref crypto) = space.crypto { + ( + crypto.header.local.sample_size(), + crypto.packet.local.tag_len(), + ) + } else if space_id == SpaceId::Data { + let zero_rtt = conn.zero_rtt_crypto.as_ref().unwrap(); + (zero_rtt.header.sample_size(), zero_rtt.packet.tag_len()) + } else { + unreachable!(); + }; + + // Each packet must be large enough for header protection sampling, i.e. the combined + // lengths of the encoded packet number and protected payload must be at least 4 bytes + // longer than the sample required for header protection. Further, each packet should be at + // least tag_len + 6 bytes larger than the destination CID on incoming packets so that the + // peer may send stateless resets that are indistinguishable from regular traffic. + + // pn_len + payload_len + tag_len >= sample_size + 4 + // payload_len >= sample_size + 4 - pn_len - tag_len + let min_size = Ord::max( + buffer.len() + (sample_size + 4).saturating_sub(number.len() + tag_len), + partial_encode.start + dst_cid.len() + 6, + ); + let max_size = buffer_capacity - tag_len; + debug_assert!(max_size >= min_size); + + Some(Self { + datagram_start, + space: space_id, + partial_encode, + exact_number, + short_header: header.is_short(), + min_size, + max_size, + tag_len, + ack_eliciting, + _span: span, + }) + } + + /// Append the minimum amount of padding to the packet such that, after encryption, the + /// enclosing datagram will occupy at least `min_size` bytes + pub(super) fn pad_to(&mut self, min_size: u16) { + // The datagram might already have a larger minimum size than the caller is requesting, if + // e.g. we're coalescing packets and have populated more than `min_size` bytes with packets + // already. + self.min_size = Ord::max( + self.min_size, + self.datagram_start + (min_size as usize) - self.tag_len, + ); + } + + pub(super) fn finish_and_track( + self, + now: Instant, + conn: &mut Connection, + sent: Option, + buffer: &mut Vec, + ) { + let ack_eliciting = self.ack_eliciting; + let exact_number = self.exact_number; + let space_id = self.space; + let (size, padded) = self.finish(conn, now, buffer); + let sent = match sent { + Some(sent) => sent, + None => return, + }; + + let size = match padded || ack_eliciting { + true => size as u16, + false => 0, + }; + + let packet = SentPacket { + path_generation: conn.path.generation(), + largest_acked: sent.largest_acked, + time_sent: now, + size, + ack_eliciting, + retransmits: sent.retransmits, + stream_frames: sent.stream_frames, + }; + + conn.path + .sent(exact_number, packet, &mut conn.spaces[space_id]); + conn.stats.path.sent_packets += 1; + conn.reset_keep_alive(now); + if size != 0 { + if ack_eliciting { + conn.spaces[space_id].time_of_last_ack_eliciting_packet = Some(now); + if conn.permit_idle_reset { + conn.reset_idle_timeout(now, space_id); + } + conn.permit_idle_reset = false; + } + conn.set_loss_detection_timer(now); + conn.path.pacing.on_transmit(size); + } + } + + /// Encrypt packet, returning the length of the packet and whether padding was added + pub(super) fn finish( + self, + conn: &mut Connection, + now: Instant, + buffer: &mut Vec, + ) -> (usize, bool) { + let pad = buffer.len() < self.min_size; + if pad { + trace!("PADDING * {}", self.min_size - buffer.len()); + buffer.resize(self.min_size, 0); + } + + let space = &conn.spaces[self.space]; + let (header_crypto, packet_crypto) = if let Some(ref crypto) = space.crypto { + (&*crypto.header.local, &*crypto.packet.local) + } else if self.space == SpaceId::Data { + let zero_rtt = conn.zero_rtt_crypto.as_ref().unwrap(); + (&*zero_rtt.header, &*zero_rtt.packet) + } else { + unreachable!("tried to send {:?} packet without keys", self.space); + }; + + debug_assert_eq!( + packet_crypto.tag_len(), + self.tag_len, + "Mismatching crypto tag len" + ); + + buffer.resize(buffer.len() + packet_crypto.tag_len(), 0); + let encode_start = self.partial_encode.start; + let packet_buf = &mut buffer[encode_start..]; + self.partial_encode.finish( + packet_buf, + header_crypto, + Some((self.exact_number, packet_crypto)), + ); + + let len = buffer.len() - encode_start; + conn.config.qlog_sink.emit_packet_sent( + self.exact_number, + len, + self.space, + self.space == SpaceId::Data && conn.spaces[SpaceId::Data].crypto.is_none(), + now, + conn.orig_rem_cid, + ); + + (len, pad) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/packet_crypto.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/packet_crypto.rs new file mode 100644 index 0000000..0aff59c --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/packet_crypto.rs @@ -0,0 +1,173 @@ +use tracing::{debug, trace}; + +use crate::Instant; +use crate::connection::spaces::PacketSpace; +use crate::crypto::{HeaderKey, KeyPair, PacketKey}; +use crate::packet::{Packet, PartialDecode, SpaceId}; +use crate::token::ResetToken; +use crate::{RESET_TOKEN_SIZE, TransportError}; + +/// Removes header protection of a packet, or returns `None` if the packet was dropped +pub(super) fn unprotect_header( + partial_decode: PartialDecode, + spaces: &[PacketSpace; 3], + zero_rtt_crypto: Option<&ZeroRttCrypto>, + stateless_reset_token: Option, +) -> Option { + let header_crypto = if partial_decode.is_0rtt() { + if let Some(crypto) = zero_rtt_crypto { + Some(&*crypto.header) + } else { + debug!("dropping unexpected 0-RTT packet"); + return None; + } + } else if let Some(space) = partial_decode.space() { + if let Some(ref crypto) = spaces[space].crypto { + Some(&*crypto.header.remote) + } else { + debug!( + "discarding unexpected {:?} packet ({} bytes)", + space, + partial_decode.len(), + ); + return None; + } + } else { + // Unprotected packet + None + }; + + let packet = partial_decode.data(); + let stateless_reset = packet.len() >= RESET_TOKEN_SIZE + 5 + && stateless_reset_token.as_deref() == Some(&packet[packet.len() - RESET_TOKEN_SIZE..]); + + match partial_decode.finish(header_crypto) { + Ok(packet) => Some(UnprotectHeaderResult { + packet: Some(packet), + stateless_reset, + }), + Err(_) if stateless_reset => Some(UnprotectHeaderResult { + packet: None, + stateless_reset: true, + }), + Err(e) => { + trace!("unable to complete packet decoding: {}", e); + None + } + } +} + +pub(super) struct UnprotectHeaderResult { + /// The packet with the now unprotected header (`None` in the case of stateless reset packets + /// that fail to be decoded) + pub(super) packet: Option, + /// Whether the packet was a stateless reset packet + pub(super) stateless_reset: bool, +} + +/// Decrypts a packet's body in-place +pub(super) fn decrypt_packet_body( + packet: &mut Packet, + spaces: &[PacketSpace; 3], + zero_rtt_crypto: Option<&ZeroRttCrypto>, + conn_key_phase: bool, + prev_crypto: Option<&PrevCrypto>, + next_crypto: Option<&KeyPair>>, +) -> Result, Option> { + if !packet.header.is_protected() { + // Unprotected packets also don't have packet numbers + return Ok(None); + } + let space = packet.header.space(); + let rx_packet = spaces[space].rx_packet; + let number = packet.header.number().ok_or(None)?.expand(rx_packet + 1); + let packet_key_phase = packet.header.key_phase(); + + let mut crypto_update = false; + let crypto = if packet.header.is_0rtt() { + &zero_rtt_crypto.unwrap().packet + } else if packet_key_phase == conn_key_phase || space != SpaceId::Data { + &spaces[space].crypto.as_ref().unwrap().packet.remote + } else if let Some(prev) = prev_crypto.and_then(|crypto| { + // If this packet comes prior to acknowledgment of the key update by the peer, + if crypto.end_packet.map_or(true, |(pn, _)| number < pn) { + // use the previous keys. + Some(crypto) + } else { + // Otherwise, this must be a remotely-initiated key update, so fall through to the + // final case. + None + } + }) { + &prev.crypto.remote + } else { + // We're in the Data space with a key phase mismatch and either there is no locally + // initiated key update or the locally initiated key update was acknowledged by a + // lower-numbered packet. The key phase mismatch must therefore represent a new + // remotely-initiated key update. + crypto_update = true; + &next_crypto.unwrap().remote + }; + + crypto + .decrypt(number, &packet.header_data, &mut packet.payload) + .map_err(|_| { + trace!("decryption failed with packet number {}", number); + None + })?; + + if !packet.reserved_bits_valid() { + return Err(Some(TransportError::PROTOCOL_VIOLATION( + "reserved bits set", + ))); + } + + let mut outgoing_key_update_acked = false; + if let Some(prev) = prev_crypto { + if prev.end_packet.is_none() && packet_key_phase == conn_key_phase { + outgoing_key_update_acked = true; + } + } + + if crypto_update { + // Validate incoming key update + if number <= rx_packet || prev_crypto.is_some_and(|x| x.update_unacked) { + return Err(Some(TransportError::KEY_UPDATE_ERROR(""))); + } + } + + Ok(Some(DecryptPacketResult { + number, + outgoing_key_update_acked, + incoming_key_update: crypto_update, + })) +} + +pub(super) struct DecryptPacketResult { + /// The packet number + pub(super) number: u64, + /// Whether a locally initiated key update has been acknowledged by the peer + pub(super) outgoing_key_update_acked: bool, + /// Whether the peer has initiated a key update + pub(super) incoming_key_update: bool, +} + +pub(super) struct PrevCrypto { + /// The keys used for the previous key phase, temporarily retained to decrypt packets sent by + /// the peer prior to its own key update. + pub(super) crypto: KeyPair>, + /// The incoming packet that ends the interval for which these keys are applicable, and the time + /// of its receipt. + /// + /// Incoming packets should be decrypted using these keys iff this is `None` or their packet + /// number is lower. `None` indicates that we have not yet received a packet using newer keys, + /// which implies that the update was locally initiated. + pub(super) end_packet: Option<(u64, Instant)>, + /// Whether the following key phase is from a remotely initiated update that we haven't acked + pub(super) update_unacked: bool, +} + +pub(super) struct ZeroRttCrypto { + pub(super) header: Box, + pub(super) packet: Box, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/paths.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/paths.rs new file mode 100644 index 0000000..70582df --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/paths.rs @@ -0,0 +1,456 @@ +use std::{cmp, net::SocketAddr}; + +use tracing::trace; + +use super::{ + mtud::MtuDiscovery, + pacing::Pacer, + spaces::{PacketSpace, SentPacket}, +}; +use crate::{Duration, Instant, TIMER_GRANULARITY, TransportConfig, congestion, packet::SpaceId}; + +#[cfg(feature = "qlog")] +use qlog::events::quic::MetricsUpdated; + +/// Description of a particular network path +pub(super) struct PathData { + pub(super) remote: SocketAddr, + pub(super) rtt: RttEstimator, + /// Whether we're enabling ECN on outgoing packets + pub(super) sending_ecn: bool, + /// Congestion controller state + pub(super) congestion: Box, + /// Pacing state + pub(super) pacing: Pacer, + pub(super) challenge: Option, + pub(super) challenge_pending: bool, + /// Whether we're certain the peer can both send and receive on this address + /// + /// Initially equal to `use_stateless_retry` for servers, and becomes false again on every + /// migration. Always true for clients. + pub(super) validated: bool, + /// Total size of all UDP datagrams sent on this path + pub(super) total_sent: u64, + /// Total size of all UDP datagrams received on this path + pub(super) total_recvd: u64, + /// The state of the MTU discovery process + pub(super) mtud: MtuDiscovery, + /// Packet number of the first packet sent after an RTT sample was collected on this path + /// + /// Used in persistent congestion determination. + pub(super) first_packet_after_rtt_sample: Option<(SpaceId, u64)>, + pub(super) in_flight: InFlight, + /// Number of the first packet sent on this path + /// + /// Used to determine whether a packet was sent on an earlier path. Insufficient to determine if + /// a packet was sent on a later path. + first_packet: Option, + + /// Snapshot of the qlog recovery metrics + #[cfg(feature = "qlog")] + recovery_metrics: RecoveryMetrics, + + /// Tag uniquely identifying a path in a connection + generation: u64, +} + +impl PathData { + pub(super) fn new( + remote: SocketAddr, + allow_mtud: bool, + peer_max_udp_payload_size: Option, + generation: u64, + now: Instant, + config: &TransportConfig, + ) -> Self { + let congestion = config + .congestion_controller_factory + .clone() + .build(now, config.get_initial_mtu()); + Self { + remote, + rtt: RttEstimator::new(config.initial_rtt), + sending_ecn: true, + pacing: Pacer::new( + config.initial_rtt, + congestion.initial_window(), + config.get_initial_mtu(), + now, + ), + congestion, + challenge: None, + challenge_pending: false, + validated: false, + total_sent: 0, + total_recvd: 0, + mtud: config + .mtu_discovery_config + .as_ref() + .filter(|_| allow_mtud) + .map_or( + MtuDiscovery::disabled(config.get_initial_mtu(), config.min_mtu), + |mtud_config| { + MtuDiscovery::new( + config.get_initial_mtu(), + config.min_mtu, + peer_max_udp_payload_size, + mtud_config.clone(), + ) + }, + ), + first_packet_after_rtt_sample: None, + in_flight: InFlight::new(), + first_packet: None, + #[cfg(feature = "qlog")] + recovery_metrics: RecoveryMetrics::default(), + generation, + } + } + + pub(super) fn from_previous( + remote: SocketAddr, + prev: &Self, + generation: u64, + now: Instant, + ) -> Self { + let congestion = prev.congestion.clone_box(); + let smoothed_rtt = prev.rtt.get(); + Self { + remote, + rtt: prev.rtt, + pacing: Pacer::new(smoothed_rtt, congestion.window(), prev.current_mtu(), now), + sending_ecn: true, + congestion, + challenge: None, + challenge_pending: false, + validated: false, + total_sent: 0, + total_recvd: 0, + mtud: prev.mtud.clone(), + first_packet_after_rtt_sample: prev.first_packet_after_rtt_sample, + in_flight: InFlight::new(), + first_packet: None, + #[cfg(feature = "qlog")] + recovery_metrics: prev.recovery_metrics.clone(), + generation, + } + } + + /// Resets RTT, congestion control and MTU states. + /// + /// This is useful when it is known the underlying path has changed. + pub(super) fn reset(&mut self, now: Instant, config: &TransportConfig) { + self.rtt = RttEstimator::new(config.initial_rtt); + self.congestion = config + .congestion_controller_factory + .clone() + .build(now, config.get_initial_mtu()); + self.mtud.reset(config.get_initial_mtu(), config.min_mtu); + } + + /// Indicates whether we're a server that hasn't validated the peer's address and hasn't + /// received enough data from the peer to permit sending `bytes_to_send` additional bytes + pub(super) fn anti_amplification_blocked(&self, bytes_to_send: u64) -> bool { + !self.validated && self.total_recvd * 3 < self.total_sent + bytes_to_send + } + + /// Returns the path's current MTU + pub(super) fn current_mtu(&self) -> u16 { + self.mtud.current_mtu() + } + + /// Account for transmission of `packet` with number `pn` in `space` + pub(super) fn sent(&mut self, pn: u64, packet: SentPacket, space: &mut PacketSpace) { + self.in_flight.insert(&packet); + if self.first_packet.is_none() { + self.first_packet = Some(pn); + } + if let Some(forgotten) = space.sent(pn, packet) { + self.remove_in_flight(&forgotten); + } + } + + /// Remove `packet` with number `pn` from this path's congestion control counters, or return + /// `false` if `pn` was sent before this path was established. + pub(super) fn remove_in_flight(&mut self, packet: &SentPacket) -> bool { + if packet.path_generation != self.generation { + return false; + } + self.in_flight.remove(packet); + true + } + + #[cfg(feature = "qlog")] + pub(super) fn qlog_recovery_metrics(&mut self, pto_count: u32) -> Option { + let controller_metrics = self.congestion.metrics(); + + let metrics = RecoveryMetrics { + min_rtt: Some(self.rtt.min), + smoothed_rtt: Some(self.rtt.get()), + latest_rtt: Some(self.rtt.latest), + rtt_variance: Some(self.rtt.var), + pto_count: Some(pto_count), + bytes_in_flight: Some(self.in_flight.bytes), + packets_in_flight: Some(self.in_flight.ack_eliciting), + + congestion_window: Some(controller_metrics.congestion_window), + ssthresh: controller_metrics.ssthresh, + pacing_rate: controller_metrics.pacing_rate, + }; + + let event = metrics.to_qlog_event(&self.recovery_metrics); + self.recovery_metrics = metrics; + event + } + + pub(super) fn generation(&self) -> u64 { + self.generation + } +} + +/// Congestion metrics as described in [`recovery_metrics_updated`]. +/// +/// [`recovery_metrics_updated`]: https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-quic-events.html#name-recovery_metrics_updated +#[cfg(feature = "qlog")] +#[derive(Default, Clone, PartialEq)] +#[non_exhaustive] +struct RecoveryMetrics { + pub min_rtt: Option, + pub smoothed_rtt: Option, + pub latest_rtt: Option, + pub rtt_variance: Option, + pub pto_count: Option, + pub bytes_in_flight: Option, + pub packets_in_flight: Option, + pub congestion_window: Option, + pub ssthresh: Option, + pub pacing_rate: Option, +} + +#[cfg(feature = "qlog")] +impl RecoveryMetrics { + /// Retain only values that have been updated since the last snapshot. + fn retain_updated(&self, previous: &Self) -> Self { + macro_rules! keep_if_changed { + ($name:ident) => { + if previous.$name == self.$name { + None + } else { + self.$name + } + }; + } + + Self { + min_rtt: keep_if_changed!(min_rtt), + smoothed_rtt: keep_if_changed!(smoothed_rtt), + latest_rtt: keep_if_changed!(latest_rtt), + rtt_variance: keep_if_changed!(rtt_variance), + pto_count: keep_if_changed!(pto_count), + bytes_in_flight: keep_if_changed!(bytes_in_flight), + packets_in_flight: keep_if_changed!(packets_in_flight), + congestion_window: keep_if_changed!(congestion_window), + ssthresh: keep_if_changed!(ssthresh), + pacing_rate: keep_if_changed!(pacing_rate), + } + } + + /// Emit a `MetricsUpdated` event containing only updated values + fn to_qlog_event(&self, previous: &Self) -> Option { + let updated = self.retain_updated(previous); + + if updated == Self::default() { + return None; + } + + Some(MetricsUpdated { + min_rtt: updated.min_rtt.map(|rtt| rtt.as_secs_f32()), + smoothed_rtt: updated.smoothed_rtt.map(|rtt| rtt.as_secs_f32()), + latest_rtt: updated.latest_rtt.map(|rtt| rtt.as_secs_f32()), + rtt_variance: updated.rtt_variance.map(|rtt| rtt.as_secs_f32()), + pto_count: updated + .pto_count + .map(|count| count.try_into().unwrap_or(u16::MAX)), + bytes_in_flight: updated.bytes_in_flight, + packets_in_flight: updated.packets_in_flight, + congestion_window: updated.congestion_window, + ssthresh: updated.ssthresh, + pacing_rate: updated.pacing_rate, + }) + } +} + +/// RTT estimation for a particular network path +#[derive(Copy, Clone)] +pub struct RttEstimator { + /// The most recent RTT measurement made when receiving an ack for a previously unacked packet + latest: Duration, + /// The smoothed RTT of the connection, computed as described in RFC6298 + smoothed: Option, + /// The RTT variance, computed as described in RFC6298 + var: Duration, + /// The minimum RTT seen in the connection, ignoring ack delay. + min: Duration, +} + +impl RttEstimator { + fn new(initial_rtt: Duration) -> Self { + Self { + latest: initial_rtt, + smoothed: None, + var: initial_rtt / 2, + min: initial_rtt, + } + } + + /// The current best RTT estimation. + pub fn get(&self) -> Duration { + self.smoothed.unwrap_or(self.latest) + } + + /// Conservative estimate of RTT + /// + /// Takes the maximum of smoothed and latest RTT, as recommended + /// in 6.1.2 of the recovery spec (draft 29). + pub fn conservative(&self) -> Duration { + self.get().max(self.latest) + } + + /// Minimum RTT registered so far for this estimator. + pub fn min(&self) -> Duration { + self.min + } + + // PTO computed as described in RFC9002#6.2.1 + pub(crate) fn pto_base(&self) -> Duration { + self.get() + cmp::max(4 * self.var, TIMER_GRANULARITY) + } + + pub(crate) fn update(&mut self, ack_delay: Duration, rtt: Duration) { + self.latest = rtt; + // min_rtt ignores ack delay. + self.min = cmp::min(self.min, self.latest); + // Based on RFC6298. + if let Some(smoothed) = self.smoothed { + let adjusted_rtt = if self.min + ack_delay <= self.latest { + self.latest - ack_delay + } else { + self.latest + }; + let var_sample = if smoothed > adjusted_rtt { + smoothed - adjusted_rtt + } else { + adjusted_rtt - smoothed + }; + self.var = (3 * self.var + var_sample) / 4; + self.smoothed = Some((7 * smoothed + adjusted_rtt) / 8); + } else { + self.smoothed = Some(self.latest); + self.var = self.latest / 2; + self.min = self.latest; + } + } +} + +#[derive(Default)] +pub(crate) struct PathResponses { + pending: Vec, +} + +impl PathResponses { + pub(crate) fn push(&mut self, packet: u64, token: u64, remote: SocketAddr) { + /// Arbitrary permissive limit to prevent abuse + const MAX_PATH_RESPONSES: usize = 16; + let response = PathResponse { + packet, + token, + remote, + }; + let existing = self.pending.iter_mut().find(|x| x.remote == remote); + if let Some(existing) = existing { + // Update a queued response + if existing.packet <= packet { + *existing = response; + } + return; + } + if self.pending.len() < MAX_PATH_RESPONSES { + self.pending.push(response); + } else { + // We don't expect to ever hit this with well-behaved peers, so we don't bother dropping + // older challenges. + trace!("ignoring excessive PATH_CHALLENGE"); + } + } + + pub(crate) fn pop_off_path(&mut self, remote: SocketAddr) -> Option<(u64, SocketAddr)> { + let response = *self.pending.last()?; + if response.remote == remote { + // We don't bother searching further because we expect that the on-path response will + // get drained in the immediate future by a call to `pop_on_path` + return None; + } + self.pending.pop(); + Some((response.token, response.remote)) + } + + pub(crate) fn pop_on_path(&mut self, remote: SocketAddr) -> Option { + let response = *self.pending.last()?; + if response.remote != remote { + // We don't bother searching further because we expect that the off-path response will + // get drained in the immediate future by a call to `pop_off_path` + return None; + } + self.pending.pop(); + Some(response.token) + } + + pub(crate) fn is_empty(&self) -> bool { + self.pending.is_empty() + } +} + +#[derive(Copy, Clone)] +struct PathResponse { + /// The packet number the corresponding PATH_CHALLENGE was received in + packet: u64, + token: u64, + /// The address the corresponding PATH_CHALLENGE was received from + remote: SocketAddr, +} + +/// Summary statistics of packets that have been sent on a particular path, but which have not yet +/// been acked or deemed lost +pub(super) struct InFlight { + /// Sum of the sizes of all sent packets considered "in flight" by congestion control + /// + /// The size does not include IP or UDP overhead. Packets only containing ACK frames do not + /// count towards this to ensure congestion control does not impede congestion feedback. + pub(super) bytes: u64, + /// Number of packets in flight containing frames other than ACK and PADDING + /// + /// This can be 0 even when bytes is not 0 because PADDING frames cause a packet to be + /// considered "in flight" by congestion control. However, if this is nonzero, bytes will always + /// also be nonzero. + pub(super) ack_eliciting: u64, +} + +impl InFlight { + fn new() -> Self { + Self { + bytes: 0, + ack_eliciting: 0, + } + } + + fn insert(&mut self, packet: &SentPacket) { + self.bytes += u64::from(packet.size); + self.ack_eliciting += u64::from(packet.ack_eliciting); + } + + /// Update counters to account for a packet becoming acknowledged, lost, or abandoned + fn remove(&mut self, packet: &SentPacket) { + self.bytes -= u64::from(packet.size); + self.ack_eliciting -= u64::from(packet.ack_eliciting); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/qlog.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/qlog.rs new file mode 100644 index 0000000..a324746 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/qlog.rs @@ -0,0 +1,190 @@ +// Function bodies in this module are regularly cfg'd out +#![allow(unused_variables)] + +#[cfg(feature = "qlog")] +use std::sync::{Arc, Mutex}; + +#[cfg(feature = "qlog")] +use qlog::{ + events::{ + Event, EventData, + quic::{ + PacketHeader, PacketLost, PacketLostTrigger, PacketReceived, PacketSent, PacketType, + }, + }, + streamer::QlogStreamer, +}; +#[cfg(feature = "qlog")] +use tracing::warn; + +use crate::{ + ConnectionId, Instant, + connection::{PathData, SentPacket}, + packet::SpaceId, +}; + +/// Shareable handle to a single qlog output stream +#[cfg(feature = "qlog")] +#[derive(Clone)] +pub struct QlogStream(pub(crate) Arc>); + +#[cfg(feature = "qlog")] +impl QlogStream { + fn emit_event(&self, orig_rem_cid: ConnectionId, event: EventData, now: Instant) { + // Time will be overwritten by `add_event_with_instant` + let mut event = Event::with_time(0.0, event); + event.group_id = Some(orig_rem_cid.to_string()); + + let mut qlog_streamer = self.0.lock().unwrap(); + if let Err(e) = qlog_streamer.add_event_with_instant(event, now) { + warn!("could not emit qlog event: {e}"); + } + } +} + +/// A [`QlogStream`] that may be either dynamically disabled or compiled out entirely +#[derive(Clone, Default)] +pub(crate) struct QlogSink { + #[cfg(feature = "qlog")] + stream: Option, +} + +impl QlogSink { + pub(crate) fn is_enabled(&self) -> bool { + #[cfg(feature = "qlog")] + { + self.stream.is_some() + } + #[cfg(not(feature = "qlog"))] + { + false + } + } + + pub(super) fn emit_recovery_metrics( + &self, + pto_count: u32, + path: &mut PathData, + now: Instant, + orig_rem_cid: ConnectionId, + ) { + #[cfg(feature = "qlog")] + { + let Some(stream) = self.stream.as_ref() else { + return; + }; + + let Some(metrics) = path.qlog_recovery_metrics(pto_count) else { + return; + }; + + stream.emit_event(orig_rem_cid, EventData::MetricsUpdated(metrics), now); + } + } + + pub(super) fn emit_packet_lost( + &self, + pn: u64, + info: &SentPacket, + lost_send_time: Instant, + space: SpaceId, + now: Instant, + orig_rem_cid: ConnectionId, + ) { + #[cfg(feature = "qlog")] + { + let Some(stream) = self.stream.as_ref() else { + return; + }; + + let event = PacketLost { + header: Some(PacketHeader { + packet_number: Some(pn), + packet_type: packet_type(space, false), + length: Some(info.size), + ..Default::default() + }), + frames: None, + trigger: Some(match info.time_sent <= lost_send_time { + true => PacketLostTrigger::TimeThreshold, + false => PacketLostTrigger::ReorderingThreshold, + }), + }; + + stream.emit_event(orig_rem_cid, EventData::PacketLost(event), now); + } + } + + pub(super) fn emit_packet_sent( + &self, + pn: u64, + len: usize, + space: SpaceId, + is_0rtt: bool, + now: Instant, + orig_rem_cid: ConnectionId, + ) { + #[cfg(feature = "qlog")] + { + let Some(stream) = self.stream.as_ref() else { + return; + }; + + let event = PacketSent { + header: PacketHeader { + packet_number: Some(pn), + packet_type: packet_type(space, is_0rtt), + length: Some(len as u16), + ..Default::default() + }, + ..Default::default() + }; + + stream.emit_event(orig_rem_cid, EventData::PacketSent(event), now); + } + } + + pub(super) fn emit_packet_received( + &self, + pn: u64, + space: SpaceId, + is_0rtt: bool, + now: Instant, + orig_rem_cid: ConnectionId, + ) { + #[cfg(feature = "qlog")] + { + let Some(stream) = self.stream.as_ref() else { + return; + }; + + let event = PacketReceived { + header: PacketHeader { + packet_number: Some(pn), + packet_type: packet_type(space, is_0rtt), + ..Default::default() + }, + ..Default::default() + }; + + stream.emit_event(orig_rem_cid, EventData::PacketReceived(event), now); + } + } +} + +#[cfg(feature = "qlog")] +impl From> for QlogSink { + fn from(stream: Option) -> Self { + Self { stream } + } +} + +#[cfg(feature = "qlog")] +fn packet_type(space: SpaceId, is_0rtt: bool) -> PacketType { + match space { + SpaceId::Initial => PacketType::Initial, + SpaceId::Handshake => PacketType::Handshake, + SpaceId::Data if is_0rtt => PacketType::ZeroRtt, + SpaceId::Data => PacketType::OneRtt, + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/send_buffer.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/send_buffer.rs new file mode 100644 index 0000000..53a7416 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/send_buffer.rs @@ -0,0 +1,394 @@ +use std::{collections::VecDeque, ops::Range}; + +use bytes::{Buf, Bytes}; + +use crate::{VarInt, range_set::RangeSet}; + +/// Buffer of outgoing retransmittable stream data +#[derive(Default, Debug)] +pub(super) struct SendBuffer { + /// Data queued by the application but not yet acknowledged. May or may not have been sent. + unacked_segments: VecDeque, + /// Total size of `unacked_segments` + unacked_len: usize, + /// The first offset that hasn't been written by the application, i.e. the offset past the end of `unacked` + offset: u64, + /// The first offset that hasn't been sent + /// + /// Always lies in (offset - unacked.len())..offset + unsent: u64, + /// Acknowledged ranges which couldn't be discarded yet as they don't include the earliest + /// offset in `unacked` + // TODO: Recover storage from these by compacting (#700) + acks: RangeSet, + /// Previously transmitted ranges deemed lost + retransmits: RangeSet, +} + +impl SendBuffer { + /// Construct an empty buffer at the initial offset + pub(super) fn new() -> Self { + Self::default() + } + + /// Append application data to the end of the stream + pub(super) fn write(&mut self, data: Bytes) { + self.unacked_len += data.len(); + self.offset += data.len() as u64; + self.unacked_segments.push_back(data); + } + + /// Discard a range of acknowledged stream data + pub(super) fn ack(&mut self, mut range: Range) { + // Clamp the range to data which is still tracked + let base_offset = self.offset - self.unacked_len as u64; + range.start = base_offset.max(range.start); + range.end = base_offset.max(range.end); + + self.acks.insert(range); + + while self.acks.min() == Some(self.offset - self.unacked_len as u64) { + let prefix = self.acks.pop_min().unwrap(); + let mut to_advance = (prefix.end - prefix.start) as usize; + + self.unacked_len -= to_advance; + while to_advance > 0 { + let front = self + .unacked_segments + .front_mut() + .expect("Expected buffered data"); + + if front.len() <= to_advance { + to_advance -= front.len(); + self.unacked_segments.pop_front(); + + if self.unacked_segments.len() * 4 < self.unacked_segments.capacity() { + self.unacked_segments.shrink_to_fit(); + } + } else { + front.advance(to_advance); + to_advance = 0; + } + } + } + } + + /// Compute the next range to transmit on this stream and update state to account for that + /// transmission. + /// + /// `max_len` here includes the space which is available to transmit the + /// offset and length of the data to send. The caller has to guarantee that + /// there is at least enough space available to write maximum-sized metadata + /// (8 byte offset + 8 byte length). + /// + /// The method returns a tuple: + /// - The first return value indicates the range of data to send + /// - The second return value indicates whether the length needs to be encoded + /// in the STREAM frames metadata (`true`), or whether it can be omitted + /// since the selected range will fill the whole packet. + pub(super) fn poll_transmit(&mut self, mut max_len: usize) -> (Range, bool) { + debug_assert!(max_len >= 8 + 8); + let mut encode_length = false; + + if let Some(range) = self.retransmits.pop_min() { + // Retransmit sent data + + // When the offset is known, we know how many bytes are required to encode it. + // Offset 0 requires no space + if range.start != 0 { + max_len -= VarInt::size(unsafe { VarInt::from_u64_unchecked(range.start) }); + } + if range.end - range.start < max_len as u64 { + encode_length = true; + max_len -= 8; + } + + let end = range.end.min((max_len as u64).saturating_add(range.start)); + if end != range.end { + self.retransmits.insert(end..range.end); + } + return (range.start..end, encode_length); + } + + // Transmit new data + + // When the offset is known, we know how many bytes are required to encode it. + // Offset 0 requires no space + if self.unsent != 0 { + max_len -= VarInt::size(unsafe { VarInt::from_u64_unchecked(self.unsent) }); + } + if self.offset - self.unsent < max_len as u64 { + encode_length = true; + max_len -= 8; + } + + let end = self + .offset + .min((max_len as u64).saturating_add(self.unsent)); + let result = self.unsent..end; + self.unsent = end; + (result, encode_length) + } + + /// Returns data which is associated with a range + /// + /// This function can return a subset of the range, if the data is stored + /// in noncontiguous fashion in the send buffer. In this case callers + /// should call the function again with an incremented start offset to + /// retrieve more data. + pub(super) fn get(&self, offsets: Range) -> &[u8] { + let base_offset = self.offset - self.unacked_len as u64; + + let mut segment_offset = base_offset; + for segment in self.unacked_segments.iter() { + if offsets.start >= segment_offset + && offsets.start < segment_offset + segment.len() as u64 + { + let start = (offsets.start - segment_offset) as usize; + let end = (offsets.end - segment_offset) as usize; + + return &segment[start..end.min(segment.len())]; + } + segment_offset += segment.len() as u64; + } + + &[] + } + + /// Queue a range of sent but unacknowledged data to be retransmitted + pub(super) fn retransmit(&mut self, range: Range) { + debug_assert!(range.end <= self.unsent, "unsent data can't be lost"); + self.retransmits.insert(range); + } + + pub(super) fn retransmit_all_for_0rtt(&mut self) { + debug_assert_eq!(self.offset, self.unacked_len as u64); + self.unsent = 0; + } + + /// First stream offset unwritten by the application, i.e. the offset that the next write will + /// begin at + pub(super) fn offset(&self) -> u64 { + self.offset + } + + /// Whether all sent data has been acknowledged + pub(super) fn is_fully_acked(&self) -> bool { + self.unacked_len == 0 + } + + /// Whether there's data to send + /// + /// There may be sent unacknowledged data even when this is false. + pub(super) fn has_unsent_data(&self) -> bool { + self.unsent != self.offset || !self.retransmits.is_empty() + } + + /// Compute the amount of data that hasn't been acknowledged + pub(super) fn unacked(&self) -> u64 { + self.unacked_len as u64 - self.acks.iter().map(|x| x.end - x.start).sum::() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fragment_with_length() { + let mut buf = SendBuffer::new(); + const MSG: &[u8] = b"Hello, world!"; + buf.write(MSG.into()); + // 0 byte offset => 19 bytes left => 13 byte data isn't enough + // with 8 bytes reserved for length 11 payload bytes will fit + assert_eq!(buf.poll_transmit(19), (0..11, true)); + assert_eq!( + buf.poll_transmit(MSG.len() + 16 - 11), + (11..MSG.len() as u64, true) + ); + assert_eq!( + buf.poll_transmit(58), + (MSG.len() as u64..MSG.len() as u64, true) + ); + } + + #[test] + fn fragment_without_length() { + let mut buf = SendBuffer::new(); + const MSG: &[u8] = b"Hello, world with some extra data!"; + buf.write(MSG.into()); + // 0 byte offset => 19 bytes left => can be filled by 34 bytes payload + assert_eq!(buf.poll_transmit(19), (0..19, false)); + assert_eq!( + buf.poll_transmit(MSG.len() - 19 + 1), + (19..MSG.len() as u64, false) + ); + assert_eq!( + buf.poll_transmit(58), + (MSG.len() as u64..MSG.len() as u64, true) + ); + } + + #[test] + fn reserves_encoded_offset() { + let mut buf = SendBuffer::new(); + + // Pretend we have more than 1 GB of data in the buffer + let chunk: Bytes = Bytes::from_static(&[0; 1024 * 1024]); + for _ in 0..1025 { + buf.write(chunk.clone()); + } + + const SIZE1: u64 = 64; + const SIZE2: u64 = 16 * 1024; + const SIZE3: u64 = 1024 * 1024 * 1024; + + // Offset 0 requires no space + assert_eq!(buf.poll_transmit(16), (0..16, false)); + buf.retransmit(0..16); + assert_eq!(buf.poll_transmit(16), (0..16, false)); + let mut transmitted = 16u64; + + // Offset 16 requires 1 byte + assert_eq!( + buf.poll_transmit((SIZE1 - transmitted + 1) as usize), + (transmitted..SIZE1, false) + ); + buf.retransmit(transmitted..SIZE1); + assert_eq!( + buf.poll_transmit((SIZE1 - transmitted + 1) as usize), + (transmitted..SIZE1, false) + ); + transmitted = SIZE1; + + // Offset 64 requires 2 bytes + assert_eq!( + buf.poll_transmit((SIZE2 - transmitted + 2) as usize), + (transmitted..SIZE2, false) + ); + buf.retransmit(transmitted..SIZE2); + assert_eq!( + buf.poll_transmit((SIZE2 - transmitted + 2) as usize), + (transmitted..SIZE2, false) + ); + transmitted = SIZE2; + + // Offset 16384 requires requires 4 bytes + assert_eq!( + buf.poll_transmit((SIZE3 - transmitted + 4) as usize), + (transmitted..SIZE3, false) + ); + buf.retransmit(transmitted..SIZE3); + assert_eq!( + buf.poll_transmit((SIZE3 - transmitted + 4) as usize), + (transmitted..SIZE3, false) + ); + transmitted = SIZE3; + + // Offset 1GB requires 8 bytes + assert_eq!( + buf.poll_transmit(chunk.len() + 8), + (transmitted..transmitted + chunk.len() as u64, false) + ); + buf.retransmit(transmitted..transmitted + chunk.len() as u64); + assert_eq!( + buf.poll_transmit(chunk.len() + 8), + (transmitted..transmitted + chunk.len() as u64, false) + ); + } + + #[test] + fn multiple_segments() { + let mut buf = SendBuffer::new(); + const MSG: &[u8] = b"Hello, world!"; + const MSG_LEN: u64 = MSG.len() as u64; + + const SEG1: &[u8] = b"He"; + buf.write(SEG1.into()); + const SEG2: &[u8] = b"llo,"; + buf.write(SEG2.into()); + const SEG3: &[u8] = b" w"; + buf.write(SEG3.into()); + const SEG4: &[u8] = b"o"; + buf.write(SEG4.into()); + const SEG5: &[u8] = b"rld!"; + buf.write(SEG5.into()); + + assert_eq!(aggregate_unacked(&buf), MSG); + + assert_eq!(buf.poll_transmit(16), (0..8, true)); + assert_eq!(buf.get(0..5), SEG1); + assert_eq!(buf.get(2..8), SEG2); + assert_eq!(buf.get(6..8), SEG3); + + assert_eq!(buf.poll_transmit(16), (8..MSG_LEN, true)); + assert_eq!(buf.get(8..MSG_LEN), SEG4); + assert_eq!(buf.get(9..MSG_LEN), SEG5); + + assert_eq!(buf.poll_transmit(42), (MSG_LEN..MSG_LEN, true)); + + // Now drain the segments + buf.ack(0..1); + assert_eq!(aggregate_unacked(&buf), &MSG[1..]); + buf.ack(0..3); + assert_eq!(aggregate_unacked(&buf), &MSG[3..]); + buf.ack(3..5); + assert_eq!(aggregate_unacked(&buf), &MSG[5..]); + buf.ack(7..9); + assert_eq!(aggregate_unacked(&buf), &MSG[5..]); + buf.ack(4..7); + assert_eq!(aggregate_unacked(&buf), &MSG[9..]); + buf.ack(0..MSG_LEN); + assert_eq!(aggregate_unacked(&buf), &[] as &[u8]); + } + + #[test] + fn retransmit() { + let mut buf = SendBuffer::new(); + const MSG: &[u8] = b"Hello, world with extra data!"; + buf.write(MSG.into()); + // Transmit two frames + assert_eq!(buf.poll_transmit(16), (0..16, false)); + assert_eq!(buf.poll_transmit(16), (16..23, true)); + // Lose the first, but not the second + buf.retransmit(0..16); + // Ensure we only retransmit the lost frame, then continue sending fresh data + assert_eq!(buf.poll_transmit(16), (0..16, false)); + assert_eq!(buf.poll_transmit(16), (23..MSG.len() as u64, true)); + // Lose the second frame + buf.retransmit(16..23); + assert_eq!(buf.poll_transmit(16), (16..23, true)); + } + + #[test] + fn ack() { + let mut buf = SendBuffer::new(); + const MSG: &[u8] = b"Hello, world!"; + buf.write(MSG.into()); + assert_eq!(buf.poll_transmit(16), (0..8, true)); + buf.ack(0..8); + assert_eq!(aggregate_unacked(&buf), &MSG[8..]); + } + + #[test] + fn reordered_ack() { + let mut buf = SendBuffer::new(); + const MSG: &[u8] = b"Hello, world with extra data!"; + buf.write(MSG.into()); + assert_eq!(buf.poll_transmit(16), (0..16, false)); + assert_eq!(buf.poll_transmit(16), (16..23, true)); + buf.ack(16..23); + assert_eq!(aggregate_unacked(&buf), MSG); + buf.ack(0..16); + assert_eq!(aggregate_unacked(&buf), &MSG[23..]); + assert!(buf.acks.is_empty()); + } + + fn aggregate_unacked(buf: &SendBuffer) -> Vec { + let mut result = Vec::new(); + for segment in buf.unacked_segments.iter() { + result.extend_from_slice(&segment[..]); + } + result + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/spaces.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/spaces.rs new file mode 100644 index 0000000..8282f70 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/spaces.rs @@ -0,0 +1,1088 @@ +use std::{ + cmp, + collections::{BTreeMap, VecDeque}, + mem, + ops::{Bound, Index, IndexMut}, +}; + +use rand::Rng; +use rustc_hash::FxHashSet; +use tracing::trace; + +use super::assembler::Assembler; +use crate::{ + Dir, Duration, Instant, SocketAddr, StreamId, TransportError, VarInt, connection::StreamsState, + crypto::Keys, frame, packet::SpaceId, range_set::ArrayRangeSet, shared::IssuedCid, +}; + +pub(super) struct PacketSpace { + pub(super) crypto: Option, + pub(super) dedup: Dedup, + /// Highest received packet number + pub(super) rx_packet: u64, + + /// Data to send + pub(super) pending: Retransmits, + /// Packet numbers to acknowledge + pub(super) pending_acks: PendingAcks, + + /// The packet number of the next packet that will be sent, if any. In the Data space, the + /// packet number stored here is sometimes skipped by [`PacketNumberFilter`] logic. + pub(super) next_packet_number: u64, + /// The largest packet number the remote peer acknowledged in an ACK frame. + pub(super) largest_acked_packet: Option, + pub(super) largest_acked_packet_sent: Instant, + /// The highest-numbered ACK-eliciting packet we've sent + pub(super) largest_ack_eliciting_sent: u64, + /// Number of packets in `sent_packets` with numbers above `largest_ack_eliciting_sent` + pub(super) unacked_non_ack_eliciting_tail: u64, + /// Transmitted but not acked + // We use a BTreeMap here so we can efficiently query by range on ACK and for loss detection + pub(super) sent_packets: BTreeMap, + /// Number of explicit congestion notification codepoints seen on incoming packets + pub(super) ecn_counters: frame::EcnCounts, + /// Recent ECN counters sent by the peer in ACK frames + /// + /// Updated (and inspected) whenever we receive an ACK with a new highest acked packet + /// number. Stored per-space to simplify verification, which would otherwise have difficulty + /// distinguishing between ECN bleaching and counts having been updated by a near-simultaneous + /// ACK already processed in another space. + pub(super) ecn_feedback: frame::EcnCounts, + + /// Incoming cryptographic handshake stream + pub(super) crypto_stream: Assembler, + /// Current offset of outgoing cryptographic handshake stream + pub(super) crypto_offset: u64, + + /// The time the most recently sent retransmittable packet was sent. + pub(super) time_of_last_ack_eliciting_packet: Option, + /// The time at which the earliest sent packet in this space will be considered lost based on + /// exceeding the reordering window in time. Only set for packets numbered prior to a packet + /// that has been acknowledged. + pub(super) loss_time: Option, + /// Number of tail loss probes to send + pub(super) loss_probes: u32, + pub(super) ping_pending: bool, + pub(super) immediate_ack_pending: bool, + /// Number of packets sent in the current key phase + pub(super) sent_with_keys: u64, +} + +impl PacketSpace { + pub(super) fn new(now: Instant) -> Self { + Self { + crypto: None, + dedup: Dedup::new(), + rx_packet: 0, + + pending: Retransmits::default(), + pending_acks: PendingAcks::new(), + + next_packet_number: 0, + largest_acked_packet: None, + largest_acked_packet_sent: now, + largest_ack_eliciting_sent: 0, + unacked_non_ack_eliciting_tail: 0, + sent_packets: BTreeMap::new(), + ecn_counters: frame::EcnCounts::ZERO, + ecn_feedback: frame::EcnCounts::ZERO, + + crypto_stream: Assembler::new(), + crypto_offset: 0, + + time_of_last_ack_eliciting_packet: None, + loss_time: None, + loss_probes: 0, + ping_pending: false, + immediate_ack_pending: false, + sent_with_keys: 0, + } + } + + /// Queue data for a tail loss probe (or anti-amplification deadlock prevention) packet + /// + /// Probes are sent similarly to normal packets when an expected ACK has not arrived. We never + /// deem a packet lost until we receive an ACK that should have included it, but if a trailing + /// run of packets (or their ACKs) are lost, this might not happen in a timely fashion. We send + /// probe packets to force an ACK, and exempt them from congestion control to prevent a deadlock + /// when the congestion window is filled with lost tail packets. + /// + /// We prefer to send new data, to make the most efficient use of bandwidth. If there's no data + /// waiting to be sent, then we retransmit in-flight data to reduce odds of loss. If there's no + /// in-flight data either, we're probably a client guarding against a handshake + /// anti-amplification deadlock and we just make something up. + pub(super) fn maybe_queue_probe( + &mut self, + request_immediate_ack: bool, + streams: &StreamsState, + ) { + if self.loss_probes == 0 { + return; + } + + if request_immediate_ack { + // The probe should be ACKed without delay (should only be used in the Data space and + // when the peer supports the acknowledgement frequency extension) + self.immediate_ack_pending = true; + } + + if !self.pending.is_empty(streams) { + // There's real data to send here, no need to make something up + return; + } + + // Retransmit the data of the oldest in-flight packet + for packet in self.sent_packets.values_mut() { + if !packet.retransmits.is_empty(streams) { + // Remove retransmitted data from the old packet so we don't end up retransmitting + // it *again* even if the copy we're sending now gets acknowledged. + self.pending |= mem::take(&mut packet.retransmits); + return; + } + } + + // Nothing new to send and nothing to retransmit, so fall back on a ping. This should only + // happen in rare cases during the handshake when the server becomes blocked by + // anti-amplification. + if !self.immediate_ack_pending { + self.ping_pending = true; + } + } + + /// Get the next outgoing packet number in this space + /// + /// In the Data space, the connection's [`PacketNumberFilter`] must be used rather than calling + /// this directly. + pub(super) fn get_tx_number(&mut self) -> u64 { + // TODO: Handle packet number overflow gracefully + assert!(self.next_packet_number < 2u64.pow(62)); + let x = self.next_packet_number; + self.next_packet_number += 1; + self.sent_with_keys += 1; + x + } + + pub(super) fn can_send(&self, streams: &StreamsState) -> SendableFrames { + let acks = self.pending_acks.can_send(); + let other = + !self.pending.is_empty(streams) || self.ping_pending || self.immediate_ack_pending; + + SendableFrames { acks, other } + } + + /// Verifies sanity of an ECN block and returns whether congestion was encountered. + pub(super) fn detect_ecn( + &mut self, + newly_acked: u64, + ecn: frame::EcnCounts, + ) -> Result { + let ect0_increase = ecn + .ect0 + .checked_sub(self.ecn_feedback.ect0) + .ok_or("peer ECT(0) count regression")?; + let ect1_increase = ecn + .ect1 + .checked_sub(self.ecn_feedback.ect1) + .ok_or("peer ECT(1) count regression")?; + let ce_increase = ecn + .ce + .checked_sub(self.ecn_feedback.ce) + .ok_or("peer CE count regression")?; + let total_increase = ect0_increase + ect1_increase + ce_increase; + if total_increase < newly_acked { + return Err("ECN bleaching"); + } + if (ect0_increase + ce_increase) < newly_acked || ect1_increase != 0 { + return Err("ECN corruption"); + } + // If total_increase > newly_acked (which happens when ACKs are lost), this is required by + // the draft so that long-term drift does not occur. If =, then the only question is whether + // to count CE packets as CE or ECT0. Recording them as CE is more consistent and keeps the + // congestion check obvious. + self.ecn_feedback = ecn; + Ok(ce_increase != 0) + } + + /// Stop tracking sent packet `number`, and return what we knew about it + pub(super) fn take(&mut self, number: u64) -> Option { + let packet = self.sent_packets.remove(&number)?; + if !packet.ack_eliciting && number > self.largest_ack_eliciting_sent { + self.unacked_non_ack_eliciting_tail = + self.unacked_non_ack_eliciting_tail.checked_sub(1).unwrap(); + } + Some(packet) + } + + /// May return a packet that should be forgotten + pub(super) fn sent(&mut self, number: u64, packet: SentPacket) -> Option { + // Retain state for at most this many non-ACK-eliciting packets sent after the most recently + // sent ACK-eliciting packet. We're never guaranteed to receive an ACK for those, and we + // can't judge them as lost without an ACK, so to limit memory in applications which receive + // packets but don't send ACK-eliciting data for long periods use we must eventually start + // forgetting about them, although it might also be reasonable to just kill the connection + // due to weird peer behavior. + const MAX_UNACKED_NON_ACK_ELICTING_TAIL: u64 = 1_000; + + let mut forgotten = None; + if packet.ack_eliciting { + self.unacked_non_ack_eliciting_tail = 0; + self.largest_ack_eliciting_sent = number; + } else if self.unacked_non_ack_eliciting_tail > MAX_UNACKED_NON_ACK_ELICTING_TAIL { + let oldest_after_ack_eliciting = *self + .sent_packets + .range(( + Bound::Excluded(self.largest_ack_eliciting_sent), + Bound::Unbounded, + )) + .next() + .unwrap() + .0; + // Per https://www.rfc-editor.org/rfc/rfc9000.html#name-frames-and-frame-types, + // non-ACK-eliciting packets must only contain PADDING, ACK, and CONNECTION_CLOSE + // frames, which require no special handling on ACK or loss beyond removal from + // in-flight counters if padded. + let packet = self + .sent_packets + .remove(&oldest_after_ack_eliciting) + .unwrap(); + debug_assert!(!packet.ack_eliciting); + forgotten = Some(packet); + } else { + self.unacked_non_ack_eliciting_tail += 1; + } + + self.sent_packets.insert(number, packet); + forgotten + } + + /// Whether any congestion-controlled packets in this space are not yet acknowledged or lost + pub(super) fn has_in_flight(&self) -> bool { + // The number of non-congestion-controlled (i.e. size == 0) packets in flight at a time + // should be small, since otherwise congestion control wouldn't be effective. Therefore, + // this shouldn't need to visit many packets before finishing one way or another. + self.sent_packets.values().any(|x| x.size != 0) + } +} + +impl Index for [PacketSpace; 3] { + type Output = PacketSpace; + fn index(&self, space: SpaceId) -> &PacketSpace { + &self.as_ref()[space as usize] + } +} + +impl IndexMut for [PacketSpace; 3] { + fn index_mut(&mut self, space: SpaceId) -> &mut PacketSpace { + &mut self.as_mut()[space as usize] + } +} + +/// Represents one or more packets subject to retransmission +#[derive(Debug, Clone)] +pub(super) struct SentPacket { + /// [`PathData::generation`](super::PathData::generation) of the path on which this packet was sent + pub(super) path_generation: u64, + /// The time the packet was sent. + pub(super) time_sent: Instant, + /// The number of bytes sent in the packet, not including UDP or IP overhead, but including QUIC + /// framing overhead. Zero if this packet is not counted towards congestion control, i.e. not an + /// "in flight" packet. + pub(super) size: u16, + /// Whether an acknowledgement is expected directly in response to this packet. + pub(super) ack_eliciting: bool, + /// The largest packet number acknowledged by this packet + pub(super) largest_acked: Option, + /// Data which needs to be retransmitted in case the packet is lost. + /// The data is boxed to minimize `SentPacket` size for the typical case of + /// packets only containing ACKs and STREAM frames. + pub(super) retransmits: ThinRetransmits, + /// Metadata for stream frames in a packet + /// + /// The actual application data is stored with the stream state. + pub(super) stream_frames: frame::StreamMetaVec, +} + +/// Retransmittable data queue +#[allow(unreachable_pub)] // fuzzing only +#[derive(Debug, Default, Clone)] +pub struct Retransmits { + pub(super) max_data: bool, + pub(super) max_stream_id: [bool; 2], + pub(super) reset_stream: Vec<(StreamId, VarInt)>, + pub(super) stop_sending: Vec, + pub(super) max_stream_data: FxHashSet, + pub(super) crypto: VecDeque, + pub(super) new_cids: Vec, + pub(super) retire_cids: Vec, + pub(super) ack_frequency: bool, + pub(super) handshake_done: bool, + /// For each enqueued NEW_TOKEN frame, a copy of the path's remote address + /// + /// There are 2 reasons this is unusual: + /// + /// - If the path changes, NEW_TOKEN frames bound for the old path are not retransmitted on the + /// new path. That is why this field stores the remote address: so that ones for old paths + /// can be filtered out. + /// - If a token is lost, a new randomly generated token is re-transmitted, rather than the + /// original. This is so that if both transmissions are received, the client won't risk + /// sending the same token twice. That is why this field does _not_ store any actual token. + /// + /// It is true that a QUIC endpoint will only want to effectively have NEW_TOKEN frames + /// enqueued for its current path at a given point in time. Based on that, we could conceivably + /// change this from a vector to an `Option<(SocketAddr, usize)>` or just a `usize` or + /// something. However, due to the architecture of Quinn, it is considerably simpler to not do + /// that; consider what such a change would mean for implementing `BitOrAssign` on Self. + pub(super) new_tokens: Vec, +} + +impl Retransmits { + pub(super) fn is_empty(&self, streams: &StreamsState) -> bool { + !self.max_data + && !self.max_stream_id.into_iter().any(|x| x) + && self.reset_stream.is_empty() + && self.stop_sending.is_empty() + && self + .max_stream_data + .iter() + .all(|&id| !streams.can_send_flow_control(id)) + && self.crypto.is_empty() + && self.new_cids.is_empty() + && self.retire_cids.is_empty() + && !self.ack_frequency + && !self.handshake_done + && self.new_tokens.is_empty() + } +} + +impl ::std::ops::BitOrAssign for Retransmits { + fn bitor_assign(&mut self, rhs: Self) { + // We reduce in-stream head-of-line blocking by queueing retransmits before other data for + // STREAM and CRYPTO frames. + self.max_data |= rhs.max_data; + for dir in Dir::iter() { + self.max_stream_id[dir as usize] |= rhs.max_stream_id[dir as usize]; + } + self.reset_stream.extend_from_slice(&rhs.reset_stream); + self.stop_sending.extend_from_slice(&rhs.stop_sending); + self.max_stream_data.extend(&rhs.max_stream_data); + for crypto in rhs.crypto.into_iter().rev() { + self.crypto.push_front(crypto); + } + self.new_cids.extend(&rhs.new_cids); + self.retire_cids.extend(rhs.retire_cids); + self.ack_frequency |= rhs.ack_frequency; + self.handshake_done |= rhs.handshake_done; + self.new_tokens.extend_from_slice(&rhs.new_tokens); + } +} + +impl ::std::ops::BitOrAssign for Retransmits { + fn bitor_assign(&mut self, rhs: ThinRetransmits) { + if let Some(retransmits) = rhs.retransmits { + self.bitor_assign(*retransmits) + } + } +} + +impl ::std::iter::FromIterator for Retransmits { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + let mut result = Self::default(); + for packet in iter { + result |= packet; + } + result + } +} + +/// A variant of `Retransmits` which only allocates storage when required +#[derive(Debug, Default, Clone)] +pub(super) struct ThinRetransmits { + retransmits: Option>, +} + +impl ThinRetransmits { + /// Returns `true` if no retransmits are necessary + pub(super) fn is_empty(&self, streams: &StreamsState) -> bool { + match &self.retransmits { + Some(retransmits) => retransmits.is_empty(streams), + None => true, + } + } + + /// Returns a reference to the retransmits stored in this box + pub(super) fn get(&self) -> Option<&Retransmits> { + self.retransmits.as_deref() + } + + /// Returns a mutable reference to the stored retransmits + /// + /// This function will allocate a backing storage if required. + pub(super) fn get_or_create(&mut self) -> &mut Retransmits { + if self.retransmits.is_none() { + self.retransmits = Some(Box::default()); + } + self.retransmits.as_deref_mut().unwrap() + } +} + +/// RFC4303-style sliding window packet number deduplicator. +/// +/// A contiguous bitfield, where each bit corresponds to a packet number and the rightmost bit is +/// always set. A set bit represents a packet that has been successfully authenticated. Bits left of +/// the window are assumed to be set. +/// +/// ```text +/// ...xxxxxxxxx 1 0 +/// ^ ^ ^ +/// window highest next +/// ``` +pub(super) struct Dedup { + window: Window, + /// Lowest packet number higher than all yet authenticated. + next: u64, +} + +/// Inner bitfield type. +/// +/// Because QUIC never reuses packet numbers, this only needs to be large enough to deal with +/// packets that are reordered but still delivered in a timely manner. +type Window = u128; + +/// Number of packets tracked by `Dedup`. +const WINDOW_SIZE: u64 = 1 + mem::size_of::() as u64 * 8; + +impl Dedup { + /// Construct an empty window positioned at the start. + pub(super) fn new() -> Self { + Self { window: 0, next: 0 } + } + + /// Highest packet number authenticated. + fn highest(&self) -> u64 { + self.next - 1 + } + + /// Record a newly authenticated packet number. + /// + /// Returns whether the packet might be a duplicate. + pub(super) fn insert(&mut self, packet: u64) -> bool { + if let Some(diff) = packet.checked_sub(self.next) { + // Right of window + self.window = ((self.window << 1) | 1) + .checked_shl(cmp::min(diff, u64::from(u32::MAX)) as u32) + .unwrap_or(0); + self.next = packet + 1; + false + } else if self.highest() - packet < WINDOW_SIZE { + // Within window + if let Some(bit) = (self.highest() - packet).checked_sub(1) { + // < highest + let mask = 1 << bit; + let duplicate = self.window & mask != 0; + self.window |= mask; + duplicate + } else { + // == highest + true + } + } else { + // Left of window + true + } + } + + /// Returns the packet number of the smallest packet missing between the provided interval + /// + /// If there are no missing packets, returns `None` + fn smallest_missing_in_interval(&self, lower_bound: u64, upper_bound: u64) -> Option { + debug_assert!(lower_bound <= upper_bound); + debug_assert!(upper_bound <= self.highest()); + const BITFIELD_SIZE: u64 = (mem::size_of::() * 8) as u64; + + // Since we already know the packets at the boundaries have been received, we only need to + // check those in between them (this removes the necessity of extra logic to deal with the + // highest packet, which is stored outside the bitfield) + let lower_bound = lower_bound + 1; + let upper_bound = upper_bound.saturating_sub(1); + + // Note: the offsets are counted from the right + // The highest packet is not included in the bitfield, so we subtract 1 to account for that + let start_offset = (self.highest() - upper_bound).max(1) - 1; + if start_offset >= BITFIELD_SIZE { + // The start offset is outside of the window. All packets outside of the window are + // considered to be received. + return None; + } + + let end_offset_exclusive = self.highest().saturating_sub(lower_bound); + + // The range is clamped at the edge of the window, because any earlier packets are + // considered to be received + let range_len = end_offset_exclusive + .saturating_sub(start_offset) + .min(BITFIELD_SIZE); + if range_len == 0 { + return None; + } + + // Ensure the shift is within bounds (we already know start_offset < BITFIELD_SIZE, + // because of the early return) + let mask = if range_len == BITFIELD_SIZE { + u128::MAX + } else { + ((1u128 << range_len) - 1) << start_offset + }; + let gaps = !self.window & mask; + + let smallest_missing_offset = 128 - gaps.leading_zeros() as u64; + let smallest_missing_packet = self.highest() - smallest_missing_offset; + + if smallest_missing_packet <= upper_bound { + Some(smallest_missing_packet) + } else { + None + } + } + + /// Returns true if there are any missing packets between the provided interval + /// + /// The provided packet numbers must have been received before calling this function + fn missing_in_interval(&self, lower_bound: u64, upper_bound: u64) -> bool { + self.smallest_missing_in_interval(lower_bound, upper_bound) + .is_some() + } +} + +/// Indicates which data is available for sending +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) struct SendableFrames { + pub(super) acks: bool, + pub(super) other: bool, +} + +impl SendableFrames { + /// Returns that no data is available for sending + pub(super) fn empty() -> Self { + Self { + acks: false, + other: false, + } + } + + /// Whether no data is sendable + pub(super) fn is_empty(&self) -> bool { + !self.acks && !self.other + } +} + +#[derive(Debug)] +pub(super) struct PendingAcks { + /// Whether we should send an ACK immediately, even if that means sending an ACK-only packet + /// + /// When `immediate_ack_required` is false, the normal behavior is to send ACK frames only when + /// there is other data to send, or when the `MaxAckDelay` timer expires. + immediate_ack_required: bool, + /// The number of ack-eliciting packets received since the last ACK frame was sent + /// + /// Once the count _exceeds_ `ack_eliciting_threshold`, an immediate ACK is required + ack_eliciting_since_last_ack_sent: u64, + non_ack_eliciting_since_last_ack_sent: u64, + ack_eliciting_threshold: u64, + /// The reordering threshold, controlling how we respond to out-of-order ack-eliciting packets + /// + /// Different values enable different behavior: + /// + /// * `0`: no special action is taken + /// * `1`: an ACK is immediately sent if it is out-of-order according to RFC 9000 + /// * `>1`: an ACK is immediately sent if it is out-of-order according to the ACK frequency draft + reordering_threshold: u64, + /// The earliest ack-eliciting packet since the last ACK was sent, used to calculate the moment + /// upon which `max_ack_delay` elapses + earliest_ack_eliciting_since_last_ack_sent: Option, + /// The packet number ranges of ack-eliciting packets the peer hasn't confirmed receipt of ACKs + /// for + ranges: ArrayRangeSet, + /// The packet with the largest packet number, and the time upon which it was received (used to + /// calculate ACK delay in [`PendingAcks::ack_delay`]) + largest_packet: Option<(u64, Instant)>, + /// The ack-eliciting packet we have received with the largest packet number + largest_ack_eliciting_packet: Option, + /// The largest acknowledged packet number sent in an ACK frame + largest_acked: Option, +} + +impl PendingAcks { + fn new() -> Self { + Self { + immediate_ack_required: false, + ack_eliciting_since_last_ack_sent: 0, + non_ack_eliciting_since_last_ack_sent: 0, + ack_eliciting_threshold: 1, + reordering_threshold: 1, + earliest_ack_eliciting_since_last_ack_sent: None, + ranges: ArrayRangeSet::default(), + largest_packet: None, + largest_ack_eliciting_packet: None, + largest_acked: None, + } + } + + pub(super) fn set_ack_frequency_params(&mut self, frame: &frame::AckFrequency) { + self.ack_eliciting_threshold = frame.ack_eliciting_threshold.into_inner(); + self.reordering_threshold = frame.reordering_threshold.into_inner(); + } + + pub(super) fn set_immediate_ack_required(&mut self) { + self.immediate_ack_required = true; + } + + pub(super) fn on_max_ack_delay_timeout(&mut self) { + self.immediate_ack_required = self.ack_eliciting_since_last_ack_sent > 0; + } + + pub(super) fn max_ack_delay_timeout(&self, max_ack_delay: Duration) -> Option { + self.earliest_ack_eliciting_since_last_ack_sent + .map(|earliest_unacked| earliest_unacked + max_ack_delay) + } + + /// Whether any ACK frames can be sent + pub(super) fn can_send(&self) -> bool { + self.immediate_ack_required && !self.ranges.is_empty() + } + + /// Returns the delay since the packet with the largest packet number was received + pub(super) fn ack_delay(&self, now: Instant) -> Duration { + self.largest_packet + .map_or(Duration::default(), |(_, received)| now - received) + } + + /// Handle receipt of a new packet + /// + /// Returns true if the max ack delay timer should be armed + pub(super) fn packet_received( + &mut self, + now: Instant, + packet_number: u64, + ack_eliciting: bool, + dedup: &Dedup, + ) -> bool { + if !ack_eliciting { + self.non_ack_eliciting_since_last_ack_sent += 1; + return false; + } + + let prev_largest_ack_eliciting = self.largest_ack_eliciting_packet.unwrap_or(0); + + // Track largest ack-eliciting packet + self.largest_ack_eliciting_packet = self + .largest_ack_eliciting_packet + .map(|pn| pn.max(packet_number)) + .or(Some(packet_number)); + + // Handle ack_eliciting_threshold + self.ack_eliciting_since_last_ack_sent += 1; + self.immediate_ack_required |= + self.ack_eliciting_since_last_ack_sent > self.ack_eliciting_threshold; + + // Handle out-of-order packets + self.immediate_ack_required |= + self.is_out_of_order(packet_number, prev_largest_ack_eliciting, dedup); + + // Arm max_ack_delay timer if necessary + if self.earliest_ack_eliciting_since_last_ack_sent.is_none() && !self.can_send() { + self.earliest_ack_eliciting_since_last_ack_sent = Some(now); + return true; + } + + false + } + + fn is_out_of_order( + &self, + packet_number: u64, + prev_largest_ack_eliciting: u64, + dedup: &Dedup, + ) -> bool { + match self.reordering_threshold { + 0 => false, + 1 => { + // From https://www.rfc-editor.org/rfc/rfc9000#section-13.2.1-7 + packet_number < prev_largest_ack_eliciting + || dedup.missing_in_interval(prev_largest_ack_eliciting, packet_number) + } + _ => { + // From acknowledgement frequency draft, section 6.1: send an ACK immediately if + // doing so would cause the sender to detect a new packet loss + let Some((largest_acked, largest_unacked)) = + self.largest_acked.zip(self.largest_ack_eliciting_packet) + else { + return false; + }; + if self.reordering_threshold > largest_acked { + return false; + } + // The largest packet number that could be declared lost without a new ACK being + // sent + let largest_reported = largest_acked - self.reordering_threshold + 1; + let Some(smallest_missing_unreported) = + dedup.smallest_missing_in_interval(largest_reported, largest_unacked) + else { + return false; + }; + largest_unacked - smallest_missing_unreported >= self.reordering_threshold + } + } + } + + /// Should be called whenever ACKs have been sent + /// + /// This will suppress sending further ACKs until additional ACK eliciting frames arrive + pub(super) fn acks_sent(&mut self) { + // It is possible (though unlikely) that the ACKs we just sent do not cover all the + // ACK-eliciting packets we have received (e.g. if there is not enough room in the packet to + // fit all the ranges). To keep things simple, however, we assume they do. If there are + // indeed some ACKs that weren't covered, the packets might be ACKed later anyway, because + // they are still contained in `self.ranges`. If we somehow fail to send the ACKs at a later + // moment, the peer will assume the packets got lost and will retransmit their frames in a + // new packet, which is suboptimal, because we already received them. Our assumption here is + // that simplicity results in code that is more performant, even in the presence of + // occasional redundant retransmits. + self.immediate_ack_required = false; + self.ack_eliciting_since_last_ack_sent = 0; + self.non_ack_eliciting_since_last_ack_sent = 0; + self.earliest_ack_eliciting_since_last_ack_sent = None; + self.largest_acked = self.largest_ack_eliciting_packet; + } + + /// Insert one packet that needs to be acknowledged + pub(super) fn insert_one(&mut self, packet: u64, now: Instant) { + self.ranges.insert_one(packet); + + if self.largest_packet.map_or(true, |(pn, _)| packet > pn) { + self.largest_packet = Some((packet, now)); + } + + if self.ranges.len() > MAX_ACK_BLOCKS { + self.ranges.pop_min(); + } + } + + /// Remove ACKs of packets numbered at or below `max` from the set of pending ACKs + pub(super) fn subtract_below(&mut self, max: u64) { + self.ranges.remove(0..(max + 1)); + } + + /// Returns the set of currently pending ACK ranges + pub(super) fn ranges(&self) -> &ArrayRangeSet { + &self.ranges + } + + /// Queue an ACK if a significant number of non-ACK-eliciting packets have not yet been + /// acknowledged + /// + /// Should be called immediately before a non-probing packet is composed, when we've already + /// committed to sending a packet regardless. + pub(super) fn maybe_ack_non_eliciting(&mut self) { + // If we're going to send a packet anyway, and we've received a significant number of + // non-ACK-eliciting packets, then include an ACK to help the peer perform timely loss + // detection even if they're not sending any ACK-eliciting packets themselves. Exact + // threshold chosen somewhat arbitrarily. + const LAZY_ACK_THRESHOLD: u64 = 10; + if self.non_ack_eliciting_since_last_ack_sent > LAZY_ACK_THRESHOLD { + self.immediate_ack_required = true; + } + } +} + +/// Helper for mitigating [optimistic ACK attacks] +/// +/// A malicious peer could prompt the local application to begin a large data transfer, and then +/// send ACKs without first waiting for data to be received. This could defeat congestion control, +/// allowing the connection to consume disproportionate resources. We therefore occasionally skip +/// packet numbers, and classify any ACK referencing a skipped packet number as a transport error. +/// +/// Skipped packet numbers occur only in the application data space (where costly transfers might +/// take place) and are distributed exponentially to reflect the reduced likelihood and impact of +/// bad behavior from a peer that has been well-behaved for an extended period. +/// +/// ACKs for packet numbers that have not yet been allocated are also a transport error, but an +/// attacker with knowledge of the congestion control algorithm in use could time falsified ACKs to +/// arrive after the packets they reference are sent. +/// +/// [optimistic ACK attacks]: https://www.rfc-editor.org/rfc/rfc9000.html#name-optimistic-ack-attack +pub(super) struct PacketNumberFilter { + /// Next outgoing packet number to skip + next_skipped_packet_number: u64, + /// Most recently skipped packet number + prev_skipped_packet_number: Option, + /// Next packet number to skip is randomly selected from 2^n..2^n+1 + exponent: u32, +} + +impl PacketNumberFilter { + pub(super) fn new(rng: &mut (impl Rng + ?Sized)) -> Self { + // First skipped PN is in 0..64 + let exponent = 6; + Self { + next_skipped_packet_number: rng.random_range(0..2u64.saturating_pow(exponent)), + prev_skipped_packet_number: None, + exponent, + } + } + + #[cfg(test)] + pub(super) fn disabled() -> Self { + Self { + next_skipped_packet_number: u64::MAX, + prev_skipped_packet_number: None, + exponent: u32::MAX, + } + } + + pub(super) fn peek(&self, space: &PacketSpace) -> u64 { + let n = space.next_packet_number; + if n != self.next_skipped_packet_number { + return n; + } + n + 1 + } + + pub(super) fn allocate( + &mut self, + rng: &mut (impl Rng + ?Sized), + space: &mut PacketSpace, + ) -> u64 { + let n = space.get_tx_number(); + if n != self.next_skipped_packet_number { + return n; + } + + trace!("skipping pn {n}"); + // Skip this packet number, and choose the next one to skip + self.prev_skipped_packet_number = Some(self.next_skipped_packet_number); + let next_exponent = self.exponent.saturating_add(1); + self.next_skipped_packet_number = rng + .random_range(2u64.saturating_pow(self.exponent)..2u64.saturating_pow(next_exponent)); + self.exponent = next_exponent; + + space.get_tx_number() + } + + pub(super) fn check_ack( + &self, + space_id: SpaceId, + range: std::ops::RangeInclusive, + ) -> Result<(), TransportError> { + if space_id == SpaceId::Data + && self + .prev_skipped_packet_number + .is_some_and(|x| range.contains(&x)) + { + return Err(TransportError::PROTOCOL_VIOLATION("unsent packet acked")); + } + Ok(()) + } +} + +/// Ensures we can always fit all our ACKs in a single minimum-MTU packet with room to spare +const MAX_ACK_BLOCKS: usize = 64; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn sanity() { + let mut dedup = Dedup::new(); + assert!(!dedup.insert(0)); + assert_eq!(dedup.next, 1); + assert_eq!(dedup.window, 0b1); + assert!(dedup.insert(0)); + assert_eq!(dedup.next, 1); + assert_eq!(dedup.window, 0b1); + assert!(!dedup.insert(1)); + assert_eq!(dedup.next, 2); + assert_eq!(dedup.window, 0b11); + assert!(!dedup.insert(2)); + assert_eq!(dedup.next, 3); + assert_eq!(dedup.window, 0b111); + assert!(!dedup.insert(4)); + assert_eq!(dedup.next, 5); + assert_eq!(dedup.window, 0b11110); + assert!(!dedup.insert(7)); + assert_eq!(dedup.next, 8); + assert_eq!(dedup.window, 0b1111_0100); + assert!(dedup.insert(4)); + assert!(!dedup.insert(3)); + assert_eq!(dedup.next, 8); + assert_eq!(dedup.window, 0b1111_1100); + assert!(!dedup.insert(6)); + assert_eq!(dedup.next, 8); + assert_eq!(dedup.window, 0b1111_1101); + assert!(!dedup.insert(5)); + assert_eq!(dedup.next, 8); + assert_eq!(dedup.window, 0b1111_1111); + } + + #[test] + fn happypath() { + let mut dedup = Dedup::new(); + for i in 0..(2 * WINDOW_SIZE) { + assert!(!dedup.insert(i)); + for j in 0..=i { + assert!(dedup.insert(j)); + } + } + } + + #[test] + fn jump() { + let mut dedup = Dedup::new(); + dedup.insert(2 * WINDOW_SIZE); + assert!(dedup.insert(WINDOW_SIZE)); + assert_eq!(dedup.next, 2 * WINDOW_SIZE + 1); + assert_eq!(dedup.window, 0); + assert!(!dedup.insert(WINDOW_SIZE + 1)); + assert_eq!(dedup.next, 2 * WINDOW_SIZE + 1); + assert_eq!(dedup.window, 1 << (WINDOW_SIZE - 2)); + } + + #[test] + fn dedup_has_missing() { + let mut dedup = Dedup::new(); + + dedup.insert(0); + assert!(!dedup.missing_in_interval(0, 0)); + + dedup.insert(1); + assert!(!dedup.missing_in_interval(0, 1)); + + dedup.insert(3); + assert!(dedup.missing_in_interval(1, 3)); + + dedup.insert(4); + assert!(!dedup.missing_in_interval(3, 4)); + assert!(dedup.missing_in_interval(0, 4)); + + dedup.insert(2); + assert!(!dedup.missing_in_interval(0, 4)); + } + + #[test] + fn dedup_outside_of_window_has_missing() { + let mut dedup = Dedup::new(); + + for i in 0..140 { + dedup.insert(i); + } + + // 0 and 4 are outside of the window + assert!(!dedup.missing_in_interval(0, 4)); + dedup.insert(160); + assert!(!dedup.missing_in_interval(0, 4)); + assert!(!dedup.missing_in_interval(0, 140)); + assert!(dedup.missing_in_interval(0, 160)); + } + + #[test] + fn dedup_smallest_missing() { + let mut dedup = Dedup::new(); + + dedup.insert(0); + assert_eq!(dedup.smallest_missing_in_interval(0, 0), None); + + dedup.insert(1); + assert_eq!(dedup.smallest_missing_in_interval(0, 1), None); + + dedup.insert(5); + dedup.insert(7); + assert_eq!(dedup.smallest_missing_in_interval(0, 7), Some(2)); + assert_eq!(dedup.smallest_missing_in_interval(5, 7), Some(6)); + + dedup.insert(2); + assert_eq!(dedup.smallest_missing_in_interval(1, 7), Some(3)); + + dedup.insert(170); + dedup.insert(172); + dedup.insert(300); + assert_eq!(dedup.smallest_missing_in_interval(170, 172), None); + + dedup.insert(500); + assert_eq!(dedup.smallest_missing_in_interval(0, 500), Some(372)); + assert_eq!(dedup.smallest_missing_in_interval(0, 373), Some(372)); + assert_eq!(dedup.smallest_missing_in_interval(0, 372), None); + } + + #[test] + fn pending_acks_first_packet_is_not_considered_reordered() { + let mut acks = PendingAcks::new(); + let mut dedup = Dedup::new(); + dedup.insert(0); + acks.packet_received(Instant::now(), 0, true, &dedup); + assert!(!acks.immediate_ack_required); + } + + #[test] + fn pending_acks_after_immediate_ack_set() { + let mut acks = PendingAcks::new(); + let mut dedup = Dedup::new(); + + // Receive ack-eliciting packet + dedup.insert(0); + let now = Instant::now(); + acks.insert_one(0, now); + acks.packet_received(now, 0, true, &dedup); + + // Sanity check + assert!(!acks.ranges.is_empty()); + assert!(!acks.can_send()); + + // Can send ACK after max_ack_delay exceeded + acks.set_immediate_ack_required(); + assert!(acks.can_send()); + } + + #[test] + fn pending_acks_ack_delay() { + let mut acks = PendingAcks::new(); + let mut dedup = Dedup::new(); + + let t1 = Instant::now(); + let t2 = t1 + Duration::from_millis(2); + let t3 = t2 + Duration::from_millis(5); + assert_eq!(acks.ack_delay(t1), Duration::from_millis(0)); + assert_eq!(acks.ack_delay(t2), Duration::from_millis(0)); + assert_eq!(acks.ack_delay(t3), Duration::from_millis(0)); + + // In-order packet + dedup.insert(0); + acks.insert_one(0, t1); + acks.packet_received(t1, 0, true, &dedup); + assert_eq!(acks.ack_delay(t1), Duration::from_millis(0)); + assert_eq!(acks.ack_delay(t2), Duration::from_millis(2)); + assert_eq!(acks.ack_delay(t3), Duration::from_millis(7)); + + // Out of order (higher than expected) + dedup.insert(3); + acks.insert_one(3, t2); + acks.packet_received(t2, 3, true, &dedup); + assert_eq!(acks.ack_delay(t2), Duration::from_millis(0)); + assert_eq!(acks.ack_delay(t3), Duration::from_millis(5)); + + // Out of order (lower than expected, so previous instant is kept) + dedup.insert(2); + acks.insert_one(2, t3); + acks.packet_received(t3, 2, true, &dedup); + assert_eq!(acks.ack_delay(t3), Duration::from_millis(5)); + } + + #[test] + fn sent_packet_size() { + // The tracking state of sent packets should be minimal, and not grow + // over time. + assert!(std::mem::size_of::() <= 128); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/stats.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/stats.rs new file mode 100644 index 0000000..f62e62e --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/stats.rs @@ -0,0 +1,174 @@ +//! Connection statistics + +use crate::{Dir, Duration, frame::Frame}; + +/// Statistics about UDP datagrams transmitted or received on a connection +/// +/// All QUIC packets are carried by UDP datagrams. Hence, these statistics cover all traffic on a connection. +#[derive(Default, Debug, Copy, Clone)] +#[non_exhaustive] +pub struct UdpStats { + /// The amount of UDP datagrams observed + pub datagrams: u64, + /// The total amount of bytes which have been transferred inside UDP datagrams + pub bytes: u64, + /// The amount of I/O operations executed + /// + /// Can be less than `datagrams` when GSO, GRO, and/or batched system calls are in use. + pub ios: u64, +} + +impl UdpStats { + pub(crate) fn on_sent(&mut self, datagrams: u64, bytes: usize) { + self.datagrams += datagrams; + self.bytes += bytes as u64; + self.ios += 1; + } +} + +/// Number of frames transmitted or received of each frame type +#[derive(Default, Copy, Clone)] +#[non_exhaustive] +#[allow(missing_docs)] +pub struct FrameStats { + pub acks: u64, + pub ack_frequency: u64, + pub crypto: u64, + pub connection_close: u64, + pub data_blocked: u64, + pub datagram: u64, + pub handshake_done: u8, + pub immediate_ack: u64, + pub max_data: u64, + pub max_stream_data: u64, + pub max_streams_bidi: u64, + pub max_streams_uni: u64, + pub new_connection_id: u64, + pub new_token: u64, + pub path_challenge: u64, + pub path_response: u64, + pub ping: u64, + pub reset_stream: u64, + pub retire_connection_id: u64, + pub stream_data_blocked: u64, + pub streams_blocked_bidi: u64, + pub streams_blocked_uni: u64, + pub stop_sending: u64, + pub stream: u64, +} + +impl FrameStats { + pub(crate) fn record(&mut self, frame: &Frame) { + match frame { + Frame::Padding => {} + Frame::Ping => self.ping += 1, + Frame::Ack(_) => self.acks += 1, + Frame::ResetStream(_) => self.reset_stream += 1, + Frame::StopSending(_) => self.stop_sending += 1, + Frame::Crypto(_) => self.crypto += 1, + Frame::Datagram(_) => self.datagram += 1, + Frame::NewToken(_) => self.new_token += 1, + Frame::MaxData(_) => self.max_data += 1, + Frame::MaxStreamData { .. } => self.max_stream_data += 1, + Frame::MaxStreams { dir, .. } => { + if *dir == Dir::Bi { + self.max_streams_bidi += 1; + } else { + self.max_streams_uni += 1; + } + } + Frame::DataBlocked { .. } => self.data_blocked += 1, + Frame::Stream(_) => self.stream += 1, + Frame::StreamDataBlocked { .. } => self.stream_data_blocked += 1, + Frame::StreamsBlocked { dir, .. } => { + if *dir == Dir::Bi { + self.streams_blocked_bidi += 1; + } else { + self.streams_blocked_uni += 1; + } + } + Frame::NewConnectionId(_) => self.new_connection_id += 1, + Frame::RetireConnectionId { .. } => self.retire_connection_id += 1, + Frame::PathChallenge(_) => self.path_challenge += 1, + Frame::PathResponse(_) => self.path_response += 1, + Frame::Close(_) => self.connection_close += 1, + Frame::AckFrequency(_) => self.ack_frequency += 1, + Frame::ImmediateAck => self.immediate_ack += 1, + Frame::HandshakeDone => self.handshake_done = self.handshake_done.saturating_add(1), + } + } +} + +impl std::fmt::Debug for FrameStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FrameStats") + .field("ACK", &self.acks) + .field("ACK_FREQUENCY", &self.ack_frequency) + .field("CONNECTION_CLOSE", &self.connection_close) + .field("CRYPTO", &self.crypto) + .field("DATA_BLOCKED", &self.data_blocked) + .field("DATAGRAM", &self.datagram) + .field("HANDSHAKE_DONE", &self.handshake_done) + .field("IMMEDIATE_ACK", &self.immediate_ack) + .field("MAX_DATA", &self.max_data) + .field("MAX_STREAM_DATA", &self.max_stream_data) + .field("MAX_STREAMS_BIDI", &self.max_streams_bidi) + .field("MAX_STREAMS_UNI", &self.max_streams_uni) + .field("NEW_CONNECTION_ID", &self.new_connection_id) + .field("NEW_TOKEN", &self.new_token) + .field("PATH_CHALLENGE", &self.path_challenge) + .field("PATH_RESPONSE", &self.path_response) + .field("PING", &self.ping) + .field("RESET_STREAM", &self.reset_stream) + .field("RETIRE_CONNECTION_ID", &self.retire_connection_id) + .field("STREAM_DATA_BLOCKED", &self.stream_data_blocked) + .field("STREAMS_BLOCKED_BIDI", &self.streams_blocked_bidi) + .field("STREAMS_BLOCKED_UNI", &self.streams_blocked_uni) + .field("STOP_SENDING", &self.stop_sending) + .field("STREAM", &self.stream) + .finish() + } +} + +/// Statistics related to a transmission path +#[derive(Debug, Default, Copy, Clone)] +#[non_exhaustive] +pub struct PathStats { + /// Current best estimate of this connection's latency (round-trip-time) + pub rtt: Duration, + /// Current congestion window of the connection + pub cwnd: u64, + /// Congestion events on the connection + pub congestion_events: u64, + /// The amount of packets lost on this path + pub lost_packets: u64, + /// The amount of bytes lost on this path + pub lost_bytes: u64, + /// The amount of packets sent on this path + pub sent_packets: u64, + /// The amount of PLPMTUD probe packets sent on this path (also counted by `sent_packets`) + pub sent_plpmtud_probes: u64, + /// The amount of PLPMTUD probe packets lost on this path (ignored by `lost_packets` and + /// `lost_bytes`) + pub lost_plpmtud_probes: u64, + /// The number of times a black hole was detected in the path + pub black_holes_detected: u64, + /// Largest UDP payload size the path currently supports + pub current_mtu: u16, +} + +/// Connection statistics +#[derive(Debug, Default, Copy, Clone)] +#[non_exhaustive] +pub struct ConnectionStats { + /// Statistics about UDP datagrams transmitted on a connection + pub udp_tx: UdpStats, + /// Statistics about UDP datagrams received on a connection + pub udp_rx: UdpStats, + /// Statistics about frames transmitted on a connection + pub frame_tx: FrameStats, + /// Statistics about frames received on a connection + pub frame_rx: FrameStats, + /// Statistics related to the current transmission path + pub path: PathStats, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/mod.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/mod.rs new file mode 100644 index 0000000..3e769c7 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/mod.rs @@ -0,0 +1,528 @@ +use std::{ + collections::{BinaryHeap, hash_map}, + io, +}; + +use bytes::Bytes; +use thiserror::Error; +use tracing::trace; + +use super::spaces::{Retransmits, ThinRetransmits}; +use crate::{ + Dir, StreamId, VarInt, + connection::streams::state::{get_or_insert_recv, get_or_insert_send}, + frame, +}; + +mod recv; +use recv::Recv; +pub use recv::{Chunks, ReadError, ReadableError}; + +mod send; +pub(crate) use send::{ByteSlice, BytesArray}; +use send::{BytesSource, Send, SendState}; +pub use send::{FinishError, WriteError, Written}; + +mod state; +#[allow(unreachable_pub)] // fuzzing only +pub use state::StreamsState; + +/// Access to streams +pub struct Streams<'a> { + pub(super) state: &'a mut StreamsState, + pub(super) conn_state: &'a super::State, +} + +#[allow(clippy::needless_lifetimes)] // Needed for cfg(fuzzing) +impl<'a> Streams<'a> { + #[cfg(fuzzing)] + pub fn new(state: &'a mut StreamsState, conn_state: &'a super::State) -> Self { + Self { state, conn_state } + } + + /// Open a single stream if possible + /// + /// Returns `None` if the streams in the given direction are currently exhausted. + pub fn open(&mut self, dir: Dir) -> Option { + if self.conn_state.is_closed() { + return None; + } + + // TODO: Queue STREAM_ID_BLOCKED if this fails + if self.state.next[dir as usize] >= self.state.max[dir as usize] { + return None; + } + + self.state.next[dir as usize] += 1; + let id = StreamId::new(self.state.side, dir, self.state.next[dir as usize] - 1); + self.state.insert(false, id); + self.state.send_streams += 1; + Some(id) + } + + /// Accept a remotely initiated stream of a certain directionality, if possible + /// + /// Returns `None` if there are no new incoming streams for this connection. + /// Has no impact on the data flow-control or stream concurrency limits. + pub fn accept(&mut self, dir: Dir) -> Option { + if self.state.next_remote[dir as usize] == self.state.next_reported_remote[dir as usize] { + return None; + } + + let x = self.state.next_reported_remote[dir as usize]; + self.state.next_reported_remote[dir as usize] = x + 1; + if dir == Dir::Bi { + self.state.send_streams += 1; + } + + Some(StreamId::new(!self.state.side, dir, x)) + } + + #[cfg(fuzzing)] + pub fn state(&mut self) -> &mut StreamsState { + self.state + } + + /// The number of streams that may have unacknowledged data. + pub fn send_streams(&self) -> usize { + self.state.send_streams + } + + /// The number of remotely initiated open streams of a certain directionality. + /// + /// Includes remotely initiated streams, which have not been accepted via [`accept`](Self::accept). + /// These streams count against the respective concurrency limit reported by + /// [`Connection::max_concurrent_streams`](super::Connection::max_concurrent_streams). + pub fn remote_open_streams(&self, dir: Dir) -> u64 { + // total opened - total closed = total opened - ( total permitted - total permitted unclosed ) + self.state.next_remote[dir as usize] + - (self.state.max_remote[dir as usize] + - self.state.allocated_remote_count[dir as usize]) + } +} + +/// Access to streams +pub struct RecvStream<'a> { + pub(super) id: StreamId, + pub(super) state: &'a mut StreamsState, + pub(super) pending: &'a mut Retransmits, +} + +impl RecvStream<'_> { + /// Read from the given recv stream + /// + /// `max_length` limits the maximum size of the returned `Bytes` value; passing `usize::MAX` + /// will yield the best performance. `ordered` will make sure the returned chunk's offset will + /// have an offset exactly equal to the previously returned offset plus the previously returned + /// bytes' length. + /// + /// Yields `Ok(None)` if the stream was finished. Otherwise, yields a segment of data and its + /// offset in the stream. If `ordered` is `false`, segments may be received in any order, and + /// the `Chunk`'s `offset` field can be used to determine ordering in the caller. + /// + /// While most applications will prefer to consume stream data in order, unordered reads can + /// improve performance when packet loss occurs and data cannot be retransmitted before the flow + /// control window is filled. On any given stream, you can switch from ordered to unordered + /// reads, but ordered reads on streams that have seen previous unordered reads will return + /// `ReadError::IllegalOrderedRead`. + pub fn read(&mut self, ordered: bool) -> Result, ReadableError> { + Chunks::new(self.id, ordered, self.state, self.pending) + } + + /// Stop accepting data on the given receive stream + /// + /// Discards unread data and notifies the peer to stop transmitting. Once stopped, further + /// attempts to operate on a stream will yield `ClosedStream` errors. + pub fn stop(&mut self, error_code: VarInt) -> Result<(), ClosedStream> { + let mut entry = match self.state.recv.entry(self.id) { + hash_map::Entry::Occupied(s) => s, + hash_map::Entry::Vacant(_) => return Err(ClosedStream { _private: () }), + }; + let stream = get_or_insert_recv(self.state.stream_receive_window)(entry.get_mut()); + + let (read_credits, stop_sending) = stream.stop()?; + if stop_sending.should_transmit() { + self.pending.stop_sending.push(frame::StopSending { + id: self.id, + error_code, + }); + } + + // We need to keep stopped streams around until they're finished or reset so we can update + // connection-level flow control to account for discarded data. Otherwise, we can discard + // state immediately. + if !stream.final_offset_unknown() { + let recv = entry.remove().expect("must have recv when stopping"); + self.state.stream_recv_freed(self.id, recv); + } + + if self.state.add_read_credits(read_credits).should_transmit() { + self.pending.max_data = true; + } + + Ok(()) + } + + /// Check whether this stream has been reset by the peer, returning the reset error code if so + /// + /// After returning `Ok(Some(_))` once, stream state will be discarded and all future calls will + /// return `Err(ClosedStream)`. + pub fn received_reset(&mut self) -> Result, ClosedStream> { + let hash_map::Entry::Occupied(entry) = self.state.recv.entry(self.id) else { + return Err(ClosedStream { _private: () }); + }; + let Some(s) = entry.get().as_ref().and_then(|s| s.as_open_recv()) else { + return Ok(None); + }; + if s.stopped { + return Err(ClosedStream { _private: () }); + } + let Some(code) = s.reset_code() else { + return Ok(None); + }; + + // Clean up state after application observes the reset, since there's no reason for the + // application to attempt to read or stop the stream once it knows it's reset + let (_, recv) = entry.remove_entry(); + self.state + .stream_recv_freed(self.id, recv.expect("must have recv on reset")); + self.state.queue_max_stream_id(self.pending); + + Ok(Some(code)) + } +} + +/// Access to streams +pub struct SendStream<'a> { + pub(super) id: StreamId, + pub(super) state: &'a mut StreamsState, + pub(super) pending: &'a mut Retransmits, + pub(super) conn_state: &'a super::State, +} + +#[allow(clippy::needless_lifetimes)] // Needed for cfg(fuzzing) +impl<'a> SendStream<'a> { + #[cfg(fuzzing)] + pub fn new( + id: StreamId, + state: &'a mut StreamsState, + pending: &'a mut Retransmits, + conn_state: &'a super::State, + ) -> Self { + Self { + id, + state, + pending, + conn_state, + } + } + + /// Send data on the given stream + /// + /// Returns the number of bytes successfully written. + pub fn write(&mut self, data: &[u8]) -> Result { + Ok(self.write_source(&mut ByteSlice::from_slice(data))?.bytes) + } + + /// Send data on the given stream + /// + /// Returns the number of bytes and chunks successfully written. + /// Note that this method might also write a partial chunk. In this case + /// [`Written::chunks`] will not count this chunk as fully written. However + /// the chunk will be advanced and contain only non-written data after the call. + pub fn write_chunks(&mut self, data: &mut [Bytes]) -> Result { + self.write_source(&mut BytesArray::from_chunks(data)) + } + + fn write_source(&mut self, source: &mut B) -> Result { + if self.conn_state.is_closed() { + trace!(%self.id, "write blocked; connection draining"); + return Err(WriteError::Blocked); + } + + let limit = self.state.write_limit(); + + let max_send_data = self.state.max_send_data(self.id); + + let stream = self + .state + .send + .get_mut(&self.id) + .map(get_or_insert_send(max_send_data)) + .ok_or(WriteError::ClosedStream)?; + + if limit == 0 { + trace!( + stream = %self.id, max_data = self.state.max_data, data_sent = self.state.data_sent, + "write blocked by connection-level flow control or send window" + ); + if !stream.connection_blocked { + stream.connection_blocked = true; + self.state.connection_blocked.push(self.id); + } + return Err(WriteError::Blocked); + } + + let was_pending = stream.is_pending(); + let written = stream.write(source, limit)?; + self.state.data_sent += written.bytes as u64; + self.state.unacked_data += written.bytes as u64; + trace!(stream = %self.id, "wrote {} bytes", written.bytes); + if !was_pending { + self.state.pending.push_pending(self.id, stream.priority); + } + Ok(written) + } + + /// Check if this stream was stopped, get the reason if it was + pub fn stopped(&self) -> Result, ClosedStream> { + match self.state.send.get(&self.id).as_ref() { + Some(Some(s)) => Ok(s.stop_reason), + Some(None) => Ok(None), + None => Err(ClosedStream { _private: () }), + } + } + + /// Finish a send stream, signalling that no more data will be sent. + /// + /// If this fails, no [`StreamEvent::Finished`] will be generated. + /// + /// [`StreamEvent::Finished`]: crate::StreamEvent::Finished + pub fn finish(&mut self) -> Result<(), FinishError> { + let max_send_data = self.state.max_send_data(self.id); + let stream = self + .state + .send + .get_mut(&self.id) + .map(get_or_insert_send(max_send_data)) + .ok_or(FinishError::ClosedStream)?; + + let was_pending = stream.is_pending(); + stream.finish()?; + if !was_pending { + self.state.pending.push_pending(self.id, stream.priority); + } + + Ok(()) + } + + /// Abandon transmitting data on a stream + /// + /// # Panics + /// - when applied to a receive stream + pub fn reset(&mut self, error_code: VarInt) -> Result<(), ClosedStream> { + let max_send_data = self.state.max_send_data(self.id); + let stream = self + .state + .send + .get_mut(&self.id) + .map(get_or_insert_send(max_send_data)) + .ok_or(ClosedStream { _private: () })?; + + if matches!(stream.state, SendState::ResetSent) { + // Redundant reset call + return Err(ClosedStream { _private: () }); + } + + // Restore the portion of the send window consumed by the data that we aren't about to + // send. We leave flow control alone because the peer's responsible for issuing additional + // credit based on the final offset communicated in the RESET_STREAM frame we send. + self.state.unacked_data -= stream.pending.unacked(); + stream.reset(); + self.pending.reset_stream.push((self.id, error_code)); + + // Don't reopen an already-closed stream we haven't forgotten yet + Ok(()) + } + + /// Set the priority of a stream + /// + /// # Panics + /// - when applied to a receive stream + pub fn set_priority(&mut self, priority: i32) -> Result<(), ClosedStream> { + let max_send_data = self.state.max_send_data(self.id); + let stream = self + .state + .send + .get_mut(&self.id) + .map(get_or_insert_send(max_send_data)) + .ok_or(ClosedStream { _private: () })?; + + stream.priority = priority; + Ok(()) + } + + /// Get the priority of a stream + /// + /// # Panics + /// - when applied to a receive stream + pub fn priority(&self) -> Result { + let stream = self + .state + .send + .get(&self.id) + .ok_or(ClosedStream { _private: () })?; + + Ok(stream.as_ref().map(|s| s.priority).unwrap_or_default()) + } +} + +/// A queue of streams with pending outgoing data, sorted by priority +struct PendingStreamsQueue { + streams: BinaryHeap, + /// The next stream to write out. This is `Some` when `TransportConfig::send_fairness(false)` and writing a stream is + /// interrupted while the stream still has some pending data. See `reinsert_pending()`. + next: Option, + /// A monotonically decreasing counter, used to implement round-robin scheduling for streams of the same priority. + /// Underflowing is not a practical concern, as it is initialized to u64::MAX and only decremented by 1 in `push_pending` + recency: u64, +} + +impl PendingStreamsQueue { + fn new() -> Self { + Self { + streams: BinaryHeap::new(), + next: None, + recency: u64::MAX, + } + } + + /// Reinsert a stream that was pending and still contains unsent data. + fn reinsert_pending(&mut self, id: StreamId, priority: i32) { + assert!(self.next.is_none()); + + self.next = Some(PendingStream { + priority, + recency: self.recency, // the value here doesn't really matter + id, + }); + } + + /// Push a pending stream ID with the given priority, queued after any already-queued streams for the priority + fn push_pending(&mut self, id: StreamId, priority: i32) { + // Note that in the case where fairness is disabled, if we have a reinserted stream we don't + // bump it even if priority > next.priority. In order to minimize fragmentation we + // always try to complete a stream once part of it has been written. + + // As the recency counter is monotonically decreasing, we know that using its value to sort this stream will queue it + // after all other queued streams of the same priority. + // This is enough to implement round-robin scheduling for streams that are still pending even after being handled, + // as in that case they are removed from the `BinaryHeap`, handled, and then immediately reinserted. + self.recency -= 1; + self.streams.push(PendingStream { + priority, + recency: self.recency, + id, + }); + } + + fn pop(&mut self) -> Option { + self.next.take().or_else(|| self.streams.pop()) + } + + fn clear(&mut self) { + self.next = None; + self.streams.clear(); + } + + fn iter(&self) -> impl Iterator { + self.next.iter().chain(self.streams.iter()) + } + + #[cfg(test)] + fn len(&self) -> usize { + self.streams.len() + self.next.is_some() as usize + } +} + +/// The [`StreamId`] of a stream with pending data queued, ordered by its priority and recency +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +struct PendingStream { + /// The priority of the stream + // Note that this field should be kept above the `recency` field, in order for the `Ord` derive to be correct + // (See https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#derivable) + priority: i32, + /// A tie-breaker for streams of the same priority, used to improve fairness by implementing round-robin scheduling: + /// Larger values are prioritized, so it is initialised to `u64::MAX`, and when a stream writes data, we know + /// that it currently has the highest recency value, so it is deprioritized by setting its recency to 1 less than the + /// previous lowest recency value, such that all other streams of this priority will get processed once before we get back + /// round to this one + recency: u64, + /// The ID of the stream + // The way this type is used ensures that every instance has a unique `recency` value, so this field should be kept below + // the `priority` and `recency` fields, so that it does not interfere with the behaviour of the `Ord` derive + id: StreamId, +} + +/// Application events about streams +#[derive(Debug, PartialEq, Eq)] +pub enum StreamEvent { + /// One or more new streams has been opened and might be readable + Opened { + /// Directionality for which streams have been opened + dir: Dir, + }, + /// A currently open stream likely has data or errors waiting to be read + Readable { + /// Which stream is now readable + id: StreamId, + }, + /// A formerly write-blocked stream might be ready for a write or have been stopped + /// + /// Only generated for streams that are currently open. + Writable { + /// Which stream is now writable + id: StreamId, + }, + /// A finished stream has been fully acknowledged or stopped + Finished { + /// Which stream has been finished + id: StreamId, + }, + /// The peer asked us to stop sending on an outgoing stream + Stopped { + /// Which stream has been stopped + id: StreamId, + /// Error code supplied by the peer + error_code: VarInt, + }, + /// At least one new stream of a certain directionality may be opened + Available { + /// Directionality for which streams are newly available + dir: Dir, + }, +} + +/// Indicates whether a frame needs to be transmitted +/// +/// This type wraps around bool and uses the `#[must_use]` attribute in order +/// to prevent accidental loss of the frame transmission requirement. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +#[must_use = "A frame might need to be enqueued"] +pub struct ShouldTransmit(bool); + +impl ShouldTransmit { + /// Returns whether a frame should be transmitted + pub fn should_transmit(self) -> bool { + self.0 + } +} + +/// Error indicating that a stream has not been opened or has already been finished or reset +#[derive(Debug, Default, Error, Clone, PartialEq, Eq)] +#[error("closed stream")] +pub struct ClosedStream { + _private: (), +} + +impl From for io::Error { + fn from(x: ClosedStream) -> Self { + Self::new(io::ErrorKind::NotConnected, x) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum StreamHalf { + Send, + Recv, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/recv.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/recv.rs new file mode 100644 index 0000000..1aee535 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/recv.rs @@ -0,0 +1,543 @@ +use std::collections::hash_map::Entry; +use std::mem; + +use thiserror::Error; +use tracing::debug; + +use super::state::get_or_insert_recv; +use super::{ClosedStream, Retransmits, ShouldTransmit, StreamId, StreamsState}; +use crate::connection::assembler::{Assembler, Chunk, IllegalOrderedRead}; +use crate::connection::streams::state::StreamRecv; +use crate::{TransportError, VarInt, frame}; + +#[derive(Debug, Default)] +pub(super) struct Recv { + // NB: when adding or removing fields, remember to update `reinit`. + state: RecvState, + pub(super) assembler: Assembler, + sent_max_stream_data: u64, + pub(super) end: u64, + pub(super) stopped: bool, +} + +impl Recv { + pub(super) fn new(initial_max_data: u64) -> Box { + Box::new(Self { + state: RecvState::default(), + assembler: Assembler::new(), + sent_max_stream_data: initial_max_data, + end: 0, + stopped: false, + }) + } + + /// Reset to the initial state + pub(super) fn reinit(&mut self, initial_max_data: u64) { + self.state = RecvState::default(); + self.assembler.reinit(); + self.sent_max_stream_data = initial_max_data; + self.end = 0; + self.stopped = false; + } + + /// Process a STREAM frame + /// + /// Return value is `(number_of_new_bytes_ingested, stream_is_closed)` + pub(super) fn ingest( + &mut self, + frame: frame::Stream, + payload_len: usize, + received: u64, + max_data: u64, + ) -> Result<(u64, bool), TransportError> { + let end = frame.offset + frame.data.len() as u64; + if end >= 2u64.pow(62) { + return Err(TransportError::FLOW_CONTROL_ERROR( + "maximum stream offset too large", + )); + } + + if let Some(final_offset) = self.final_offset() { + if end > final_offset || (frame.fin && end != final_offset) { + debug!(end, final_offset, "final size error"); + return Err(TransportError::FINAL_SIZE_ERROR("")); + } + } + + let new_bytes = self.credit_consumed_by(end, received, max_data)?; + + // Stopped streams don't need to wait for the actual data, they just need to know + // how much there was. + if frame.fin && !self.stopped { + if let RecvState::Recv { ref mut size } = self.state { + *size = Some(end); + } + } + + self.end = self.end.max(end); + // Don't bother storing data or releasing stream-level flow control credit if the stream's + // already stopped + if !self.stopped { + self.assembler.insert(frame.offset, frame.data, payload_len); + } + + Ok((new_bytes, frame.fin && self.stopped)) + } + + pub(super) fn stop(&mut self) -> Result<(u64, ShouldTransmit), ClosedStream> { + if self.stopped { + return Err(ClosedStream { _private: () }); + } + + self.stopped = true; + self.assembler.clear(); + // Issue flow control credit for unread data + let read_credits = self.end - self.assembler.bytes_read(); + // This may send a spurious STOP_SENDING if we've already received all data, but it's a bit + // fiddly to distinguish that from the case where we've received a FIN but are missing some + // data that the peer might still be trying to retransmit, in which case a STOP_SENDING is + // still useful. + Ok((read_credits, ShouldTransmit(self.is_receiving()))) + } + + /// Returns the window that should be advertised in a `MAX_STREAM_DATA` frame + /// + /// The method returns a tuple which consists of the window that should be + /// announced, as well as a boolean parameter which indicates if a new + /// transmission of the value is recommended. If the boolean value is + /// `false` the new window should only be transmitted if a previous transmission + /// had failed. + pub(super) fn max_stream_data(&mut self, stream_receive_window: u64) -> (u64, ShouldTransmit) { + let max_stream_data = self.assembler.bytes_read() + stream_receive_window; + + // Only announce a window update if it's significant enough + // to make it worthwhile sending a MAX_STREAM_DATA frame. + // We use here a fraction of the configured stream receive window to make + // the decision, and accommodate for streams using bigger windows requiring + // less updates. A fixed size would also work - but it would need to be + // smaller than `stream_receive_window` in order to make sure the stream + // does not get stuck. + let diff = max_stream_data - self.sent_max_stream_data; + let transmit = self.can_send_flow_control() && diff >= (stream_receive_window / 8); + (max_stream_data, ShouldTransmit(transmit)) + } + + /// Records that a `MAX_STREAM_DATA` announcing a certain window was sent + /// + /// This will suppress enqueuing further `MAX_STREAM_DATA` frames unless + /// either the previous transmission was not acknowledged or the window + /// further increased. + pub(super) fn record_sent_max_stream_data(&mut self, sent_value: u64) { + if sent_value > self.sent_max_stream_data { + self.sent_max_stream_data = sent_value; + } + } + + /// Whether the total amount of data that the peer will send on this stream is unknown + /// + /// True until we've received either a reset or the final frame. + /// + /// Implies that the sender might benefit from stream-level flow control updates, and we might + /// need to issue connection-level flow control updates due to flow control budget use by this + /// stream in the future, even if it's been stopped. + pub(super) fn final_offset_unknown(&self) -> bool { + matches!(self.state, RecvState::Recv { size: None }) + } + + /// Whether stream-level flow control updates should be sent for this stream + pub(super) fn can_send_flow_control(&self) -> bool { + // Stream-level flow control is redundant if the sender has already sent the whole stream, + // and moot if we no longer want data on this stream. + self.final_offset_unknown() && !self.stopped + } + + /// Whether data is still being accepted from the peer + pub(super) fn is_receiving(&self) -> bool { + matches!(self.state, RecvState::Recv { .. }) + } + + fn final_offset(&self) -> Option { + match self.state { + RecvState::Recv { size } => size, + RecvState::ResetRecvd { size, .. } => Some(size), + } + } + + /// Returns `false` iff the reset was redundant + pub(super) fn reset( + &mut self, + error_code: VarInt, + final_offset: VarInt, + received: u64, + max_data: u64, + ) -> Result { + // Validate final_offset + if let Some(offset) = self.final_offset() { + if offset != final_offset.into_inner() { + return Err(TransportError::FINAL_SIZE_ERROR("inconsistent value")); + } + } else if self.end > u64::from(final_offset) { + return Err(TransportError::FINAL_SIZE_ERROR( + "lower than high water mark", + )); + } + self.credit_consumed_by(final_offset.into(), received, max_data)?; + + if matches!(self.state, RecvState::ResetRecvd { .. }) { + return Ok(false); + } + self.state = RecvState::ResetRecvd { + size: final_offset.into(), + error_code, + }; + // Nuke buffers so that future reads fail immediately, which ensures future reads don't + // issue flow control credit redundant to that already issued. We could instead special-case + // reset streams during read, but it's unclear if there's any benefit to retaining data for + // reset streams. + self.assembler.clear(); + Ok(true) + } + + pub(super) fn reset_code(&self) -> Option { + match self.state { + RecvState::ResetRecvd { error_code, .. } => Some(error_code), + _ => None, + } + } + + /// Compute the amount of flow control credit consumed, or return an error if more was consumed + /// than issued + fn credit_consumed_by( + &self, + offset: u64, + received: u64, + max_data: u64, + ) -> Result { + let prev_end = self.end; + let new_bytes = offset.saturating_sub(prev_end); + if offset > self.sent_max_stream_data || received + new_bytes > max_data { + debug!( + received, + new_bytes, + max_data, + offset, + stream_max_data = self.sent_max_stream_data, + "flow control error" + ); + return Err(TransportError::FLOW_CONTROL_ERROR("")); + } + + Ok(new_bytes) + } +} + +/// Chunks returned from [`RecvStream::read()`][crate::RecvStream::read]. +/// +/// ### Note: Finalization Needed +/// Bytes read from the stream are not released from the congestion window until +/// either [`Self::finalize()`] is called, or this type is dropped. +/// +/// It is recommended that you call [`Self::finalize()`] because it returns a flag +/// telling you whether reading from the stream has resulted in the need to transmit a packet. +/// +/// If this type is leaked, the stream will remain blocked on the remote peer until +/// another read from the stream is done. +pub struct Chunks<'a> { + id: StreamId, + ordered: bool, + streams: &'a mut StreamsState, + pending: &'a mut Retransmits, + state: ChunksState, + read: u64, +} + +impl<'a> Chunks<'a> { + pub(super) fn new( + id: StreamId, + ordered: bool, + streams: &'a mut StreamsState, + pending: &'a mut Retransmits, + ) -> Result { + let mut entry = match streams.recv.entry(id) { + Entry::Occupied(entry) => entry, + Entry::Vacant(_) => return Err(ReadableError::ClosedStream), + }; + + let mut recv = + match get_or_insert_recv(streams.stream_receive_window)(entry.get_mut()).stopped { + true => return Err(ReadableError::ClosedStream), + false => entry.remove().unwrap().into_inner(), // this can't fail due to the previous get_or_insert_with + }; + + recv.assembler.ensure_ordering(ordered)?; + Ok(Self { + id, + ordered, + streams, + pending, + state: ChunksState::Readable(recv), + read: 0, + }) + } + + /// Next + /// + /// Should call finalize() when done calling this. + pub fn next(&mut self, max_length: usize) -> Result, ReadError> { + let rs = match self.state { + ChunksState::Readable(ref mut rs) => rs, + ChunksState::Reset(error_code) => { + return Err(ReadError::Reset(error_code)); + } + ChunksState::Finished => { + return Ok(None); + } + ChunksState::Finalized => panic!("must not call next() after finalize()"), + }; + + if let Some(chunk) = rs.assembler.read(max_length, self.ordered) { + self.read += chunk.bytes.len() as u64; + return Ok(Some(chunk)); + } + + match rs.state { + RecvState::ResetRecvd { error_code, .. } => { + debug_assert_eq!(self.read, 0, "reset streams have empty buffers"); + let state = mem::replace(&mut self.state, ChunksState::Reset(error_code)); + // At this point if we have `rs` self.state must be `ChunksState::Readable` + let recv = match state { + ChunksState::Readable(recv) => StreamRecv::Open(recv), + _ => unreachable!("state must be ChunkState::Readable"), + }; + self.streams.stream_recv_freed(self.id, recv); + Err(ReadError::Reset(error_code)) + } + RecvState::Recv { size } => { + if size == Some(rs.end) && rs.assembler.bytes_read() == rs.end { + let state = mem::replace(&mut self.state, ChunksState::Finished); + // At this point if we have `rs` self.state must be `ChunksState::Readable` + let recv = match state { + ChunksState::Readable(recv) => StreamRecv::Open(recv), + _ => unreachable!("state must be ChunkState::Readable"), + }; + self.streams.stream_recv_freed(self.id, recv); + Ok(None) + } else { + // We don't need a distinct `ChunksState` variant for a blocked stream because + // retrying a read harmlessly re-traces our steps back to returning + // `Err(Blocked)` again. The buffers can't refill and the stream's own state + // can't change so long as this `Chunks` exists. + Err(ReadError::Blocked) + } + } + } + } + + /// Mark the read data as consumed from the stream. + /// + /// The number of read bytes will be released from the congestion window, + /// allowing the remote peer to send more data if it was previously blocked. + /// + /// If [`ShouldTransmit::should_transmit()`] returns `true`, + /// a packet needs to be sent to the peer informing them that the stream is unblocked. + /// This means that you should call [`Connection::poll_transmit()`][crate::Connection::poll_transmit] + /// and send the returned packet as soon as is reasonable, to unblock the remote peer. + pub fn finalize(mut self) -> ShouldTransmit { + self.finalize_inner() + } + + fn finalize_inner(&mut self) -> ShouldTransmit { + let state = mem::replace(&mut self.state, ChunksState::Finalized); + if let ChunksState::Finalized = state { + // Noop on repeated calls + return ShouldTransmit(false); + } + + // We issue additional stream ID credit after the application is notified that a previously + // open stream has finished or been reset and we've therefore disposed of its state, as + // recorded by `stream_freed` calls in `next`. + let mut should_transmit = self.streams.queue_max_stream_id(self.pending); + + // If the stream hasn't finished, we may need to issue stream-level flow control credit + if let ChunksState::Readable(mut rs) = state { + let (_, max_stream_data) = rs.max_stream_data(self.streams.stream_receive_window); + should_transmit |= max_stream_data.0; + if max_stream_data.0 { + self.pending.max_stream_data.insert(self.id); + } + // Return the stream to storage for future use + self.streams + .recv + .insert(self.id, Some(StreamRecv::Open(rs))); + } + + // Issue connection-level flow control credit for any data we read regardless of state + let max_data = self.streams.add_read_credits(self.read); + self.pending.max_data |= max_data.0; + should_transmit |= max_data.0; + ShouldTransmit(should_transmit) + } +} + +impl Drop for Chunks<'_> { + fn drop(&mut self) { + let _ = self.finalize_inner(); + } +} + +enum ChunksState { + Readable(Box), + Reset(VarInt), + Finished, + Finalized, +} + +/// Errors triggered when reading from a recv stream +#[derive(Debug, Error, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum ReadError { + /// No more data is currently available on this stream. + /// + /// If more data on this stream is received from the peer, an `Event::StreamReadable` will be + /// generated for this stream, indicating that retrying the read might succeed. + #[error("blocked")] + Blocked, + /// The peer abandoned transmitting data on this stream. + /// + /// Carries an application-defined error code. + #[error("reset by peer: code {0}")] + Reset(VarInt), +} + +/// Errors triggered when opening a recv stream for reading +#[derive(Debug, Error, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum ReadableError { + /// The stream has not been opened or was already stopped, finished, or reset + #[error("closed stream")] + ClosedStream, + /// Attempted an ordered read following an unordered read + /// + /// Performing an unordered read allows discontinuities to arise in the receive buffer of a + /// stream which cannot be recovered, making further ordered reads impossible. + #[error("ordered read after unordered read")] + IllegalOrderedRead, +} + +impl From for ReadableError { + fn from(_: IllegalOrderedRead) -> Self { + Self::IllegalOrderedRead + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum RecvState { + Recv { size: Option }, + ResetRecvd { size: u64, error_code: VarInt }, +} + +impl Default for RecvState { + fn default() -> Self { + Self::Recv { size: None } + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + + use crate::{Dir, Side}; + + use super::*; + + #[test] + fn reordered_frames_while_stopped() { + const INITIAL_BYTES: u64 = 3; + const INITIAL_OFFSET: u64 = 3; + const RECV_WINDOW: u64 = 8; + let mut s = Recv::new(RECV_WINDOW); + let mut data_recvd = 0; + // Receive bytes 3..6 + let (new_bytes, is_closed) = s + .ingest( + frame::Stream { + id: StreamId::new(Side::Client, Dir::Uni, 0), + offset: INITIAL_OFFSET, + fin: false, + data: Bytes::from_static(&[0; INITIAL_BYTES as usize]), + }, + 123, + data_recvd, + data_recvd + 1024, + ) + .unwrap(); + data_recvd += new_bytes; + assert_eq!(new_bytes, INITIAL_OFFSET + INITIAL_BYTES); + assert!(!is_closed); + + let (credits, transmit) = s.stop().unwrap(); + assert!(transmit.should_transmit()); + assert_eq!( + credits, + INITIAL_OFFSET + INITIAL_BYTES, + "full connection flow control credit is issued by stop" + ); + + let (max_stream_data, transmit) = s.max_stream_data(RECV_WINDOW); + assert!(!transmit.should_transmit()); + assert_eq!( + max_stream_data, RECV_WINDOW, + "stream flow control credit isn't issued by stop" + ); + + // Receive byte 7 + let (new_bytes, is_closed) = s + .ingest( + frame::Stream { + id: StreamId::new(Side::Client, Dir::Uni, 0), + offset: RECV_WINDOW - 1, + fin: false, + data: Bytes::from_static(&[0; 1]), + }, + 123, + data_recvd, + data_recvd + 1024, + ) + .unwrap(); + data_recvd += new_bytes; + assert_eq!(new_bytes, RECV_WINDOW - (INITIAL_OFFSET + INITIAL_BYTES)); + assert!(!is_closed); + + let (max_stream_data, transmit) = s.max_stream_data(RECV_WINDOW); + assert!(!transmit.should_transmit()); + assert_eq!( + max_stream_data, RECV_WINDOW, + "stream flow control credit isn't issued after stop" + ); + + // Receive bytes 0..3 + let (new_bytes, is_closed) = s + .ingest( + frame::Stream { + id: StreamId::new(Side::Client, Dir::Uni, 0), + offset: 0, + fin: false, + data: Bytes::from_static(&[0; INITIAL_OFFSET as usize]), + }, + 123, + data_recvd, + data_recvd + 1024, + ) + .unwrap(); + assert_eq!( + new_bytes, 0, + "reordered frames don't issue connection-level flow control for stopped streams" + ); + assert!(!is_closed); + + let (max_stream_data, transmit) = s.max_stream_data(RECV_WINDOW); + assert!(!transmit.should_transmit()); + assert_eq!( + max_stream_data, RECV_WINDOW, + "stream flow control credit isn't issued after stop" + ); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/send.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/send.rs new file mode 100644 index 0000000..7b3db80 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/send.rs @@ -0,0 +1,402 @@ +use bytes::Bytes; +use thiserror::Error; + +use crate::{VarInt, connection::send_buffer::SendBuffer, frame}; + +#[derive(Debug)] +pub(super) struct Send { + pub(super) max_data: u64, + pub(super) state: SendState, + pub(super) pending: SendBuffer, + pub(super) priority: i32, + /// Whether a frame containing a FIN bit must be transmitted, even if we don't have any new data + pub(super) fin_pending: bool, + /// Whether this stream is in the `connection_blocked` list of `Streams` + pub(super) connection_blocked: bool, + /// The reason the peer wants us to stop, if `STOP_SENDING` was received + pub(super) stop_reason: Option, +} + +impl Send { + pub(super) fn new(max_data: VarInt) -> Box { + Box::new(Self { + max_data: max_data.into(), + state: SendState::Ready, + pending: SendBuffer::new(), + priority: 0, + fin_pending: false, + connection_blocked: false, + stop_reason: None, + }) + } + + /// Whether the stream has been reset + pub(super) fn is_reset(&self) -> bool { + matches!(self.state, SendState::ResetSent) + } + + pub(super) fn finish(&mut self) -> Result<(), FinishError> { + if let Some(error_code) = self.stop_reason { + Err(FinishError::Stopped(error_code)) + } else if self.state == SendState::Ready { + self.state = SendState::DataSent { + finish_acked: false, + }; + self.fin_pending = true; + Ok(()) + } else { + Err(FinishError::ClosedStream) + } + } + + pub(super) fn write( + &mut self, + source: &mut S, + limit: u64, + ) -> Result { + if !self.is_writable() { + return Err(WriteError::ClosedStream); + } + if let Some(error_code) = self.stop_reason { + return Err(WriteError::Stopped(error_code)); + } + let budget = self.max_data - self.pending.offset(); + if budget == 0 { + return Err(WriteError::Blocked); + } + let mut limit = limit.min(budget) as usize; + + let mut result = Written::default(); + loop { + let (chunk, chunks_consumed) = source.pop_chunk(limit); + result.chunks += chunks_consumed; + result.bytes += chunk.len(); + + if chunk.is_empty() { + break; + } + + limit -= chunk.len(); + self.pending.write(chunk); + } + + Ok(result) + } + + /// Update stream state due to a reset sent by the local application + pub(super) fn reset(&mut self) { + use SendState::*; + if let DataSent { .. } | Ready = self.state { + self.state = ResetSent; + } + } + + /// Handle STOP_SENDING + /// + /// Returns true if the stream was stopped due to this frame, and false + /// if it had been stopped before + pub(super) fn try_stop(&mut self, error_code: VarInt) -> bool { + if self.stop_reason.is_none() { + self.stop_reason = Some(error_code); + true + } else { + false + } + } + + /// Returns whether the stream has been finished and all data has been acknowledged by the peer + pub(super) fn ack(&mut self, frame: frame::StreamMeta) -> bool { + self.pending.ack(frame.offsets); + match self.state { + SendState::DataSent { + ref mut finish_acked, + } => { + *finish_acked |= frame.fin; + *finish_acked && self.pending.is_fully_acked() + } + _ => false, + } + } + + /// Handle increase to stream-level flow control limit + /// + /// Returns whether the stream was unblocked + pub(super) fn increase_max_data(&mut self, offset: u64) -> bool { + if offset <= self.max_data || self.state != SendState::Ready { + return false; + } + let was_blocked = self.pending.offset() == self.max_data; + self.max_data = offset; + was_blocked + } + + pub(super) fn offset(&self) -> u64 { + self.pending.offset() + } + + pub(super) fn is_pending(&self) -> bool { + self.pending.has_unsent_data() || self.fin_pending + } + + pub(super) fn is_writable(&self) -> bool { + matches!(self.state, SendState::Ready) + } +} + +/// A [`BytesSource`] implementation for `&'a mut [Bytes]` +/// +/// The type allows to dequeue [`Bytes`] chunks from an array of chunks, up to +/// a configured limit. +pub(crate) struct BytesArray<'a> { + /// The wrapped slice of `Bytes` + chunks: &'a mut [Bytes], + /// The amount of chunks consumed from this source + consumed: usize, +} + +impl<'a> BytesArray<'a> { + pub(crate) fn from_chunks(chunks: &'a mut [Bytes]) -> Self { + Self { + chunks, + consumed: 0, + } + } +} + +impl BytesSource for BytesArray<'_> { + fn pop_chunk(&mut self, limit: usize) -> (Bytes, usize) { + // The loop exists to skip empty chunks while still marking them as + // consumed + let mut chunks_consumed = 0; + + while self.consumed < self.chunks.len() { + let chunk = &mut self.chunks[self.consumed]; + + if chunk.len() <= limit { + let chunk = std::mem::take(chunk); + self.consumed += 1; + chunks_consumed += 1; + if chunk.is_empty() { + continue; + } + return (chunk, chunks_consumed); + } else if limit > 0 { + let chunk = chunk.split_to(limit); + return (chunk, chunks_consumed); + } else { + break; + } + } + + (Bytes::new(), chunks_consumed) + } +} + +/// A [`BytesSource`] implementation for `&[u8]` +/// +/// The type allows to dequeue a single [`Bytes`] chunk, which will be lazily +/// created from a reference. This allows to defer the allocation until it is +/// known how much data needs to be copied. +pub(crate) struct ByteSlice<'a> { + /// The wrapped byte slice + data: &'a [u8], +} + +impl<'a> ByteSlice<'a> { + pub(crate) fn from_slice(data: &'a [u8]) -> Self { + Self { data } + } +} + +impl BytesSource for ByteSlice<'_> { + fn pop_chunk(&mut self, limit: usize) -> (Bytes, usize) { + let limit = limit.min(self.data.len()); + if limit == 0 { + return (Bytes::new(), 0); + } + + let chunk = Bytes::from(self.data[..limit].to_owned()); + self.data = &self.data[chunk.len()..]; + + let chunks_consumed = usize::from(self.data.is_empty()); + (chunk, chunks_consumed) + } +} + +/// A source of one or more buffers which can be converted into `Bytes` buffers on demand +/// +/// The purpose of this data type is to defer conversion as long as possible, +/// so that no heap allocation is required in case no data is writable. +pub(super) trait BytesSource { + /// Returns the next chunk from the source of owned chunks. + /// + /// This method will consume parts of the source. + /// Calling it will yield `Bytes` elements up to the configured `limit`. + /// + /// The method returns a tuple: + /// - The first item is the yielded `Bytes` element. The element will be + /// empty if the limit is zero or no more data is available. + /// - The second item returns how many complete chunks inside the source had + /// had been consumed. This can be less than 1, if a chunk inside the + /// source had been truncated in order to adhere to the limit. It can also + /// be more than 1, if zero-length chunks had been skipped. + fn pop_chunk(&mut self, limit: usize) -> (Bytes, usize); +} + +/// Indicates how many bytes and chunks had been transferred in a write operation +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct Written { + /// The amount of bytes which had been written + pub bytes: usize, + /// The amount of full chunks which had been written + /// + /// If a chunk was only partially written, it will not be counted by this field. + pub chunks: usize, +} + +/// Errors triggered while writing to a send stream +#[derive(Debug, Error, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum WriteError { + /// The peer is not able to accept additional data, or the connection is congested. + /// + /// If the peer issues additional flow control credit, a [`StreamEvent::Writable`] event will + /// be generated, indicating that retrying the write might succeed. + /// + /// [`StreamEvent::Writable`]: crate::StreamEvent::Writable + #[error("unable to accept further writes")] + Blocked, + /// The peer is no longer accepting data on this stream, and it has been implicitly reset. The + /// stream cannot be finished or further written to. + /// + /// Carries an application-defined error code. + /// + /// [`StreamEvent::Finished`]: crate::StreamEvent::Finished + #[error("stopped by peer: code {0}")] + Stopped(VarInt), + /// The stream has not been opened or has already been finished or reset + #[error("closed stream")] + ClosedStream, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) enum SendState { + /// Sending new data + Ready, + /// Stream was finished; now sending retransmits only + DataSent { finish_acked: bool }, + /// Sent RESET + ResetSent, +} + +/// Reasons why attempting to finish a stream might fail +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum FinishError { + /// The peer is no longer accepting data on this stream. No + /// [`StreamEvent::Finished`] event will be emitted for this stream. + /// + /// Carries an application-defined error code. + /// + /// [`StreamEvent::Finished`]: crate::StreamEvent::Finished + #[error("stopped by peer: code {0}")] + Stopped(VarInt), + /// The stream has not been opened or was already finished or reset + #[error("closed stream")] + ClosedStream, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bytes_array() { + let full = b"Hello World 123456789 ABCDEFGHJIJKLMNOPQRSTUVWXYZ".to_owned(); + for limit in 0..full.len() { + let mut chunks = [ + Bytes::from_static(b""), + Bytes::from_static(b"Hello "), + Bytes::from_static(b"Wo"), + Bytes::from_static(b""), + Bytes::from_static(b"r"), + Bytes::from_static(b"ld"), + Bytes::from_static(b""), + Bytes::from_static(b" 12345678"), + Bytes::from_static(b"9 ABCDE"), + Bytes::from_static(b"F"), + Bytes::from_static(b"GHJIJKLMNOPQRSTUVWXYZ"), + ]; + let num_chunks = chunks.len(); + let last_chunk_len = chunks[chunks.len() - 1].len(); + + let mut array = BytesArray::from_chunks(&mut chunks); + + let mut buf = Vec::new(); + let mut chunks_popped = 0; + let mut chunks_consumed = 0; + let mut remaining = limit; + loop { + let (chunk, consumed) = array.pop_chunk(remaining); + chunks_consumed += consumed; + + if !chunk.is_empty() { + buf.extend_from_slice(&chunk); + remaining -= chunk.len(); + chunks_popped += 1; + } else { + break; + } + } + + assert_eq!(&buf[..], &full[..limit]); + + if limit == full.len() { + // Full consumption of the last chunk + assert_eq!(chunks_consumed, num_chunks); + // Since there are empty chunks, we consume more than there are popped + assert_eq!(chunks_consumed, chunks_popped + 3); + } else if limit > full.len() - last_chunk_len { + // Partial consumption of the last chunk + assert_eq!(chunks_consumed, num_chunks - 1); + assert_eq!(chunks_consumed, chunks_popped + 2); + } + } + } + + #[test] + fn byte_slice() { + let full = b"Hello World 123456789 ABCDEFGHJIJKLMNOPQRSTUVWXYZ".to_owned(); + for limit in 0..full.len() { + let mut array = ByteSlice::from_slice(&full[..]); + + let mut buf = Vec::new(); + let mut chunks_popped = 0; + let mut chunks_consumed = 0; + let mut remaining = limit; + loop { + let (chunk, consumed) = array.pop_chunk(remaining); + chunks_consumed += consumed; + + if !chunk.is_empty() { + buf.extend_from_slice(&chunk); + remaining -= chunk.len(); + chunks_popped += 1; + } else { + break; + } + } + + assert_eq!(&buf[..], &full[..limit]); + if limit != 0 { + assert_eq!(chunks_popped, 1); + } else { + assert_eq!(chunks_popped, 0); + } + + if limit == full.len() { + assert_eq!(chunks_consumed, 1); + } else { + assert_eq!(chunks_consumed, 0); + } + } + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/state.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/state.rs new file mode 100644 index 0000000..09644fe --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/streams/state.rs @@ -0,0 +1,2102 @@ +use std::{ + collections::{VecDeque, hash_map}, + convert::TryFrom, + mem, +}; + +use bytes::BufMut; +use rustc_hash::FxHashMap; +use tracing::{debug, trace}; + +use super::{ + PendingStreamsQueue, Recv, Retransmits, Send, SendState, ShouldTransmit, StreamEvent, + StreamHalf, ThinRetransmits, +}; +use crate::{ + Dir, MAX_STREAM_COUNT, Side, StreamId, TransportError, VarInt, + coding::BufMutExt, + connection::stats::FrameStats, + frame::{self, FrameStruct, StreamMetaVec}, + transport_parameters::TransportParameters, +}; + +/// Wrapper around `Recv` that facilitates reusing `Recv` instances +#[derive(Debug)] +pub(super) enum StreamRecv { + /// A `Recv` that is ready to be opened + Free(Box), + /// A `Recv` that has been opened + Open(Box), +} + +impl StreamRecv { + /// Returns a reference to the inner `Recv` if the stream is open + pub(super) fn as_open_recv(&self) -> Option<&Recv> { + match self { + Self::Open(r) => Some(r), + _ => None, + } + } + + // Returns a mutable reference to the inner `Recv` if the stream is open + pub(super) fn as_open_recv_mut(&mut self) -> Option<&mut Recv> { + match self { + Self::Open(r) => Some(r), + _ => None, + } + } + + // Returns the inner `Recv` + pub(super) fn into_inner(self) -> Box { + match self { + Self::Free(r) | Self::Open(r) => r, + } + } + + // Reinitialize the stream so the inner `Recv` can be reused + pub(super) fn free(self, initial_max_data: u64) -> Self { + match self { + Self::Free(_) => unreachable!("Self::Free on reinit()"), + Self::Open(mut recv) => { + recv.reinit(initial_max_data); + Self::Free(recv) + } + } + } +} + +#[allow(unreachable_pub)] // fuzzing only +pub struct StreamsState { + pub(super) side: Side, + // Set of streams that are currently open, or could be immediately opened by the peer + pub(super) send: FxHashMap>>, + pub(super) recv: FxHashMap>, + pub(super) free_recv: Vec, + pub(super) next: [u64; 2], + /// Maximum number of locally-initiated streams that may be opened over the lifetime of the + /// connection so far, per direction + pub(super) max: [u64; 2], + /// Maximum number of remotely-initiated streams that may be opened over the lifetime of the + /// connection so far, per direction + pub(super) max_remote: [u64; 2], + /// Value of `max_remote` most recently transmitted to the peer in a `MAX_STREAMS` frame + sent_max_remote: [u64; 2], + /// Number of streams that we've given the peer permission to open and which aren't fully closed + pub(super) allocated_remote_count: [u64; 2], + /// Size of the desired stream flow control window. May be smaller than `allocated_remote_count` + /// due to `set_max_concurrent` calls. + max_concurrent_remote_count: [u64; 2], + /// Whether `max_concurrent_remote_count` has ever changed + flow_control_adjusted: bool, + /// Lowest remotely-initiated stream index that haven't actually been opened by the peer + pub(super) next_remote: [u64; 2], + /// Whether the remote endpoint has opened any streams the application doesn't know about yet, + /// per directionality + opened: [bool; 2], + // Next to report to the application, once opened + pub(super) next_reported_remote: [u64; 2], + /// Number of outbound streams + /// + /// This differs from `self.send.len()` in that it does not include streams that the peer is + /// permitted to open but which have not yet been opened. + pub(super) send_streams: usize, + /// Streams with outgoing data queued, sorted by priority + pub(super) pending: PendingStreamsQueue, + + events: VecDeque, + /// Streams blocked on connection-level flow control or stream window space + /// + /// Streams are only added to this list when a write fails. + pub(super) connection_blocked: Vec, + /// Connection-level flow control budget dictated by the peer + pub(super) max_data: u64, + /// The initial receive window + receive_window: u64, + /// Limit on incoming data, which is transmitted through `MAX_DATA` frames + local_max_data: u64, + /// The last value of `MAX_DATA` which had been queued for transmission in + /// an outgoing `MAX_DATA` frame + sent_max_data: VarInt, + /// Sum of current offsets of all send streams. + pub(super) data_sent: u64, + /// Sum of end offsets of all receive streams. Includes gaps, so it's an upper bound. + data_recvd: u64, + /// Total quantity of unacknowledged outgoing data + pub(super) unacked_data: u64, + /// Configured upper bound for `unacked_data`. + /// + /// Note this may be less than `unacked_data` if the user has set a new value. + pub(super) send_window: u64, + /// Configured upper bound for how much unacked data the peer can send us per stream + pub(super) stream_receive_window: u64, + + // Pertinent state from the TransportParameters supplied by the peer + initial_max_stream_data_uni: VarInt, + initial_max_stream_data_bidi_local: VarInt, + initial_max_stream_data_bidi_remote: VarInt, + + /// The shrink to be applied to local_max_data when receive_window is shrunk + receive_window_shrink_debt: u64, +} + +impl StreamsState { + #[allow(unreachable_pub)] // fuzzing only + pub fn new( + side: Side, + max_remote_uni: VarInt, + max_remote_bi: VarInt, + send_window: u64, + receive_window: VarInt, + stream_receive_window: VarInt, + ) -> Self { + let mut this = Self { + side, + send: FxHashMap::default(), + recv: FxHashMap::default(), + free_recv: Vec::new(), + next: [0, 0], + max: [0, 0], + max_remote: [max_remote_bi.into(), max_remote_uni.into()], + sent_max_remote: [max_remote_bi.into(), max_remote_uni.into()], + allocated_remote_count: [max_remote_bi.into(), max_remote_uni.into()], + max_concurrent_remote_count: [max_remote_bi.into(), max_remote_uni.into()], + flow_control_adjusted: false, + next_remote: [0, 0], + opened: [false, false], + next_reported_remote: [0, 0], + send_streams: 0, + pending: PendingStreamsQueue::new(), + events: VecDeque::new(), + connection_blocked: Vec::new(), + max_data: 0, + receive_window: receive_window.into(), + local_max_data: receive_window.into(), + sent_max_data: receive_window, + data_sent: 0, + data_recvd: 0, + unacked_data: 0, + send_window, + stream_receive_window: stream_receive_window.into(), + initial_max_stream_data_uni: 0u32.into(), + initial_max_stream_data_bidi_local: 0u32.into(), + initial_max_stream_data_bidi_remote: 0u32.into(), + receive_window_shrink_debt: 0, + }; + + for dir in Dir::iter() { + for i in 0..this.max_remote[dir as usize] { + this.insert(true, StreamId::new(!side, dir, i)); + } + } + + this + } + + pub(crate) fn set_params(&mut self, params: &TransportParameters) { + self.initial_max_stream_data_uni = params.initial_max_stream_data_uni; + self.initial_max_stream_data_bidi_local = params.initial_max_stream_data_bidi_local; + self.initial_max_stream_data_bidi_remote = params.initial_max_stream_data_bidi_remote; + self.max[Dir::Bi as usize] = params.initial_max_streams_bidi.into(); + self.max[Dir::Uni as usize] = params.initial_max_streams_uni.into(); + self.received_max_data(params.initial_max_data); + for i in 0..self.max_remote[Dir::Bi as usize] { + let id = StreamId::new(!self.side, Dir::Bi, i); + if let Some(s) = self.send.get_mut(&id).and_then(|s| s.as_mut()) { + s.max_data = params.initial_max_stream_data_bidi_local.into(); + } + } + } + + /// Ensure we have space for at least a full flow control window of remotely-initiated streams + /// to be open, and notify the peer if the window has moved + fn ensure_remote_streams(&mut self, dir: Dir) { + let new_count = self.max_concurrent_remote_count[dir as usize] + .saturating_sub(self.allocated_remote_count[dir as usize]); + for i in 0..new_count { + let id = StreamId::new(!self.side, dir, self.max_remote[dir as usize] + i); + self.insert(true, id); + } + self.allocated_remote_count[dir as usize] += new_count; + self.max_remote[dir as usize] += new_count; + } + + pub(crate) fn zero_rtt_rejected(&mut self) { + // Revert to initial state for outgoing streams + for dir in Dir::iter() { + for i in 0..self.next[dir as usize] { + // We don't bother calling `stream_freed` here because we explicitly reset affected + // counters below. + let id = StreamId::new(self.side, dir, i); + self.send.remove(&id).unwrap(); + if let Dir::Bi = dir { + self.recv.remove(&id).unwrap(); + } + } + self.next[dir as usize] = 0; + + // If 0-RTT was rejected, any flow control frames we sent were lost. + if self.flow_control_adjusted { + // Conservative approximation of whatever we sent in transport parameters + self.sent_max_remote[dir as usize] = 0; + } + } + + self.pending.clear(); + self.send_streams = 0; + self.data_sent = 0; + self.connection_blocked.clear(); + } + + /// Process incoming stream frame + /// + /// If successful, returns whether a `MAX_DATA` frame needs to be transmitted + pub(crate) fn received( + &mut self, + frame: frame::Stream, + payload_len: usize, + ) -> Result { + let id = frame.id; + self.validate_receive_id(id).map_err(|e| { + debug!("received illegal STREAM frame"); + e + })?; + + let rs = match self + .recv + .get_mut(&id) + .map(get_or_insert_recv(self.stream_receive_window)) + { + Some(rs) => rs, + None => { + trace!("dropping frame for closed stream"); + return Ok(ShouldTransmit(false)); + } + }; + + if !rs.is_receiving() { + trace!("dropping frame for finished stream"); + return Ok(ShouldTransmit(false)); + } + + let (new_bytes, closed) = + rs.ingest(frame, payload_len, self.data_recvd, self.local_max_data)?; + self.data_recvd = self.data_recvd.saturating_add(new_bytes); + + if !rs.stopped { + self.on_stream_frame(true, id); + return Ok(ShouldTransmit(false)); + } + + // Stopped streams become closed instantly on FIN, so check whether we need to clean up + if closed { + let rs = self.recv.remove(&id).flatten().unwrap(); + self.stream_recv_freed(id, rs); + } + + // We don't buffer data on stopped streams, so issue flow control credit immediately + Ok(self.add_read_credits(new_bytes)) + } + + /// Process incoming RESET_STREAM frame + /// + /// If successful, returns whether a `MAX_DATA` frame needs to be transmitted + #[allow(unreachable_pub)] // fuzzing only + pub fn received_reset( + &mut self, + frame: frame::ResetStream, + ) -> Result { + let frame::ResetStream { + id, + error_code, + final_offset, + } = frame; + self.validate_receive_id(id).map_err(|e| { + debug!("received illegal RESET_STREAM frame"); + e + })?; + + let rs = match self + .recv + .get_mut(&id) + .map(get_or_insert_recv(self.stream_receive_window)) + { + Some(stream) => stream, + None => { + trace!("received RESET_STREAM on closed stream"); + return Ok(ShouldTransmit(false)); + } + }; + + // State transition + if !rs.reset( + error_code, + final_offset, + self.data_recvd, + self.local_max_data, + )? { + // Redundant reset + return Ok(ShouldTransmit(false)); + } + let bytes_read = rs.assembler.bytes_read(); + let stopped = rs.stopped; + let end = rs.end; + if stopped { + // Stopped streams should be disposed immediately on reset + let rs = self.recv.remove(&id).flatten().unwrap(); + self.stream_recv_freed(id, rs); + } + self.on_stream_frame(!stopped, id); + + // Update connection-level flow control + Ok(if bytes_read != final_offset.into_inner() { + // bytes_read is always <= end, so this won't underflow. + self.data_recvd = self + .data_recvd + .saturating_add(u64::from(final_offset) - end); + self.add_read_credits(u64::from(final_offset) - bytes_read) + } else { + ShouldTransmit(false) + }) + } + + /// Process incoming `STOP_SENDING` frame + #[allow(unreachable_pub)] // fuzzing only + pub fn received_stop_sending(&mut self, id: StreamId, error_code: VarInt) { + let max_send_data = self.max_send_data(id); + let stream = match self + .send + .get_mut(&id) + .map(get_or_insert_send(max_send_data)) + { + Some(ss) => ss, + None => return, + }; + + if stream.try_stop(error_code) { + self.events + .push_back(StreamEvent::Stopped { id, error_code }); + self.on_stream_frame(false, id); + } + } + + pub(crate) fn reset_acked(&mut self, id: StreamId) { + match self.send.entry(id) { + hash_map::Entry::Vacant(_) => {} + hash_map::Entry::Occupied(e) => { + if let Some(SendState::ResetSent) = e.get().as_ref().map(|s| s.state) { + e.remove_entry(); + self.stream_freed(id, StreamHalf::Send); + } + } + } + } + + /// Whether any stream data is queued, regardless of control frames + pub(crate) fn can_send_stream_data(&self) -> bool { + // Reset streams may linger in the pending stream list, but will never produce stream frames + self.pending.iter().any(|stream| { + self.send + .get(&stream.id) + .and_then(|s| s.as_ref()) + .is_some_and(|s| !s.is_reset()) + }) + } + + /// Whether MAX_STREAM_DATA frames could be sent for stream `id` + pub(crate) fn can_send_flow_control(&self, id: StreamId) -> bool { + self.recv + .get(&id) + .and_then(|s| s.as_ref()) + .and_then(|s| s.as_open_recv()) + .is_some_and(|s| s.can_send_flow_control()) + } + + pub(in crate::connection) fn write_control_frames( + &mut self, + buf: &mut Vec, + pending: &mut Retransmits, + retransmits: &mut ThinRetransmits, + stats: &mut FrameStats, + max_size: usize, + ) { + // RESET_STREAM + while buf.len() + frame::ResetStream::SIZE_BOUND < max_size { + let (id, error_code) = match pending.reset_stream.pop() { + Some(x) => x, + None => break, + }; + let stream = match self.send.get_mut(&id).and_then(|s| s.as_mut()) { + Some(x) => x, + None => continue, + }; + trace!(stream = %id, "RESET_STREAM"); + retransmits + .get_or_create() + .reset_stream + .push((id, error_code)); + frame::ResetStream { + id, + error_code, + final_offset: VarInt::try_from(stream.offset()).expect("impossibly large offset"), + } + .encode(buf); + stats.reset_stream += 1; + } + + // STOP_SENDING + while buf.len() + frame::StopSending::SIZE_BOUND < max_size { + let frame = match pending.stop_sending.pop() { + Some(x) => x, + None => break, + }; + // We may need to transmit STOP_SENDING even for streams whose state we have discarded, + // because we are able to discard local state for stopped streams immediately upon + // receiving FIN, even if the peer still has arbitrarily large amounts of data to + // (re)transmit due to loss or unconventional sending strategy. We could fine-tune this + // a little by dropping the frame if we specifically know the stream's been reset by the + // peer, but we discard that information as soon as the application consumes it, so it + // can't be relied upon regardless. + trace!(stream = %frame.id, "STOP_SENDING"); + frame.encode(buf); + retransmits.get_or_create().stop_sending.push(frame); + stats.stop_sending += 1; + } + + // MAX_DATA + if pending.max_data && buf.len() + 9 < max_size { + pending.max_data = false; + + // `local_max_data` can grow bigger than `VarInt`. + // For transmission inside QUIC frames we need to clamp it to the + // maximum allowed `VarInt` size. + let max = VarInt::try_from(self.local_max_data).unwrap_or(VarInt::MAX); + + trace!(value = max.into_inner(), "MAX_DATA"); + if max > self.sent_max_data { + // Record that a `MAX_DATA` announcing a certain window was sent. This will + // suppress enqueuing further `MAX_DATA` frames unless either the previous + // transmission was not acknowledged or the window further increased. + self.sent_max_data = max; + } + + retransmits.get_or_create().max_data = true; + buf.write(frame::FrameType::MAX_DATA); + buf.write(max); + stats.max_data += 1; + } + + // MAX_STREAM_DATA + while buf.len() + 17 < max_size { + let id = match pending.max_stream_data.iter().next() { + Some(x) => *x, + None => break, + }; + pending.max_stream_data.remove(&id); + let rs = match self + .recv + .get_mut(&id) + .and_then(|s| s.as_mut()) + .and_then(|s| s.as_open_recv_mut()) + { + Some(x) => x, + None => continue, + }; + if !rs.can_send_flow_control() { + continue; + } + retransmits.get_or_create().max_stream_data.insert(id); + + let (max, _) = rs.max_stream_data(self.stream_receive_window); + rs.record_sent_max_stream_data(max); + + trace!(stream = %id, max = max, "MAX_STREAM_DATA"); + buf.write(frame::FrameType::MAX_STREAM_DATA); + buf.write(id); + buf.write_var(max); + stats.max_stream_data += 1; + } + + // MAX_STREAMS + for dir in Dir::iter() { + if !pending.max_stream_id[dir as usize] || buf.len() + 9 >= max_size { + continue; + } + + pending.max_stream_id[dir as usize] = false; + retransmits.get_or_create().max_stream_id[dir as usize] = true; + self.sent_max_remote[dir as usize] = self.max_remote[dir as usize]; + trace!( + value = self.max_remote[dir as usize], + "MAX_STREAMS ({:?})", dir + ); + buf.write(match dir { + Dir::Uni => frame::FrameType::MAX_STREAMS_UNI, + Dir::Bi => frame::FrameType::MAX_STREAMS_BIDI, + }); + buf.write_var(self.max_remote[dir as usize]); + match dir { + Dir::Uni => stats.max_streams_uni += 1, + Dir::Bi => stats.max_streams_bidi += 1, + } + } + } + + pub(crate) fn write_stream_frames( + &mut self, + buf: &mut Vec, + max_buf_size: usize, + fair: bool, + ) -> StreamMetaVec { + let mut stream_frames = StreamMetaVec::new(); + while buf.len() + frame::Stream::SIZE_BOUND < max_buf_size { + if max_buf_size + .checked_sub(buf.len() + frame::Stream::SIZE_BOUND) + .is_none() + { + break; + } + + // Pop the stream of the highest priority that currently has pending data + // If the stream still has some pending data left after writing, it will be reinserted, otherwise not + let Some(stream) = self.pending.pop() else { + break; + }; + + let id = stream.id; + + let stream = match self.send.get_mut(&id).and_then(|s| s.as_mut()) { + Some(s) => s, + // Stream was reset with pending data and the reset was acknowledged + None => continue, + }; + + // Reset streams aren't removed from the pending list and still exist while the peer + // hasn't acknowledged the reset, but should not generate STREAM frames, so we need to + // check for them explicitly. + if stream.is_reset() { + continue; + } + + // Now that we know the `StreamId`, we can better account for how many bytes + // are required to encode it. + let max_buf_size = max_buf_size - buf.len() - 1 - VarInt::size(id.into()); + let (offsets, encode_length) = stream.pending.poll_transmit(max_buf_size); + let fin = offsets.end == stream.pending.offset() + && matches!(stream.state, SendState::DataSent { .. }); + if fin { + stream.fin_pending = false; + } + + if stream.is_pending() { + // If the stream still has pending data, reinsert it, possibly with an updated priority value + // Fairness with other streams is achieved by implementing round-robin scheduling, + // so that the other streams will have a chance to write data + // before we touch this stream again. + if fair { + self.pending.push_pending(id, stream.priority); + } else { + self.pending.reinsert_pending(id, stream.priority); + } + } + + let meta = frame::StreamMeta { id, offsets, fin }; + trace!(id = %meta.id, off = meta.offsets.start, len = meta.offsets.end - meta.offsets.start, fin = meta.fin, "STREAM"); + meta.encode(encode_length, buf); + + // The range might not be retrievable in a single `get` if it is + // stored in noncontiguous fashion. Therefore this loop iterates + // until the range is fully copied into the frame. + let mut offsets = meta.offsets.clone(); + while offsets.start != offsets.end { + let data = stream.pending.get(offsets.clone()); + offsets.start += data.len() as u64; + buf.put_slice(data); + } + stream_frames.push(meta); + } + + stream_frames + } + + /// Notify the application that new streams were opened or a stream became readable. + fn on_stream_frame(&mut self, notify_readable: bool, stream: StreamId) { + if stream.initiator() == self.side { + // Notifying about the opening of locally-initiated streams would be redundant. + if notify_readable { + self.events.push_back(StreamEvent::Readable { id: stream }); + } + return; + } + let next = &mut self.next_remote[stream.dir() as usize]; + if stream.index() >= *next { + *next = stream.index() + 1; + self.opened[stream.dir() as usize] = true; + } else if notify_readable { + self.events.push_back(StreamEvent::Readable { id: stream }); + } + } + + pub(crate) fn received_ack_of(&mut self, frame: frame::StreamMeta) { + let mut entry = match self.send.entry(frame.id) { + hash_map::Entry::Vacant(_) => return, + hash_map::Entry::Occupied(e) => e, + }; + + let stream = match entry.get_mut().as_mut() { + Some(s) => s, + None => { + // Because we only call this after sending data on this stream, + // this closure should be unreachable. If we did somehow screw that up, + // then we might hit an underflow below with unpredictable effects down + // the line. Best to short-circuit. + return; + } + }; + + if stream.is_reset() { + // We account for outstanding data on reset streams at time of reset + return; + } + let id = frame.id; + self.unacked_data -= frame.offsets.end - frame.offsets.start; + if !stream.ack(frame) { + // The stream is unfinished or may still need retransmits + return; + } + + entry.remove_entry(); + self.stream_freed(id, StreamHalf::Send); + self.events.push_back(StreamEvent::Finished { id }); + } + + pub(crate) fn retransmit(&mut self, frame: frame::StreamMeta) { + let stream = match self.send.get_mut(&frame.id).and_then(|s| s.as_mut()) { + // Loss of data on a closed stream is a noop + None => return, + Some(x) => x, + }; + if !stream.is_pending() { + self.pending.push_pending(frame.id, stream.priority); + } + stream.fin_pending |= frame.fin; + stream.pending.retransmit(frame.offsets); + } + + pub(crate) fn retransmit_all_for_0rtt(&mut self) { + for dir in Dir::iter() { + for index in 0..self.next[dir as usize] { + let id = StreamId::new(Side::Client, dir, index); + let stream = match self.send.get_mut(&id).and_then(|s| s.as_mut()) { + Some(stream) => stream, + None => continue, + }; + if stream.pending.is_fully_acked() && !stream.fin_pending { + // Stream data can't be acked in 0-RTT, so we must not have sent anything on + // this stream + continue; + } + if !stream.is_pending() { + self.pending.push_pending(id, stream.priority); + } + stream.pending.retransmit_all_for_0rtt(); + } + } + } + + pub(crate) fn received_max_streams( + &mut self, + dir: Dir, + count: u64, + ) -> Result<(), TransportError> { + if count > MAX_STREAM_COUNT { + return Err(TransportError::FRAME_ENCODING_ERROR( + "unrepresentable stream limit", + )); + } + + let current = &mut self.max[dir as usize]; + if count > *current { + *current = count; + self.events.push_back(StreamEvent::Available { dir }); + } + + Ok(()) + } + + /// Handle increase to connection-level flow control limit + pub(crate) fn received_max_data(&mut self, n: VarInt) { + self.max_data = self.max_data.max(n.into()); + } + + pub(crate) fn received_max_stream_data( + &mut self, + id: StreamId, + offset: u64, + ) -> Result<(), TransportError> { + if id.initiator() != self.side && id.dir() == Dir::Uni { + debug!("got MAX_STREAM_DATA on recv-only {}", id); + return Err(TransportError::STREAM_STATE_ERROR( + "MAX_STREAM_DATA on recv-only stream", + )); + } + + let write_limit = self.write_limit(); + let max_send_data = self.max_send_data(id); + if let Some(ss) = self + .send + .get_mut(&id) + .map(get_or_insert_send(max_send_data)) + { + if ss.increase_max_data(offset) { + if write_limit > 0 { + self.events.push_back(StreamEvent::Writable { id }); + } else if !ss.connection_blocked { + // The stream is still blocked on the connection flow control + // window. In order to get unblocked when the window relaxes + // it needs to be in the connection blocked list. + ss.connection_blocked = true; + self.connection_blocked.push(id); + } + } + } else if id.initiator() == self.side && self.is_local_unopened(id) { + debug!("got MAX_STREAM_DATA on unopened {}", id); + return Err(TransportError::STREAM_STATE_ERROR( + "MAX_STREAM_DATA on unopened stream", + )); + } + + self.on_stream_frame(false, id); + Ok(()) + } + + /// Returns the maximum amount of data this is allowed to be written on the connection + pub(crate) fn write_limit(&self) -> u64 { + (self.max_data - self.data_sent) + // `send_window` can be set after construction to something *less* than `unacked_data` + .min(self.send_window.saturating_sub(self.unacked_data)) + } + + /// Yield stream events + pub(crate) fn poll(&mut self) -> Option { + if let Some(dir) = Dir::iter().find(|&i| mem::replace(&mut self.opened[i as usize], false)) + { + return Some(StreamEvent::Opened { dir }); + } + + if self.write_limit() > 0 { + while let Some(id) = self.connection_blocked.pop() { + let stream = match self.send.get_mut(&id).and_then(|s| s.as_mut()) { + None => continue, + Some(s) => s, + }; + + debug_assert!(stream.connection_blocked); + stream.connection_blocked = false; + + // If it's no longer sensible to write to a stream (even to detect an error) then don't + // report it. + if stream.is_writable() && stream.max_data > stream.offset() { + return Some(StreamEvent::Writable { id }); + } + } + } + + self.events.pop_front() + } + + /// Queues MAX_STREAM_ID frames in `pending` if needed + /// + /// Returns whether any frames were queued. + pub(crate) fn queue_max_stream_id(&mut self, pending: &mut Retransmits) -> bool { + let mut queued = false; + for dir in Dir::iter() { + let diff = self.max_remote[dir as usize] - self.sent_max_remote[dir as usize]; + // To reduce traffic, only announce updates if at least 1/8 of the flow control window + // has been consumed. + if diff > self.max_concurrent_remote_count[dir as usize] / 8 { + pending.max_stream_id[dir as usize] = true; + queued = true; + } + } + queued + } + + /// Check for errors entailed by the peer's use of `id` as a send stream + fn validate_receive_id(&mut self, id: StreamId) -> Result<(), TransportError> { + if self.side == id.initiator() { + match id.dir() { + Dir::Uni => { + return Err(TransportError::STREAM_STATE_ERROR( + "illegal operation on send-only stream", + )); + } + Dir::Bi if id.index() >= self.next[Dir::Bi as usize] => { + return Err(TransportError::STREAM_STATE_ERROR( + "operation on unopened stream", + )); + } + Dir::Bi => {} + }; + } else { + let limit = self.max_remote[id.dir() as usize]; + if id.index() >= limit { + return Err(TransportError::STREAM_LIMIT_ERROR("")); + } + } + Ok(()) + } + + /// Whether a locally initiated stream has never been open + pub(crate) fn is_local_unopened(&self, id: StreamId) -> bool { + id.index() >= self.next[id.dir() as usize] + } + + pub(crate) fn set_max_concurrent(&mut self, dir: Dir, count: VarInt) { + self.flow_control_adjusted = true; + self.max_concurrent_remote_count[dir as usize] = count.into(); + self.ensure_remote_streams(dir); + } + + pub(crate) fn max_concurrent(&self, dir: Dir) -> u64 { + self.allocated_remote_count[dir as usize] + } + + pub(crate) fn set_send_window(&mut self, send_window: u64) { + self.send_window = send_window; + } + + /// Set the receive_window and returns whether the receive_window has been + /// expanded or shrunk: true if expanded, false if shrunk. + pub(crate) fn set_receive_window(&mut self, receive_window: VarInt) -> bool { + let receive_window = receive_window.into(); + let mut expanded = false; + if receive_window > self.receive_window { + self.local_max_data = self + .local_max_data + .saturating_add(receive_window - self.receive_window); + expanded = true; + } else { + let diff = self.receive_window - receive_window; + self.receive_window_shrink_debt = self.receive_window_shrink_debt.saturating_add(diff); + } + self.receive_window = receive_window; + expanded + } + + pub(super) fn insert(&mut self, remote: bool, id: StreamId) { + let bi = id.dir() == Dir::Bi; + // bidirectional OR (unidirectional AND NOT remote) + if bi || !remote { + assert!(self.send.insert(id, None).is_none()); + } + // bidirectional OR (unidirectional AND remote) + if bi || remote { + let recv = self.free_recv.pop(); + assert!(self.recv.insert(id, recv).is_none()); + } + } + + /// Adds credits to the connection flow control window + /// + /// Returns whether a `MAX_DATA` frame should be enqueued as soon as possible. + /// This will only be the case if the window update would is significant + /// enough. As soon as a window update with a `MAX_DATA` frame has been + /// queued, the [`Recv::record_sent_max_stream_data`] function should be called to + /// suppress sending further updates until the window increases significantly + /// again. + pub(super) fn add_read_credits(&mut self, credits: u64) -> ShouldTransmit { + if credits > self.receive_window_shrink_debt { + let net_credits = credits - self.receive_window_shrink_debt; + self.local_max_data = self.local_max_data.saturating_add(net_credits); + self.receive_window_shrink_debt = 0; + } else { + self.receive_window_shrink_debt -= credits; + } + + if self.local_max_data > VarInt::MAX.into_inner() { + return ShouldTransmit(false); + } + + // Only announce a window update if it's significant enough + // to make it worthwhile sending a MAX_DATA frame. + // We use a fraction of the configured connection receive window to make + // the decision, to accommodate for connection using bigger windows requiring + // less updates. + let diff = self.local_max_data - self.sent_max_data.into_inner(); + ShouldTransmit(diff >= (self.receive_window / 8)) + } + + /// Update counters for removal of a stream + pub(super) fn stream_freed(&mut self, id: StreamId, half: StreamHalf) { + if id.initiator() != self.side { + let fully_free = id.dir() == Dir::Uni + || match half { + StreamHalf::Send => !self.recv.contains_key(&id), + StreamHalf::Recv => !self.send.contains_key(&id), + }; + if fully_free { + self.allocated_remote_count[id.dir() as usize] -= 1; + self.ensure_remote_streams(id.dir()); + } + } + if half == StreamHalf::Send { + self.send_streams -= 1; + } + } + + pub(super) fn stream_recv_freed(&mut self, id: StreamId, recv: StreamRecv) { + self.free_recv.push(recv.free(self.stream_receive_window)); + self.stream_freed(id, StreamHalf::Recv); + } + + pub(super) fn max_send_data(&self, id: StreamId) -> VarInt { + let remote = self.side != id.initiator(); + match id.dir() { + Dir::Uni => self.initial_max_stream_data_uni, + // Remote/local appear reversed here because the transport parameters are named from + // the perspective of the peer. + Dir::Bi if remote => self.initial_max_stream_data_bidi_local, + Dir::Bi => self.initial_max_stream_data_bidi_remote, + } + } +} + +#[inline] +pub(super) fn get_or_insert_send( + max_data: VarInt, +) -> impl Fn(&mut Option>) -> &mut Box { + move |opt| opt.get_or_insert_with(|| Send::new(max_data)) +} + +#[inline] +pub(super) fn get_or_insert_recv( + initial_max_data: u64, +) -> impl FnMut(&mut Option) -> &mut Recv { + move |opt| { + *opt = opt.take().map(|s| match s { + StreamRecv::Free(recv) => StreamRecv::Open(recv), + s => s, + }); + opt.get_or_insert_with(|| StreamRecv::Open(Recv::new(initial_max_data))) + .as_open_recv_mut() + .unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ReadableError, RecvStream, SendStream, TransportErrorCode, WriteError, + connection::State as ConnState, connection::Streams, + }; + use bytes::Bytes; + + fn make(side: Side) -> StreamsState { + StreamsState::new( + side, + 128u32.into(), + 128u32.into(), + 1024 * 1024, + (1024 * 1024u32).into(), + (1024 * 1024u32).into(), + ) + } + + #[test] + fn trivial_flow_control() { + let mut client = StreamsState::new( + Side::Client, + 1u32.into(), + 1u32.into(), + 1024 * 1024, + (1024 * 1024u32).into(), + (1024 * 1024u32).into(), + ); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + let initial_max = client.local_max_data; + const MESSAGE_SIZE: usize = 2048; + assert_eq!( + client + .received( + frame::Stream { + id, + offset: 0, + fin: true, + data: Bytes::from_static(&[0; MESSAGE_SIZE]), + }, + 2048 + ) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.data_recvd, 2048); + assert_eq!(client.local_max_data - initial_max, 0); + + let mut pending = Retransmits::default(); + let mut recv = RecvStream { + id, + state: &mut client, + pending: &mut pending, + }; + + let mut chunks = recv.read(true).unwrap(); + assert_eq!( + chunks.next(MESSAGE_SIZE).unwrap().unwrap().bytes.len(), + MESSAGE_SIZE + ); + assert!(chunks.next(0).unwrap().is_none()); + let should_transmit = chunks.finalize(); + assert!(should_transmit.0); + assert!(pending.max_stream_id[Dir::Uni as usize]); + assert_eq!(client.local_max_data - initial_max, MESSAGE_SIZE as u64); + } + + #[test] + fn reset_flow_control() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + let initial_max = client.local_max_data; + assert_eq!( + client + .received( + frame::Stream { + id, + offset: 0, + fin: false, + data: Bytes::from_static(&[0; 2048]), + }, + 2048 + ) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.data_recvd, 2048); + assert_eq!(client.local_max_data - initial_max, 0); + + let mut pending = Retransmits::default(); + let mut recv = RecvStream { + id, + state: &mut client, + pending: &mut pending, + }; + + let mut chunks = recv.read(true).unwrap(); + chunks.next(1024).unwrap(); + let _ = chunks.finalize(); + assert_eq!(client.local_max_data - initial_max, 1024); + assert_eq!( + client + .received_reset(frame::ResetStream { + id, + error_code: 0u32.into(), + final_offset: 4096u32.into(), + }) + .unwrap(), + ShouldTransmit(false) + ); + + assert_eq!(client.data_recvd, 4096); + assert_eq!(client.local_max_data - initial_max, 4096); + + // Ensure reading after a reset doesn't issue redundant credit + let mut recv = RecvStream { + id, + state: &mut client, + pending: &mut pending, + }; + let mut chunks = recv.read(true).unwrap(); + assert_eq!( + chunks.next(1024).unwrap_err(), + crate::ReadError::Reset(0u32.into()) + ); + let _ = chunks.finalize(); + assert_eq!(client.data_recvd, 4096); + assert_eq!(client.local_max_data - initial_max, 4096); + } + + #[test] + fn reset_after_empty_frame_flow_control() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + let initial_max = client.local_max_data; + assert_eq!( + client + .received( + frame::Stream { + id, + offset: 4096, + fin: false, + data: Bytes::from_static(&[0; 0]), + }, + 0 + ) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.data_recvd, 4096); + assert_eq!(client.local_max_data - initial_max, 0); + assert_eq!( + client + .received_reset(frame::ResetStream { + id, + error_code: 0u32.into(), + final_offset: 4096u32.into(), + }) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.data_recvd, 4096); + assert_eq!(client.local_max_data - initial_max, 4096); + } + + #[test] + fn duplicate_reset_flow_control() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + assert_eq!( + client + .received_reset(frame::ResetStream { + id, + error_code: 0u32.into(), + final_offset: 4096u32.into(), + }) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.data_recvd, 4096); + assert_eq!( + client + .received_reset(frame::ResetStream { + id, + error_code: 0u32.into(), + final_offset: 4096u32.into(), + }) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.data_recvd, 4096); + } + + #[test] + fn recv_stopped() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + let initial_max = client.local_max_data; + assert_eq!( + client + .received( + frame::Stream { + id, + offset: 0, + fin: false, + data: Bytes::from_static(&[0; 32]), + }, + 32 + ) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.local_max_data, initial_max); + + let mut pending = Retransmits::default(); + let mut recv = RecvStream { + id, + state: &mut client, + pending: &mut pending, + }; + + recv.stop(0u32.into()).unwrap(); + assert_eq!(recv.pending.stop_sending.len(), 1); + assert!(!recv.pending.max_data); + + assert!(recv.stop(0u32.into()).is_err()); + assert_eq!(recv.read(true).err(), Some(ReadableError::ClosedStream)); + assert_eq!(recv.read(false).err(), Some(ReadableError::ClosedStream)); + + assert_eq!(client.local_max_data - initial_max, 32); + assert_eq!( + client + .received( + frame::Stream { + id, + offset: 32, + fin: true, + data: Bytes::from_static(&[0; 16]), + }, + 16 + ) + .unwrap(), + ShouldTransmit(false) + ); + assert_eq!(client.local_max_data - initial_max, 48); + assert!(!client.recv.contains_key(&id)); + } + + #[test] + fn stopped_reset() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + // Server opens stream + assert_eq!( + client + .received( + frame::Stream { + id, + offset: 0, + fin: false, + data: Bytes::from_static(&[0; 32]) + }, + 32 + ) + .unwrap(), + ShouldTransmit(false) + ); + + let mut pending = Retransmits::default(); + let mut recv = RecvStream { + id, + state: &mut client, + pending: &mut pending, + }; + + recv.stop(0u32.into()).unwrap(); + assert_eq!(pending.stop_sending.len(), 1); + assert!(!pending.max_data); + + // Server complies + let prev_max = client.max_remote[Dir::Uni as usize]; + assert_eq!( + client + .received_reset(frame::ResetStream { + id, + error_code: 0u32.into(), + final_offset: 32u32.into(), + }) + .unwrap(), + ShouldTransmit(false) + ); + assert!(!client.recv.contains_key(&id), "stream state is freed"); + assert_eq!(client.max_remote[Dir::Uni as usize], prev_max + 1); + } + + #[test] + fn send_stopped() { + let mut server = make(Side::Server); + server.set_params(&TransportParameters { + initial_max_streams_uni: 1u32.into(), + initial_max_data: 42u32.into(), + initial_max_stream_data_uni: 42u32.into(), + ..TransportParameters::default() + }); + + let (mut pending, state) = (Retransmits::default(), ConnState::Established); + let id = Streams { + state: &mut server, + conn_state: &state, + } + .open(Dir::Uni) + .unwrap(); + + let mut stream = SendStream { + id, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + + let error_code = 0u32.into(); + stream.state.received_stop_sending(id, error_code); + assert!( + stream + .state + .events + .contains(&StreamEvent::Stopped { id, error_code }) + ); + stream.state.events.clear(); + + assert_eq!(stream.write(&[]), Err(WriteError::Stopped(error_code))); + + stream.reset(0u32.into()).unwrap(); + assert_eq!(stream.write(&[]), Err(WriteError::ClosedStream)); + + // A duplicate frame is a no-op + stream.state.received_stop_sending(id, error_code); + assert!(stream.state.events.is_empty()); + } + + #[test] + fn final_offset_flow_control() { + let mut client = make(Side::Client); + assert_eq!( + client + .received_reset(frame::ResetStream { + id: StreamId::new(Side::Server, Dir::Uni, 0), + error_code: 0u32.into(), + final_offset: VarInt::MAX, + }) + .unwrap_err() + .code, + TransportErrorCode::FLOW_CONTROL_ERROR + ); + } + + #[test] + fn stream_priority() { + let mut server = make(Side::Server); + server.set_params(&TransportParameters { + initial_max_streams_bidi: 3u32.into(), + initial_max_data: 10u32.into(), + initial_max_stream_data_bidi_remote: 10u32.into(), + ..TransportParameters::default() + }); + + let (mut pending, state) = (Retransmits::default(), ConnState::Established); + let mut streams = Streams { + state: &mut server, + conn_state: &state, + }; + + let id_high = streams.open(Dir::Bi).unwrap(); + let id_mid = streams.open(Dir::Bi).unwrap(); + let id_low = streams.open(Dir::Bi).unwrap(); + + let mut mid = SendStream { + id: id_mid, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + mid.write(b"mid").unwrap(); + + let mut low = SendStream { + id: id_low, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + low.set_priority(-1).unwrap(); + low.write(b"low").unwrap(); + + let mut high = SendStream { + id: id_high, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + high.set_priority(1).unwrap(); + high.write(b"high").unwrap(); + + let mut buf = Vec::with_capacity(40); + let meta = server.write_stream_frames(&mut buf, 40, true); + assert_eq!(meta[0].id, id_high); + assert_eq!(meta[1].id, id_mid); + assert_eq!(meta[2].id, id_low); + + assert!(!server.can_send_stream_data()); + assert_eq!(server.pending.len(), 0); + } + + #[test] + fn requeue_stream_priority() { + let mut server = make(Side::Server); + server.set_params(&TransportParameters { + initial_max_streams_bidi: 3u32.into(), + initial_max_data: 1000u32.into(), + initial_max_stream_data_bidi_remote: 1000u32.into(), + ..TransportParameters::default() + }); + + let (mut pending, state) = (Retransmits::default(), ConnState::Established); + let mut streams = Streams { + state: &mut server, + conn_state: &state, + }; + + let id_high = streams.open(Dir::Bi).unwrap(); + let id_mid = streams.open(Dir::Bi).unwrap(); + + let mut mid = SendStream { + id: id_mid, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + assert_eq!(mid.write(b"mid").unwrap(), 3); + assert_eq!(server.pending.len(), 1); + + let mut high = SendStream { + id: id_high, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + high.set_priority(1).unwrap(); + assert_eq!(high.write(&[0; 200]).unwrap(), 200); + assert_eq!(server.pending.len(), 2); + + // Requeue the high priority stream to lowest priority. The initial send + // still uses high priority since it's queued that way. After that it will + // switch to low priority + let mut high = SendStream { + id: id_high, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + high.set_priority(-1).unwrap(); + + let mut buf = Vec::with_capacity(1000); + let meta = server.write_stream_frames(&mut buf, 40, true); + assert_eq!(meta.len(), 1); + assert_eq!(meta[0].id, id_high); + + // After requeuing we should end up with 2 priorities - not 3 + assert_eq!(server.pending.len(), 2); + + // Send the remaining data. The initial mid priority one should go first now + let meta = server.write_stream_frames(&mut buf, 1000, true); + assert_eq!(meta.len(), 2); + assert_eq!(meta[0].id, id_mid); + assert_eq!(meta[1].id, id_high); + + assert!(!server.can_send_stream_data()); + assert_eq!(server.pending.len(), 0); + } + + #[test] + fn same_stream_priority() { + for fair in [true, false] { + let mut server = make(Side::Server); + server.set_params(&TransportParameters { + initial_max_streams_bidi: 3u32.into(), + initial_max_data: 300u32.into(), + initial_max_stream_data_bidi_remote: 300u32.into(), + ..TransportParameters::default() + }); + + let (mut pending, state) = (Retransmits::default(), ConnState::Established); + let mut streams = Streams { + state: &mut server, + conn_state: &state, + }; + + // a, b and c all have the same priority + let id_a = streams.open(Dir::Bi).unwrap(); + let id_b = streams.open(Dir::Bi).unwrap(); + let id_c = streams.open(Dir::Bi).unwrap(); + + let mut stream_a = SendStream { + id: id_a, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream_a.write(&[b'a'; 100]).unwrap(); + + let mut stream_b = SendStream { + id: id_b, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream_b.write(&[b'b'; 100]).unwrap(); + + let mut stream_c = SendStream { + id: id_c, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream_c.write(&[b'c'; 100]).unwrap(); + + let mut metas = vec![]; + let mut buf = Vec::with_capacity(1024); + + // loop until all the streams are written + loop { + let buf_len = buf.len(); + let meta = server.write_stream_frames(&mut buf, buf_len + 40, fair); + if meta.is_empty() { + break; + } + metas.extend(meta); + } + + assert!(!server.can_send_stream_data()); + assert_eq!(server.pending.len(), 0); + + let stream_ids = metas.iter().map(|m| m.id).collect::>(); + if fair { + // When fairness is enabled, if we run out of buffer space to write out a stream, + // the stream is re-queued after all the streams with the same priority. + assert_eq!( + stream_ids, + vec![id_a, id_b, id_c, id_a, id_b, id_c, id_a, id_b, id_c] + ); + } else { + // When fairness is disabled the stream is re-queued before all the other streams + // with the same priority. + assert_eq!( + stream_ids, + vec![id_a, id_a, id_a, id_b, id_b, id_b, id_c, id_c, id_c] + ); + } + } + } + + #[test] + fn unfair_priority_bump() { + let mut server = make(Side::Server); + server.set_params(&TransportParameters { + initial_max_streams_bidi: 3u32.into(), + initial_max_data: 300u32.into(), + initial_max_stream_data_bidi_remote: 300u32.into(), + ..TransportParameters::default() + }); + + let (mut pending, state) = (Retransmits::default(), ConnState::Established); + let mut streams = Streams { + state: &mut server, + conn_state: &state, + }; + + // a, and b have the same priority, c has higher priority + let id_a = streams.open(Dir::Bi).unwrap(); + let id_b = streams.open(Dir::Bi).unwrap(); + let id_c = streams.open(Dir::Bi).unwrap(); + + let mut stream_a = SendStream { + id: id_a, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream_a.write(&[b'a'; 100]).unwrap(); + + let mut stream_b = SendStream { + id: id_b, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream_b.write(&[b'b'; 100]).unwrap(); + + let mut metas = vec![]; + let mut buf = Vec::with_capacity(1024); + + // Write the first chunk of stream_a + let buf_len = buf.len(); + let meta = server.write_stream_frames(&mut buf, buf_len + 40, false); + assert!(!meta.is_empty()); + metas.extend(meta); + + // Queue stream_c which has higher priority + let mut stream_c = SendStream { + id: id_c, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream_c.set_priority(1).unwrap(); + stream_c.write(&[b'b'; 100]).unwrap(); + + // loop until all the streams are written + loop { + let buf_len = buf.len(); + let meta = server.write_stream_frames(&mut buf, buf_len + 40, false); + if meta.is_empty() { + break; + } + metas.extend(meta); + } + + assert!(!server.can_send_stream_data()); + assert_eq!(server.pending.len(), 0); + + let stream_ids = metas.iter().map(|m| m.id).collect::>(); + assert_eq!( + stream_ids, + // stream_c bumps stream_b but doesn't bump stream_a which had already been partly + // written out + vec![id_a, id_a, id_a, id_c, id_c, id_c, id_b, id_b, id_b] + ); + } + + #[test] + fn stop_finished() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + // Server finishes stream + let _ = client + .received( + frame::Stream { + id, + offset: 0, + fin: true, + data: Bytes::from_static(&[0; 32]), + }, + 32, + ) + .unwrap(); + let mut pending = Retransmits::default(); + let mut stream = RecvStream { + id, + state: &mut client, + pending: &mut pending, + }; + stream.stop(0u32.into()).unwrap(); + assert!(client.recv.get_mut(&id).is_none(), "stream is freed"); + } + + // Verify that a stream that's been reset doesn't cause the appearance of pending data + #[test] + fn reset_stream_cannot_send() { + let mut server = make(Side::Server); + server.set_params(&TransportParameters { + initial_max_streams_uni: 1u32.into(), + initial_max_data: 42u32.into(), + initial_max_stream_data_uni: 42u32.into(), + ..TransportParameters::default() + }); + let (mut pending, state) = (Retransmits::default(), ConnState::Established); + let mut streams = Streams { + state: &mut server, + conn_state: &state, + }; + + let id = streams.open(Dir::Uni).unwrap(); + let mut stream = SendStream { + id, + state: &mut server, + pending: &mut pending, + conn_state: &state, + }; + stream.write(b"hello").unwrap(); + stream.reset(0u32.into()).unwrap(); + + assert_eq!(pending.reset_stream, &[(id, 0u32.into())]); + assert!(!server.can_send_stream_data()); + } + + #[test] + fn stream_limit_fixed() { + let mut client = make(Side::Client); + // Open streams 0-127 + assert_eq!( + client.received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 127), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ), + Ok(ShouldTransmit(false)) + ); + // Try to open stream 128, exceeding limit + assert_eq!( + client + .received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 128), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ) + .unwrap_err() + .code, + TransportErrorCode::STREAM_LIMIT_ERROR + ); + + // Free stream 127 + let mut pending = Retransmits::default(); + let mut stream = RecvStream { + id: StreamId::new(Side::Server, Dir::Uni, 127), + state: &mut client, + pending: &mut pending, + }; + stream.stop(0u32.into()).unwrap(); + + // Open stream 128 + assert_eq!( + client.received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 128), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ), + Ok(ShouldTransmit(false)) + ); + } + + #[test] + fn stream_limit_grows() { + let mut client = make(Side::Client); + // Open streams 0-127 + assert_eq!( + client.received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 127), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ), + Ok(ShouldTransmit(false)) + ); + // Try to open stream 128, exceeding limit + assert_eq!( + client + .received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 128), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ) + .unwrap_err() + .code, + TransportErrorCode::STREAM_LIMIT_ERROR + ); + + // Relax limit by one + client.set_max_concurrent(Dir::Uni, 129u32.into()); + + // Open stream 128 + assert_eq!( + client.received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 128), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ), + Ok(ShouldTransmit(false)) + ); + } + + #[test] + fn stream_limit_shrinks() { + let mut client = make(Side::Client); + // Open streams 0-127 + assert_eq!( + client.received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 127), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ), + Ok(ShouldTransmit(false)) + ); + + // Tighten limit by one + client.set_max_concurrent(Dir::Uni, 127u32.into()); + + // Free stream 127 + let mut pending = Retransmits::default(); + let mut stream = RecvStream { + id: StreamId::new(Side::Server, Dir::Uni, 127), + state: &mut client, + pending: &mut pending, + }; + stream.stop(0u32.into()).unwrap(); + + // Try to open stream 128, still exceeding limit + assert_eq!( + client + .received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 128), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ) + .unwrap_err() + .code, + TransportErrorCode::STREAM_LIMIT_ERROR + ); + + // Free stream 126 + assert_eq!( + client.received_reset(frame::ResetStream { + id: StreamId::new(Side::Server, Dir::Uni, 126), + error_code: 0u32.into(), + final_offset: 0u32.into(), + }), + Ok(ShouldTransmit(false)) + ); + let mut pending = Retransmits::default(); + let mut stream = RecvStream { + id: StreamId::new(Side::Server, Dir::Uni, 126), + state: &mut client, + pending: &mut pending, + }; + stream.stop(0u32.into()).unwrap(); + + // Open stream 128 + assert_eq!( + client.received( + frame::Stream { + id: StreamId::new(Side::Server, Dir::Uni, 128), + offset: 0, + fin: true, + data: Bytes::from_static(&[]), + }, + 0 + ), + Ok(ShouldTransmit(false)) + ); + } + + #[test] + fn remote_stream_capacity() { + let mut client = make(Side::Client); + for _ in 0..2 { + client.set_max_concurrent(Dir::Uni, 200u32.into()); + client.set_max_concurrent(Dir::Bi, 201u32.into()); + assert_eq!(client.recv.len(), 200 + 201); + assert_eq!(client.max_remote[Dir::Uni as usize], 200); + assert_eq!(client.max_remote[Dir::Bi as usize], 201); + } + } + + #[test] + fn expand_receive_window() { + let mut server = make(Side::Server); + let new_receive_window = 2 * server.receive_window as u32; + let expanded = server.set_receive_window(new_receive_window.into()); + assert!(expanded); + assert_eq!(server.receive_window, new_receive_window as u64); + assert_eq!(server.local_max_data, new_receive_window as u64); + assert_eq!(server.receive_window_shrink_debt, 0); + let prev_local_max_data = server.local_max_data; + + // credit, expecting all of them added to local_max_data + let credits = 1024u64; + let should_transmit = server.add_read_credits(credits); + assert_eq!(server.receive_window_shrink_debt, 0); + assert_eq!(server.local_max_data, prev_local_max_data + credits); + assert!(should_transmit.should_transmit()); + } + + #[test] + fn shrink_receive_window() { + let mut server = make(Side::Server); + let new_receive_window = server.receive_window as u32 / 2; + let prev_local_max_data = server.local_max_data; + + // shrink the receive_winbow, local_max_data is not expected to be changed + let shrink_diff = server.receive_window - new_receive_window as u64; + let expanded = server.set_receive_window(new_receive_window.into()); + assert!(!expanded); + assert_eq!(server.receive_window, new_receive_window as u64); + assert_eq!(server.local_max_data, prev_local_max_data); + assert_eq!(server.receive_window_shrink_debt, shrink_diff); + let prev_local_max_data = server.local_max_data; + + // credit twice, local_max_data does not change as it is absorbed by receive_window_shrink_debt + let credits = 1024u64; + for _ in 0..2 { + let expected_receive_window_shrink_debt = server.receive_window_shrink_debt - credits; + let should_transmit = server.add_read_credits(credits); + assert_eq!( + server.receive_window_shrink_debt, + expected_receive_window_shrink_debt + ); + assert_eq!(server.local_max_data, prev_local_max_data); + assert!(!should_transmit.should_transmit()); + } + + // credit again which exceeds all remaining expected_receive_window_shrink_debt + let credits = 1024 * 512; + let prev_local_max_data = server.local_max_data; + let expected_local_max_data = + server.local_max_data + (credits - server.receive_window_shrink_debt); + let _should_transmit = server.add_read_credits(credits); + assert_eq!(server.receive_window_shrink_debt, 0); + assert_eq!(server.local_max_data, expected_local_max_data); + assert!(server.local_max_data > prev_local_max_data); + + // credit again, all should be added to local_max_data + let credits = 1024 * 512; + let expected_local_max_data = server.local_max_data + credits; + let should_transmit = server.add_read_credits(credits); + assert_eq!(server.receive_window_shrink_debt, 0); + assert_eq!(server.local_max_data, expected_local_max_data); + assert!(should_transmit.should_transmit()); + } + + #[test] + fn expand_send_window() { + let mut server = make(Side::Server); + + let initial_send_window = server.send_window; + let larger_send_window = initial_send_window * 2; + + // Set `initial_max_data` larger than `send_window` so we're limited by local flow control + server.set_params(&TransportParameters { + initial_max_data: VarInt::MAX, + initial_max_stream_data_uni: VarInt::MAX, + initial_max_streams_uni: VarInt::from_u32(100), + ..TransportParameters::default() + }); + + assert_eq!(server.write_limit(), initial_send_window); + assert_eq!(server.poll(), None); + + let mut retransmits = Retransmits::default(); + let conn_state = ConnState::Established; + + let stream_id = Streams { + state: &mut server, + conn_state: &conn_state, + } + .open(Dir::Uni) + .expect("should be able to open a stream"); + + let mut stream = SendStream { + id: stream_id, + state: &mut server, + pending: &mut retransmits, + conn_state: &conn_state, + }; + + // Check that the stream accepts `initial_send_window` bytes + let initial_send_len = initial_send_window as usize; + let data = vec![0xFFu8; initial_send_len]; + + assert_eq!(stream.write(&data), Ok(initial_send_len)); + + // Try to write the same data again, observe that it's blocked + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + + // Check that we get a `Writable` event after increasing the send window + stream.state.set_send_window(larger_send_window); + assert_eq!( + stream.state.poll(), + Some(StreamEvent::Writable { id: stream_id }) + ); + + // Check that the stream accepts the exact same amount of data again + assert_eq!(stream.write(&data), Ok(initial_send_len)); + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + + assert_eq!(stream.state.poll(), None); + + // Ack the data + stream.state.received_ack_of(frame::StreamMeta { + id: stream_id, + offsets: 0..larger_send_window, + fin: false, + }); + + assert_eq!( + stream.state.poll(), + Some(StreamEvent::Writable { id: stream_id }) + ); + + // Check that our full send window is available again + assert_eq!(stream.write(&data), Ok(initial_send_len)); + assert_eq!(stream.write(&data), Ok(initial_send_len)); + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + } + + #[test] + fn shrink_send_window() { + let mut server = make(Side::Server); + + let initial_send_window = server.send_window; + let smaller_send_window = server.send_window / 2; + + // Set `initial_max_data` larger than `send_window` so we're limited by local flow control + server.set_params(&TransportParameters { + initial_max_data: VarInt::MAX, + initial_max_stream_data_uni: VarInt::MAX, + initial_max_streams_uni: VarInt::from_u32(100), + ..TransportParameters::default() + }); + + assert_eq!(server.write_limit(), initial_send_window); + assert_eq!(server.poll(), None); + + let mut retransmits = Retransmits::default(); + let conn_state = ConnState::Established; + + let stream_id = Streams { + state: &mut server, + conn_state: &conn_state, + } + .open(Dir::Uni) + .expect("should be able to open a stream"); + + let mut stream = SendStream { + id: stream_id, + state: &mut server, + pending: &mut retransmits, + conn_state: &conn_state, + }; + + let initial_send_len = initial_send_window as usize; + + let data = vec![0xFFu8; initial_send_len]; + + // Assert that the full send window is accepted + assert_eq!(stream.write(&data), Ok(initial_send_len)); + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + + assert_eq!(stream.state.write_limit(), 0); + assert_eq!(stream.state.poll(), None); + + // Shrink our send window, assert that it's still not writable + stream.state.set_send_window(smaller_send_window); + assert_eq!(stream.state.write_limit(), 0); + assert_eq!(stream.state.poll(), None); + + // Assert that data is still not accepted + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + + // Ack some data, assert that writes are still not accepted due to outstanding sends + stream.state.received_ack_of(frame::StreamMeta { + id: stream_id, + offsets: 0..smaller_send_window, + fin: false, + }); + + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + + // Ack the rest of the data + stream.state.received_ack_of(frame::StreamMeta { + id: stream_id, + offsets: smaller_send_window..initial_send_window, + fin: false, + }); + + // This should generate a `Writable` event + assert_eq!( + stream.state.poll(), + Some(StreamEvent::Writable { id: stream_id }) + ); + assert_eq!(stream.state.write_limit(), smaller_send_window); + + // Assert that only `smaller_send_window` bytes are accepted + assert_eq!(stream.write(&data), Ok(smaller_send_window as usize)); + assert_eq!(stream.write(&data), Err(WriteError::Blocked)); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/timer.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/timer.rs new file mode 100644 index 0000000..566652d --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/connection/timer.rs @@ -0,0 +1,65 @@ +use crate::Instant; + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) enum Timer { + /// When to send an ack-eliciting probe packet or declare unacked packets lost + LossDetection = 0, + /// When to close the connection after no activity + Idle = 1, + /// When the close timer expires, the connection has been gracefully terminated. + Close = 2, + /// When keys are discarded because they should not be needed anymore + KeyDiscard = 3, + /// When to give up on validating a new path to the peer + PathValidation = 4, + /// When to send a `PING` frame to keep the connection alive + KeepAlive = 5, + /// When pacing will allow us to send a packet + Pacing = 6, + /// When to invalidate old CID and proactively push new one via NEW_CONNECTION_ID frame + PushNewCid = 7, + /// When to send an immediate ACK if there are unacked ack-eliciting packets of the peer + MaxAckDelay = 8, +} + +impl Timer { + pub(crate) const VALUES: [Self; 9] = [ + Self::LossDetection, + Self::Idle, + Self::Close, + Self::KeyDiscard, + Self::PathValidation, + Self::KeepAlive, + Self::Pacing, + Self::PushNewCid, + Self::MaxAckDelay, + ]; +} + +/// A table of data associated with each distinct kind of `Timer` +#[derive(Debug, Copy, Clone, Default)] +pub(crate) struct TimerTable { + data: [Option; 10], +} + +impl TimerTable { + pub(super) fn set(&mut self, timer: Timer, time: Instant) { + self.data[timer as usize] = Some(time); + } + + pub(super) fn get(&self, timer: Timer) -> Option { + self.data[timer as usize] + } + + pub(super) fn stop(&mut self, timer: Timer) { + self.data[timer as usize] = None; + } + + pub(super) fn next_timeout(&self) -> Option { + self.data.iter().filter_map(|&x| x).min() + } + + pub(super) fn is_expired(&self, timer: Timer, after: Instant) -> bool { + self.data[timer as usize].is_some_and(|x| x <= after) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/constant_time.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/constant_time.rs new file mode 100644 index 0000000..94cf6c4 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/constant_time.rs @@ -0,0 +1,22 @@ +// This function is non-inline to prevent the optimizer from looking inside it. +#[inline(never)] +fn constant_time_ne(a: &[u8], b: &[u8]) -> u8 { + assert!(a.len() == b.len()); + + // These useless slices make the optimizer elide the bounds checks. + // See the comment in clone_from_slice() added on Rust commit 6a7bc47. + let len = a.len(); + let a = &a[..len]; + let b = &b[..len]; + + let mut tmp = 0; + for i in 0..len { + tmp |= a[i] ^ b[i]; + } + tmp // The compare with 0 must happen outside this function. +} + +/// Compares byte strings in constant time. +pub(crate) fn eq(a: &[u8], b: &[u8]) -> bool { + a.len() == b.len() && constant_time_ne(a, b) == 0 +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto.rs new file mode 100644 index 0000000..aebd864 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto.rs @@ -0,0 +1,223 @@ +//! Traits and implementations for the QUIC cryptography protocol +//! +//! The protocol logic in Quinn is contained in types that abstract over the actual +//! cryptographic protocol used. This module contains the traits used for this +//! abstraction layer as well as a single implementation of these traits that uses +//! *ring* and rustls to implement the TLS protocol support. +//! +//! Note that usage of any protocol (version) other than TLS 1.3 does not conform to any +//! published versions of the specification, and will not be supported in QUIC v1. + +use std::{any::Any, str, sync::Arc}; + +use bytes::BytesMut; + +use crate::{ + ConnectError, Side, TransportError, shared::ConnectionId, + transport_parameters::TransportParameters, +}; + +/// Cryptography interface based on *ring* +#[cfg(any(feature = "aws-lc-rs", feature = "ring"))] +pub(crate) mod ring_like; +/// TLS interface based on rustls +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +pub mod rustls; + +/// A cryptographic session (commonly TLS) +pub trait Session: Send + Sync + 'static { + /// Create the initial set of keys given the client's initial destination ConnectionId + fn initial_keys(&self, dst_cid: &ConnectionId, side: Side) -> Keys; + + /// Get data negotiated during the handshake, if available + /// + /// Returns `None` until the connection emits `HandshakeDataReady`. + fn handshake_data(&self) -> Option>; + + /// Get the peer's identity, if available + fn peer_identity(&self) -> Option>; + + /// Get the 0-RTT keys if available (clients only) + /// + /// On the client side, this method can be used to see if 0-RTT key material is available + /// to start sending data before the protocol handshake has completed. + /// + /// Returns `None` if the key material is not available. This might happen if you have + /// not connected to this server before. + fn early_crypto(&self) -> Option<(Box, Box)>; + + /// If the 0-RTT-encrypted data has been accepted by the peer + fn early_data_accepted(&self) -> Option; + + /// Returns `true` until the connection is fully established. + fn is_handshaking(&self) -> bool; + + /// Read bytes of handshake data + /// + /// This should be called with the contents of `CRYPTO` frames. If it returns `Ok`, the + /// caller should call `write_handshake()` to check if the crypto protocol has anything + /// to send to the peer. This method will only return `true` the first time that + /// handshake data is available. Future calls will always return false. + /// + /// On success, returns `true` iff `self.handshake_data()` has been populated. + fn read_handshake(&mut self, buf: &[u8]) -> Result; + + /// The peer's QUIC transport parameters + /// + /// These are only available after the first flight from the peer has been received. + fn transport_parameters(&self) -> Result, TransportError>; + + /// Writes handshake bytes into the given buffer and optionally returns the negotiated keys + /// + /// When the handshake proceeds to the next phase, this method will return a new set of + /// keys to encrypt data with. + fn write_handshake(&mut self, buf: &mut Vec) -> Option; + + /// Compute keys for the next key update + fn next_1rtt_keys(&mut self) -> Option>>; + + /// Verify the integrity of a retry packet + fn is_valid_retry(&self, orig_dst_cid: &ConnectionId, header: &[u8], payload: &[u8]) -> bool; + + /// Fill `output` with `output.len()` bytes of keying material derived + /// from the [Session]'s secrets, using `label` and `context` for domain + /// separation. + /// + /// This function will fail, returning [ExportKeyingMaterialError], + /// if the requested output length is too large. + fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> Result<(), ExportKeyingMaterialError>; +} + +/// A pair of keys for bidirectional communication +pub struct KeyPair { + /// Key for encrypting data + pub local: T, + /// Key for decrypting data + pub remote: T, +} + +/// A complete set of keys for a certain packet space +pub struct Keys { + /// Header protection keys + pub header: KeyPair>, + /// Packet protection keys + pub packet: KeyPair>, +} + +/// Client-side configuration for the crypto protocol +pub trait ClientConfig: Send + Sync { + /// Start a client session with this configuration + fn start_session( + self: Arc, + version: u32, + server_name: &str, + params: &TransportParameters, + ) -> Result, ConnectError>; +} + +/// Server-side configuration for the crypto protocol +pub trait ServerConfig: Send + Sync { + /// Create the initial set of keys given the client's initial destination ConnectionId + fn initial_keys( + &self, + version: u32, + dst_cid: &ConnectionId, + ) -> Result; + + /// Generate the integrity tag for a retry packet + /// + /// Never called if `initial_keys` rejected `version`. + fn retry_tag(&self, version: u32, orig_dst_cid: &ConnectionId, packet: &[u8]) -> [u8; 16]; + + /// Start a server session with this configuration + /// + /// Never called if `initial_keys` rejected `version`. + fn start_session( + self: Arc, + version: u32, + params: &TransportParameters, + ) -> Box; +} + +/// Keys used to protect packet payloads +pub trait PacketKey: Send + Sync { + /// Encrypt the packet payload with the given packet number + fn encrypt(&self, packet: u64, buf: &mut [u8], header_len: usize); + /// Decrypt the packet payload with the given packet number + fn decrypt( + &self, + packet: u64, + header: &[u8], + payload: &mut BytesMut, + ) -> Result<(), CryptoError>; + /// The length of the AEAD tag appended to packets on encryption + fn tag_len(&self) -> usize; + /// Maximum number of packets that may be sent using a single key + fn confidentiality_limit(&self) -> u64; + /// Maximum number of incoming packets that may fail decryption before the connection must be + /// abandoned + fn integrity_limit(&self) -> u64; +} + +/// Keys used to protect packet headers +pub trait HeaderKey: Send + Sync { + /// Decrypt the given packet's header + fn decrypt(&self, pn_offset: usize, packet: &mut [u8]); + /// Encrypt the given packet's header + fn encrypt(&self, pn_offset: usize, packet: &mut [u8]); + /// The sample size used for this key's algorithm + fn sample_size(&self) -> usize; +} + +/// A key for signing with HMAC-based algorithms +pub trait HmacKey: Send + Sync { + /// Method for signing a message + fn sign(&self, data: &[u8], signature_out: &mut [u8]); + /// Length of `sign`'s output + fn signature_len(&self) -> usize; + /// Method for verifying a message + fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), CryptoError>; +} + +/// Error returned by [Session::export_keying_material]. +/// +/// This error occurs if the requested output length is too large. +#[derive(Debug, PartialEq, Eq)] +pub struct ExportKeyingMaterialError; + +/// A pseudo random key for HKDF +pub trait HandshakeTokenKey: Send + Sync { + /// Derive AEAD using hkdf + fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Box; +} + +/// A key for sealing data with AEAD-based algorithms +pub trait AeadKey { + /// Method for sealing message `data` + fn seal(&self, data: &mut Vec, additional_data: &[u8]) -> Result<(), CryptoError>; + /// Method for opening a sealed message `data` + fn open<'a>( + &self, + data: &'a mut [u8], + additional_data: &[u8], + ) -> Result<&'a mut [u8], CryptoError>; +} + +/// Generic crypto errors +#[derive(Debug)] +pub struct CryptoError; + +/// Error indicating that the specified QUIC version is not supported +#[derive(Debug)] +pub struct UnsupportedVersion; + +impl From for ConnectError { + fn from(_: UnsupportedVersion) -> Self { + Self::UnsupportedVersion + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto/ring_like.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto/ring_like.rs new file mode 100644 index 0000000..1b5f301 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto/ring_like.rs @@ -0,0 +1,57 @@ +#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] +use aws_lc_rs::{aead, error, hkdf, hmac}; +#[cfg(feature = "ring")] +use ring::{aead, error, hkdf, hmac}; + +use crate::crypto::{self, CryptoError}; + +impl crypto::HmacKey for hmac::Key { + fn sign(&self, data: &[u8], out: &mut [u8]) { + out.copy_from_slice(hmac::sign(self, data).as_ref()); + } + + fn signature_len(&self) -> usize { + 32 + } + + fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), CryptoError> { + Ok(hmac::verify(self, data, signature)?) + } +} + +impl crypto::HandshakeTokenKey for hkdf::Prk { + fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Box { + let mut key_buffer = [0u8; 32]; + let info = [random_bytes]; + let okm = self.expand(&info, hkdf::HKDF_SHA256).unwrap(); + + okm.fill(&mut key_buffer).unwrap(); + + let key = aead::UnboundKey::new(&aead::AES_256_GCM, &key_buffer).unwrap(); + Box::new(aead::LessSafeKey::new(key)) + } +} + +impl crypto::AeadKey for aead::LessSafeKey { + fn seal(&self, data: &mut Vec, additional_data: &[u8]) -> Result<(), CryptoError> { + let aad = aead::Aad::from(additional_data); + let zero_nonce = aead::Nonce::assume_unique_for_key([0u8; 12]); + Ok(self.seal_in_place_append_tag(zero_nonce, aad, data)?) + } + + fn open<'a>( + &self, + data: &'a mut [u8], + additional_data: &[u8], + ) -> Result<&'a mut [u8], CryptoError> { + let aad = aead::Aad::from(additional_data); + let zero_nonce = aead::Nonce::assume_unique_for_key([0u8; 12]); + Ok(self.open_in_place(zero_nonce, aad, data)?) + } +} + +impl From for CryptoError { + fn from(_: error::Unspecified) -> Self { + Self + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto/rustls.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto/rustls.rs new file mode 100644 index 0000000..b45ad3e --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/crypto/rustls.rs @@ -0,0 +1,656 @@ +use std::{any::Any, io, str, sync::Arc}; + +#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] +use aws_lc_rs::aead; +use bytes::BytesMut; +#[cfg(feature = "ring")] +use ring::aead; +pub use rustls::Error; +#[cfg(feature = "__rustls-post-quantum-test")] +use rustls::NamedGroup; +use rustls::{ + self, CipherSuite, + client::danger::ServerCertVerifier, + pki_types::{CertificateDer, PrivateKeyDer, ServerName}, + quic::{Connection, HeaderProtectionKey, KeyChange, PacketKey, Secrets, Suite, Version}, +}; +#[cfg(feature = "platform-verifier")] +use rustls_platform_verifier::BuilderVerifierExt; + +use crate::{ + ConnectError, ConnectionId, Side, TransportError, TransportErrorCode, + crypto::{ + self, CryptoError, ExportKeyingMaterialError, HeaderKey, KeyPair, Keys, UnsupportedVersion, + }, + transport_parameters::TransportParameters, +}; + +impl From for rustls::Side { + fn from(s: Side) -> Self { + match s { + Side::Client => Self::Client, + Side::Server => Self::Server, + } + } +} + +/// A rustls TLS session +pub struct TlsSession { + version: Version, + got_handshake_data: bool, + next_secrets: Option, + inner: Connection, + suite: Suite, +} + +impl TlsSession { + fn side(&self) -> Side { + match self.inner { + Connection::Client(_) => Side::Client, + Connection::Server(_) => Side::Server, + } + } +} + +impl crypto::Session for TlsSession { + fn initial_keys(&self, dst_cid: &ConnectionId, side: Side) -> Keys { + initial_keys(self.version, *dst_cid, side, &self.suite) + } + + fn handshake_data(&self) -> Option> { + if !self.got_handshake_data { + return None; + } + Some(Box::new(HandshakeData { + protocol: self.inner.alpn_protocol().map(|x| x.into()), + server_name: match self.inner { + Connection::Client(_) => None, + Connection::Server(ref session) => session.server_name().map(|x| x.into()), + }, + #[cfg(feature = "__rustls-post-quantum-test")] + negotiated_key_exchange_group: self + .inner + .negotiated_key_exchange_group() + .expect("key exchange group is negotiated") + .name(), + })) + } + + /// For the rustls `TlsSession`, the `Any` type is `Vec` + fn peer_identity(&self) -> Option> { + self.inner.peer_certificates().map(|v| -> Box { + Box::new( + v.iter() + .map(|v| v.clone().into_owned()) + .collect::>>(), + ) + }) + } + + fn early_crypto(&self) -> Option<(Box, Box)> { + let keys = self.inner.zero_rtt_keys()?; + Some((Box::new(keys.header), Box::new(keys.packet))) + } + + fn early_data_accepted(&self) -> Option { + match self.inner { + Connection::Client(ref session) => Some(session.is_early_data_accepted()), + _ => None, + } + } + + fn is_handshaking(&self) -> bool { + self.inner.is_handshaking() + } + + fn read_handshake(&mut self, buf: &[u8]) -> Result { + self.inner.read_hs(buf).map_err(|e| { + if let Some(alert) = self.inner.alert() { + TransportError { + code: TransportErrorCode::crypto(alert.into()), + frame: None, + reason: e.to_string(), + } + } else { + TransportError::PROTOCOL_VIOLATION(format!("TLS error: {e}")) + } + })?; + if !self.got_handshake_data { + // Hack around the lack of an explicit signal from rustls to reflect ClientHello being + // ready on incoming connections, or ALPN negotiation completing on outgoing + // connections. + let have_server_name = match self.inner { + Connection::Client(_) => false, + Connection::Server(ref session) => session.server_name().is_some(), + }; + if self.inner.alpn_protocol().is_some() || have_server_name || !self.is_handshaking() { + self.got_handshake_data = true; + return Ok(true); + } + } + Ok(false) + } + + fn transport_parameters(&self) -> Result, TransportError> { + match self.inner.quic_transport_parameters() { + None => Ok(None), + Some(buf) => match TransportParameters::read(self.side(), &mut io::Cursor::new(buf)) { + Ok(params) => Ok(Some(params)), + Err(e) => Err(e.into()), + }, + } + } + + fn write_handshake(&mut self, buf: &mut Vec) -> Option { + let keys = match self.inner.write_hs(buf)? { + KeyChange::Handshake { keys } => keys, + KeyChange::OneRtt { keys, next } => { + self.next_secrets = Some(next); + keys + } + }; + + Some(Keys { + header: KeyPair { + local: Box::new(keys.local.header), + remote: Box::new(keys.remote.header), + }, + packet: KeyPair { + local: Box::new(keys.local.packet), + remote: Box::new(keys.remote.packet), + }, + }) + } + + fn next_1rtt_keys(&mut self) -> Option>> { + let secrets = self.next_secrets.as_mut()?; + let keys = secrets.next_packet_keys(); + Some(KeyPair { + local: Box::new(keys.local), + remote: Box::new(keys.remote), + }) + } + + fn is_valid_retry(&self, orig_dst_cid: &ConnectionId, header: &[u8], payload: &[u8]) -> bool { + let tag_start = match payload.len().checked_sub(16) { + Some(x) => x, + None => return false, + }; + + let mut pseudo_packet = + Vec::with_capacity(header.len() + payload.len() + orig_dst_cid.len() + 1); + pseudo_packet.push(orig_dst_cid.len() as u8); + pseudo_packet.extend_from_slice(orig_dst_cid); + pseudo_packet.extend_from_slice(header); + let tag_start = tag_start + pseudo_packet.len(); + pseudo_packet.extend_from_slice(payload); + + let (nonce, key) = match self.version { + Version::V1 => (RETRY_INTEGRITY_NONCE_V1, RETRY_INTEGRITY_KEY_V1), + Version::V1Draft => (RETRY_INTEGRITY_NONCE_DRAFT, RETRY_INTEGRITY_KEY_DRAFT), + _ => unreachable!(), + }; + + let nonce = aead::Nonce::assume_unique_for_key(nonce); + let key = aead::LessSafeKey::new(aead::UnboundKey::new(&aead::AES_128_GCM, &key).unwrap()); + + let (aad, tag) = pseudo_packet.split_at_mut(tag_start); + key.open_in_place(nonce, aead::Aad::from(aad), tag).is_ok() + } + + fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> Result<(), ExportKeyingMaterialError> { + self.inner + .export_keying_material(output, label, Some(context)) + .map_err(|_| ExportKeyingMaterialError)?; + Ok(()) + } +} + +const RETRY_INTEGRITY_KEY_DRAFT: [u8; 16] = [ + 0xcc, 0xce, 0x18, 0x7e, 0xd0, 0x9a, 0x09, 0xd0, 0x57, 0x28, 0x15, 0x5a, 0x6c, 0xb9, 0x6b, 0xe1, +]; +const RETRY_INTEGRITY_NONCE_DRAFT: [u8; 12] = [ + 0xe5, 0x49, 0x30, 0xf9, 0x7f, 0x21, 0x36, 0xf0, 0x53, 0x0a, 0x8c, 0x1c, +]; + +const RETRY_INTEGRITY_KEY_V1: [u8; 16] = [ + 0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e, +]; +const RETRY_INTEGRITY_NONCE_V1: [u8; 12] = [ + 0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb, +]; + +impl crypto::HeaderKey for Box { + fn decrypt(&self, pn_offset: usize, packet: &mut [u8]) { + let (header, sample) = packet.split_at_mut(pn_offset + 4); + let (first, rest) = header.split_at_mut(1); + let pn_end = Ord::min(pn_offset + 3, rest.len()); + self.decrypt_in_place( + &sample[..self.sample_size()], + &mut first[0], + &mut rest[pn_offset - 1..pn_end], + ) + .unwrap(); + } + + fn encrypt(&self, pn_offset: usize, packet: &mut [u8]) { + let (header, sample) = packet.split_at_mut(pn_offset + 4); + let (first, rest) = header.split_at_mut(1); + let pn_end = Ord::min(pn_offset + 3, rest.len()); + self.encrypt_in_place( + &sample[..self.sample_size()], + &mut first[0], + &mut rest[pn_offset - 1..pn_end], + ) + .unwrap(); + } + + fn sample_size(&self) -> usize { + self.sample_len() + } +} + +/// Authentication data for (rustls) TLS session +pub struct HandshakeData { + /// The negotiated application protocol, if ALPN is in use + /// + /// Guaranteed to be set if a nonempty list of protocols was specified for this connection. + pub protocol: Option>, + /// The server name specified by the client, if any + /// + /// Always `None` for outgoing connections + pub server_name: Option, + /// The key exchange group negotiated with the peer + #[cfg(feature = "__rustls-post-quantum-test")] + pub negotiated_key_exchange_group: NamedGroup, +} + +/// A QUIC-compatible TLS client configuration +/// +/// Quinn implicitly constructs a `QuicClientConfig` with reasonable defaults within +/// [`ClientConfig::with_root_certificates()`][root_certs] and [`ClientConfig::with_platform_verifier()`][platform]. +/// Alternatively, `QuicClientConfig`'s [`TryFrom`] implementation can be used to wrap around a +/// custom [`rustls::ClientConfig`], in which case care should be taken around certain points: +/// +/// - If `enable_early_data` is not set to true, then sending 0-RTT data will not be possible on +/// outgoing connections. +/// - The [`rustls::ClientConfig`] must have TLS 1.3 support enabled for conversion to succeed. +/// +/// The object in the `resumption` field of the inner [`rustls::ClientConfig`] determines whether +/// calling `into_0rtt` on outgoing connections returns `Ok` or `Err`. It typically allows +/// `into_0rtt` to proceed if it recognizes the server name, and defaults to an in-memory cache of +/// 256 server names. +/// +/// [root_certs]: crate::config::ClientConfig::with_root_certificates() +/// [platform]: crate::config::ClientConfig::with_platform_verifier() +pub struct QuicClientConfig { + pub(crate) inner: Arc, + initial: Suite, +} + +impl QuicClientConfig { + #[cfg(feature = "platform-verifier")] + pub(crate) fn with_platform_verifier() -> Result { + // Keep in sync with `inner()` below + let mut inner = rustls::ClientConfig::builder_with_provider(configured_provider()) + .with_protocol_versions(&[&rustls::version::TLS13]) + .unwrap() // The default providers support TLS 1.3 + .with_platform_verifier()? + .with_no_client_auth(); + + inner.enable_early_data = true; + Ok(Self { + // We're confident that the *ring* default provider contains TLS13_AES_128_GCM_SHA256 + initial: initial_suite_from_provider(inner.crypto_provider()) + .expect("no initial cipher suite found"), + inner: Arc::new(inner), + }) + } + + /// Initialize a sane QUIC-compatible TLS client configuration + /// + /// QUIC requires that TLS 1.3 be enabled. Advanced users can use any [`rustls::ClientConfig`] that + /// satisfies this requirement. + pub(crate) fn new(verifier: Arc) -> Self { + let inner = Self::inner(verifier); + Self { + // We're confident that the *ring* default provider contains TLS13_AES_128_GCM_SHA256 + initial: initial_suite_from_provider(inner.crypto_provider()) + .expect("no initial cipher suite found"), + inner: Arc::new(inner), + } + } + + /// Initialize a QUIC-compatible TLS client configuration with a separate initial cipher suite + /// + /// This is useful if you want to avoid the initial cipher suite for traffic encryption. + pub fn with_initial( + inner: Arc, + initial: Suite, + ) -> Result { + match initial.suite.common.suite { + CipherSuite::TLS13_AES_128_GCM_SHA256 => Ok(Self { inner, initial }), + _ => Err(NoInitialCipherSuite { specific: true }), + } + } + + pub(crate) fn inner(verifier: Arc) -> rustls::ClientConfig { + // Keep in sync with `with_platform_verifier()` above + let mut config = rustls::ClientConfig::builder_with_provider(configured_provider()) + .with_protocol_versions(&[&rustls::version::TLS13]) + .unwrap() // The default providers support TLS 1.3 + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_no_client_auth(); + + config.enable_early_data = true; + config + } +} + +impl crypto::ClientConfig for QuicClientConfig { + fn start_session( + self: Arc, + version: u32, + server_name: &str, + params: &TransportParameters, + ) -> Result, ConnectError> { + let version = interpret_version(version)?; + Ok(Box::new(TlsSession { + version, + got_handshake_data: false, + next_secrets: None, + inner: rustls::quic::Connection::Client( + rustls::quic::ClientConnection::new( + self.inner.clone(), + version, + ServerName::try_from(server_name) + .map_err(|_| ConnectError::InvalidServerName(server_name.into()))? + .to_owned(), + to_vec(params), + ) + .unwrap(), + ), + suite: self.initial, + })) + } +} + +impl TryFrom for QuicClientConfig { + type Error = NoInitialCipherSuite; + + fn try_from(inner: rustls::ClientConfig) -> Result { + Arc::new(inner).try_into() + } +} + +impl TryFrom> for QuicClientConfig { + type Error = NoInitialCipherSuite; + + fn try_from(inner: Arc) -> Result { + Ok(Self { + initial: initial_suite_from_provider(inner.crypto_provider()) + .ok_or(NoInitialCipherSuite { specific: false })?, + inner, + }) + } +} + +/// The initial cipher suite (AES-128-GCM-SHA256) is not available +/// +/// When the cipher suite is supplied `with_initial()`, it must be +/// [`CipherSuite::TLS13_AES_128_GCM_SHA256`]. When the cipher suite is derived from a config's +/// [`CryptoProvider`][provider], that provider must reference a cipher suite with the same ID. +/// +/// [provider]: rustls::crypto::CryptoProvider +#[derive(Clone, Debug)] +pub struct NoInitialCipherSuite { + /// Whether the initial cipher suite was supplied by the caller + specific: bool, +} + +impl std::fmt::Display for NoInitialCipherSuite { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(match self.specific { + true => "invalid cipher suite specified", + false => "no initial cipher suite found", + }) + } +} + +impl std::error::Error for NoInitialCipherSuite {} + +/// A QUIC-compatible TLS server configuration +/// +/// Quinn implicitly constructs a `QuicServerConfig` with reasonable defaults within +/// [`ServerConfig::with_single_cert()`][single]. Alternatively, `QuicServerConfig`'s [`TryFrom`] +/// implementation or `with_initial` method can be used to wrap around a custom +/// [`rustls::ServerConfig`], in which case care should be taken around certain points: +/// +/// - If `max_early_data_size` is not set to `u32::MAX`, the server will not be able to accept +/// incoming 0-RTT data. QUIC prohibits `max_early_data_size` values other than 0 or `u32::MAX`. +/// - The `rustls::ServerConfig` must have TLS 1.3 support enabled for conversion to succeed. +/// +/// [single]: crate::config::ServerConfig::with_single_cert() +pub struct QuicServerConfig { + inner: Arc, + initial: Suite, +} + +impl QuicServerConfig { + pub(crate) fn new( + cert_chain: Vec>, + key: PrivateKeyDer<'static>, + ) -> Result { + let inner = Self::inner(cert_chain, key)?; + Ok(Self { + // We're confident that the *ring* default provider contains TLS13_AES_128_GCM_SHA256 + initial: initial_suite_from_provider(inner.crypto_provider()) + .expect("no initial cipher suite found"), + inner: Arc::new(inner), + }) + } + + /// Initialize a QUIC-compatible TLS client configuration with a separate initial cipher suite + /// + /// This is useful if you want to avoid the initial cipher suite for traffic encryption. + pub fn with_initial( + inner: Arc, + initial: Suite, + ) -> Result { + match initial.suite.common.suite { + CipherSuite::TLS13_AES_128_GCM_SHA256 => Ok(Self { inner, initial }), + _ => Err(NoInitialCipherSuite { specific: true }), + } + } + + /// Initialize a sane QUIC-compatible TLS server configuration + /// + /// QUIC requires that TLS 1.3 be enabled, and that the maximum early data size is either 0 or + /// `u32::MAX`. Advanced users can use any [`rustls::ServerConfig`] that satisfies these + /// requirements. + pub(crate) fn inner( + cert_chain: Vec>, + key: PrivateKeyDer<'static>, + ) -> Result { + let mut inner = rustls::ServerConfig::builder_with_provider(configured_provider()) + .with_protocol_versions(&[&rustls::version::TLS13]) + .unwrap() // The *ring* default provider supports TLS 1.3 + .with_no_client_auth() + .with_single_cert(cert_chain, key)?; + + inner.max_early_data_size = u32::MAX; + Ok(inner) + } +} + +impl TryFrom for QuicServerConfig { + type Error = NoInitialCipherSuite; + + fn try_from(inner: rustls::ServerConfig) -> Result { + Arc::new(inner).try_into() + } +} + +impl TryFrom> for QuicServerConfig { + type Error = NoInitialCipherSuite; + + fn try_from(inner: Arc) -> Result { + Ok(Self { + initial: initial_suite_from_provider(inner.crypto_provider()) + .ok_or(NoInitialCipherSuite { specific: false })?, + inner, + }) + } +} + +impl crypto::ServerConfig for QuicServerConfig { + fn start_session( + self: Arc, + version: u32, + params: &TransportParameters, + ) -> Box { + // Safe: `start_session()` is never called if `initial_keys()` rejected `version` + let version = interpret_version(version).unwrap(); + Box::new(TlsSession { + version, + got_handshake_data: false, + next_secrets: None, + inner: rustls::quic::Connection::Server( + rustls::quic::ServerConnection::new(self.inner.clone(), version, to_vec(params)) + .unwrap(), + ), + suite: self.initial, + }) + } + + fn initial_keys( + &self, + version: u32, + dst_cid: &ConnectionId, + ) -> Result { + let version = interpret_version(version)?; + Ok(initial_keys(version, *dst_cid, Side::Server, &self.initial)) + } + + fn retry_tag(&self, version: u32, orig_dst_cid: &ConnectionId, packet: &[u8]) -> [u8; 16] { + // Safe: `start_session()` is never called if `initial_keys()` rejected `version` + let version = interpret_version(version).unwrap(); + let (nonce, key) = match version { + Version::V1 => (RETRY_INTEGRITY_NONCE_V1, RETRY_INTEGRITY_KEY_V1), + Version::V1Draft => (RETRY_INTEGRITY_NONCE_DRAFT, RETRY_INTEGRITY_KEY_DRAFT), + _ => unreachable!(), + }; + + let mut pseudo_packet = Vec::with_capacity(packet.len() + orig_dst_cid.len() + 1); + pseudo_packet.push(orig_dst_cid.len() as u8); + pseudo_packet.extend_from_slice(orig_dst_cid); + pseudo_packet.extend_from_slice(packet); + + let nonce = aead::Nonce::assume_unique_for_key(nonce); + let key = aead::LessSafeKey::new(aead::UnboundKey::new(&aead::AES_128_GCM, &key).unwrap()); + + let tag = key + .seal_in_place_separate_tag(nonce, aead::Aad::from(pseudo_packet), &mut []) + .unwrap(); + let mut result = [0; 16]; + result.copy_from_slice(tag.as_ref()); + result + } +} + +pub(crate) fn initial_suite_from_provider( + provider: &Arc, +) -> Option { + provider + .cipher_suites + .iter() + .find_map(|cs| match (cs.suite(), cs.tls13()) { + (rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, Some(suite)) => { + Some(suite.quic_suite()) + } + _ => None, + }) + .flatten() +} + +pub(crate) fn configured_provider() -> Arc { + #[cfg(all(feature = "rustls-aws-lc-rs", not(feature = "rustls-ring")))] + let provider = rustls::crypto::aws_lc_rs::default_provider(); + #[cfg(feature = "rustls-ring")] + let provider = rustls::crypto::ring::default_provider(); + Arc::new(provider) +} + +fn to_vec(params: &TransportParameters) -> Vec { + let mut bytes = Vec::new(); + params.write(&mut bytes); + bytes +} + +pub(crate) fn initial_keys( + version: Version, + dst_cid: ConnectionId, + side: Side, + suite: &Suite, +) -> Keys { + let keys = suite.keys(&dst_cid, side.into(), version); + Keys { + header: KeyPair { + local: Box::new(keys.local.header), + remote: Box::new(keys.remote.header), + }, + packet: KeyPair { + local: Box::new(keys.local.packet), + remote: Box::new(keys.remote.packet), + }, + } +} + +impl crypto::PacketKey for Box { + fn encrypt(&self, packet: u64, buf: &mut [u8], header_len: usize) { + let (header, payload_tag) = buf.split_at_mut(header_len); + let (payload, tag_storage) = payload_tag.split_at_mut(payload_tag.len() - self.tag_len()); + let tag = self.encrypt_in_place(packet, &*header, payload).unwrap(); + tag_storage.copy_from_slice(tag.as_ref()); + } + + fn decrypt( + &self, + packet: u64, + header: &[u8], + payload: &mut BytesMut, + ) -> Result<(), CryptoError> { + let plain = self + .decrypt_in_place(packet, header, payload.as_mut()) + .map_err(|_| CryptoError)?; + let plain_len = plain.len(); + payload.truncate(plain_len); + Ok(()) + } + + fn tag_len(&self) -> usize { + (**self).tag_len() + } + + fn confidentiality_limit(&self) -> u64 { + (**self).confidentiality_limit() + } + + fn integrity_limit(&self) -> u64 { + (**self).integrity_limit() + } +} + +fn interpret_version(version: u32) -> Result { + match version { + 0xff00_001d..=0xff00_0020 => Ok(Version::V1Draft), + 0x0000_0001 | 0xff00_0021..=0xff00_0022 => Ok(Version::V1), + _ => Err(UnsupportedVersion), + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/endpoint.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/endpoint.rs new file mode 100644 index 0000000..c046945 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/endpoint.rs @@ -0,0 +1,1331 @@ +use std::{ + collections::{HashMap, hash_map}, + convert::TryFrom, + fmt, mem, + net::{IpAddr, SocketAddr}, + ops::{Index, IndexMut}, + sync::Arc, +}; + +use bytes::{BufMut, Bytes, BytesMut}; +use rand::{Rng, RngCore, SeedableRng, rngs::StdRng}; +use rustc_hash::FxHashMap; +use slab::Slab; +use thiserror::Error; +use tracing::{debug, error, trace, warn}; + +use crate::{ + Duration, INITIAL_MTU, Instant, MAX_CID_SIZE, MIN_INITIAL_SIZE, RESET_TOKEN_SIZE, ResetToken, + Side, Transmit, TransportConfig, TransportError, + cid_generator::ConnectionIdGenerator, + coding::BufMutExt, + config::{ClientConfig, EndpointConfig, ServerConfig}, + connection::{Connection, ConnectionError, SideArgs}, + crypto::{self, Keys, UnsupportedVersion}, + frame, + packet::{ + FixedLengthConnectionIdParser, Header, InitialHeader, InitialPacket, PacketDecodeError, + PacketNumber, PartialDecode, ProtectedInitialHeader, + }, + shared::{ + ConnectionEvent, ConnectionEventInner, ConnectionId, DatagramConnectionEvent, EcnCodepoint, + EndpointEvent, EndpointEventInner, IssuedCid, + }, + token::{IncomingToken, InvalidRetryTokenError, Token, TokenPayload}, + transport_parameters::{PreferredAddress, TransportParameters}, +}; + +/// The main entry point to the library +/// +/// This object performs no I/O whatsoever. Instead, it consumes incoming packets and +/// connection-generated events via `handle` and `handle_event`. +pub struct Endpoint { + rng: StdRng, + index: ConnectionIndex, + connections: Slab, + local_cid_generator: Box, + config: Arc, + server_config: Option>, + /// Whether the underlying UDP socket promises not to fragment packets + allow_mtud: bool, + /// Time at which a stateless reset was most recently sent + last_stateless_reset: Option, + /// Buffered Initial and 0-RTT messages for pending incoming connections + incoming_buffers: Slab, + all_incoming_buffers_total_bytes: u64, +} + +impl Endpoint { + /// Create a new endpoint + /// + /// `allow_mtud` enables path MTU detection when requested by `Connection` configuration for + /// better performance. This requires that outgoing packets are never fragmented, which can be + /// achieved via e.g. the `IPV6_DONTFRAG` socket option. + /// + /// If `rng_seed` is provided, it will be used to initialize the endpoint's rng (having priority + /// over the rng seed configured in [`EndpointConfig`]). Note that the `rng_seed` parameter will + /// be removed in a future release, so prefer setting it to `None` and configuring rng seeds + /// using [`EndpointConfig::rng_seed`]. + pub fn new( + config: Arc, + server_config: Option>, + allow_mtud: bool, + rng_seed: Option<[u8; 32]>, + ) -> Self { + let rng_seed = rng_seed.or(config.rng_seed); + Self { + rng: rng_seed.map_or(StdRng::from_os_rng(), StdRng::from_seed), + index: ConnectionIndex::default(), + connections: Slab::new(), + local_cid_generator: (config.connection_id_generator_factory.as_ref())(), + config, + server_config, + allow_mtud, + last_stateless_reset: None, + incoming_buffers: Slab::new(), + all_incoming_buffers_total_bytes: 0, + } + } + + /// Replace the server configuration, affecting new incoming connections only + pub fn set_server_config(&mut self, server_config: Option>) { + self.server_config = server_config; + } + + /// Process `EndpointEvent`s emitted from related `Connection`s + /// + /// In turn, processing this event may return a `ConnectionEvent` for the same `Connection`. + pub fn handle_event( + &mut self, + ch: ConnectionHandle, + event: EndpointEvent, + ) -> Option { + use EndpointEventInner::*; + match event.0 { + NeedIdentifiers(now, n) => { + return Some(self.send_new_identifiers(now, ch, n)); + } + ResetToken(remote, token) => { + if let Some(old) = self.connections[ch].reset_token.replace((remote, token)) { + self.index.connection_reset_tokens.remove(old.0, old.1); + } + if self.index.connection_reset_tokens.insert(remote, token, ch) { + warn!("duplicate reset token"); + } + } + RetireConnectionId(now, seq, allow_more_cids) => { + if let Some(cid) = self.connections[ch].loc_cids.remove(&seq) { + trace!("peer retired CID {}: {}", seq, cid); + self.index.retire(cid); + if allow_more_cids { + return Some(self.send_new_identifiers(now, ch, 1)); + } + } + } + Drained => { + if let Some(conn) = self.connections.try_remove(ch.0) { + self.index.remove(&conn); + } else { + // This indicates a bug in downstream code, which could cause spurious + // connection loss instead of this error if the CID was (re)allocated prior to + // the illegal call. + error!(id = ch.0, "unknown connection drained"); + } + } + } + None + } + + /// Process an incoming UDP datagram + pub fn handle( + &mut self, + now: Instant, + remote: SocketAddr, + local_ip: Option, + ecn: Option, + data: BytesMut, + buf: &mut Vec, + ) -> Option { + // Partially decode packet or short-circuit if unable + let datagram_len = data.len(); + let event = match PartialDecode::new( + data, + &FixedLengthConnectionIdParser::new(self.local_cid_generator.cid_len()), + &self.config.supported_versions, + self.config.grease_quic_bit, + ) { + Ok((first_decode, remaining)) => DatagramConnectionEvent { + now, + remote, + ecn, + first_decode, + remaining, + }, + Err(PacketDecodeError::UnsupportedVersion { + src_cid, + dst_cid, + version, + }) => { + if self.server_config.is_none() { + debug!("dropping packet with unsupported version"); + return None; + } + trace!("sending version negotiation"); + // Negotiate versions + Header::VersionNegotiate { + random: self.rng.random::() | 0x40, + src_cid: dst_cid, + dst_cid: src_cid, + } + .encode(buf); + // Grease with a reserved version + buf.write::(match version { + 0x0a1a_2a3a => 0x0a1a_2a4a, + _ => 0x0a1a_2a3a, + }); + for &version in &self.config.supported_versions { + buf.write(version); + } + return Some(DatagramEvent::Response(Transmit { + destination: remote, + ecn: None, + size: buf.len(), + segment_size: None, + src_ip: local_ip, + })); + } + Err(e) => { + trace!("malformed header: {}", e); + return None; + } + }; + + let addresses = FourTuple { remote, local_ip }; + let dst_cid = event.first_decode.dst_cid(); + + if let Some(route_to) = self.index.get(&addresses, &event.first_decode) { + // Handle packet on existing connection + match route_to { + RouteDatagramTo::Incoming(incoming_idx) => { + let incoming_buffer = &mut self.incoming_buffers[incoming_idx]; + let config = &self.server_config.as_ref().unwrap(); + + if incoming_buffer + .total_bytes + .checked_add(datagram_len as u64) + .is_some_and(|n| n <= config.incoming_buffer_size) + && self + .all_incoming_buffers_total_bytes + .checked_add(datagram_len as u64) + .is_some_and(|n| n <= config.incoming_buffer_size_total) + { + incoming_buffer.datagrams.push(event); + incoming_buffer.total_bytes += datagram_len as u64; + self.all_incoming_buffers_total_bytes += datagram_len as u64; + } + + None + } + RouteDatagramTo::Connection(ch) => Some(DatagramEvent::ConnectionEvent( + ch, + ConnectionEvent(ConnectionEventInner::Datagram(event)), + )), + } + } else if event.first_decode.initial_header().is_some() { + // Potentially create a new connection + + self.handle_first_packet(datagram_len, event, addresses, buf) + } else if event.first_decode.has_long_header() { + debug!( + "ignoring non-initial packet for unknown connection {}", + dst_cid + ); + None + } else if !event.first_decode.is_initial() + && self.local_cid_generator.validate(dst_cid).is_err() + { + debug!("dropping packet with invalid CID"); + None + } else if dst_cid.is_empty() { + trace!("dropping unrecognized short packet without ID"); + None + } else { + // If we got this far, we're receiving a seemingly valid packet for an unknown + // connection. Send a stateless reset if possible. + self.stateless_reset(now, datagram_len, addresses, *dst_cid, buf) + .map(DatagramEvent::Response) + } + } + + fn stateless_reset( + &mut self, + now: Instant, + inciting_dgram_len: usize, + addresses: FourTuple, + dst_cid: ConnectionId, + buf: &mut Vec, + ) -> Option { + if self + .last_stateless_reset + .is_some_and(|last| last + self.config.min_reset_interval > now) + { + debug!("ignoring unexpected packet within minimum stateless reset interval"); + return None; + } + + /// Minimum amount of padding for the stateless reset to look like a short-header packet + const MIN_PADDING_LEN: usize = 5; + + // Prevent amplification attacks and reset loops by ensuring we pad to at most 1 byte + // smaller than the inciting packet. + let max_padding_len = match inciting_dgram_len.checked_sub(RESET_TOKEN_SIZE) { + Some(headroom) if headroom > MIN_PADDING_LEN => headroom - 1, + _ => { + debug!( + "ignoring unexpected {} byte packet: not larger than minimum stateless reset size", + inciting_dgram_len + ); + return None; + } + }; + + debug!( + "sending stateless reset for {} to {}", + dst_cid, addresses.remote + ); + self.last_stateless_reset = Some(now); + // Resets with at least this much padding can't possibly be distinguished from real packets + const IDEAL_MIN_PADDING_LEN: usize = MIN_PADDING_LEN + MAX_CID_SIZE; + let padding_len = if max_padding_len <= IDEAL_MIN_PADDING_LEN { + max_padding_len + } else { + self.rng + .random_range(IDEAL_MIN_PADDING_LEN..max_padding_len) + }; + buf.reserve(padding_len + RESET_TOKEN_SIZE); + buf.resize(padding_len, 0); + self.rng.fill_bytes(&mut buf[0..padding_len]); + buf[0] = 0b0100_0000 | (buf[0] >> 2); + buf.extend_from_slice(&ResetToken::new(&*self.config.reset_key, dst_cid)); + + debug_assert!(buf.len() < inciting_dgram_len); + + Some(Transmit { + destination: addresses.remote, + ecn: None, + size: buf.len(), + segment_size: None, + src_ip: addresses.local_ip, + }) + } + + /// Initiate a connection + pub fn connect( + &mut self, + now: Instant, + config: ClientConfig, + remote: SocketAddr, + server_name: &str, + ) -> Result<(ConnectionHandle, Connection), ConnectError> { + if self.cids_exhausted() { + return Err(ConnectError::CidsExhausted); + } + if remote.port() == 0 || remote.ip().is_unspecified() { + return Err(ConnectError::InvalidRemoteAddress(remote)); + } + if !self.config.supported_versions.contains(&config.version) { + return Err(ConnectError::UnsupportedVersion); + } + + let remote_id = (config.initial_dst_cid_provider)(); + trace!(initial_dcid = %remote_id); + + let ch = ConnectionHandle(self.connections.vacant_key()); + let loc_cid = self.new_cid(ch); + let params = TransportParameters::new( + &config.transport, + &self.config, + self.local_cid_generator.as_ref(), + loc_cid, + None, + &mut self.rng, + ); + let tls = config + .crypto + .start_session(config.version, server_name, ¶ms)?; + + let conn = self.add_connection( + ch, + config.version, + remote_id, + loc_cid, + remote_id, + FourTuple { + remote, + local_ip: None, + }, + now, + tls, + config.transport, + SideArgs::Client { + token_store: config.token_store, + server_name: server_name.into(), + }, + ); + Ok((ch, conn)) + } + + fn send_new_identifiers( + &mut self, + now: Instant, + ch: ConnectionHandle, + num: u64, + ) -> ConnectionEvent { + let mut ids = vec![]; + for _ in 0..num { + let id = self.new_cid(ch); + let meta = &mut self.connections[ch]; + let sequence = meta.cids_issued; + meta.cids_issued += 1; + meta.loc_cids.insert(sequence, id); + ids.push(IssuedCid { + sequence, + id, + reset_token: ResetToken::new(&*self.config.reset_key, id), + }); + } + ConnectionEvent(ConnectionEventInner::NewIdentifiers(ids, now)) + } + + /// Generate a connection ID for `ch` + fn new_cid(&mut self, ch: ConnectionHandle) -> ConnectionId { + loop { + let cid = self.local_cid_generator.generate_cid(); + if cid.is_empty() { + // Zero-length CID; nothing to track + debug_assert_eq!(self.local_cid_generator.cid_len(), 0); + return cid; + } + if let hash_map::Entry::Vacant(e) = self.index.connection_ids.entry(cid) { + e.insert(ch); + break cid; + } + } + } + + fn handle_first_packet( + &mut self, + datagram_len: usize, + event: DatagramConnectionEvent, + addresses: FourTuple, + buf: &mut Vec, + ) -> Option { + let dst_cid = event.first_decode.dst_cid(); + let header = event.first_decode.initial_header().unwrap(); + + let Some(server_config) = &self.server_config else { + debug!("packet for unrecognized connection {}", dst_cid); + return self + .stateless_reset(event.now, datagram_len, addresses, *dst_cid, buf) + .map(DatagramEvent::Response); + }; + + if datagram_len < MIN_INITIAL_SIZE as usize { + debug!("ignoring short initial for connection {}", dst_cid); + return None; + } + + let crypto = match server_config.crypto.initial_keys(header.version, dst_cid) { + Ok(keys) => keys, + Err(UnsupportedVersion) => { + // This probably indicates that the user set supported_versions incorrectly in + // `EndpointConfig`. + debug!( + "ignoring initial packet version {:#x} unsupported by cryptographic layer", + header.version + ); + return None; + } + }; + + if let Err(reason) = self.early_validate_first_packet(header) { + return Some(DatagramEvent::Response(self.initial_close( + header.version, + addresses, + &crypto, + &header.src_cid, + reason, + buf, + ))); + } + + let packet = match event.first_decode.finish(Some(&*crypto.header.remote)) { + Ok(packet) => packet, + Err(e) => { + trace!("unable to decode initial packet: {}", e); + return None; + } + }; + + if !packet.reserved_bits_valid() { + debug!("dropping connection attempt with invalid reserved bits"); + return None; + } + + let Header::Initial(header) = packet.header else { + panic!("non-initial packet in handle_first_packet()"); + }; + + let server_config = self.server_config.as_ref().unwrap().clone(); + + let token = match IncomingToken::from_header(&header, &server_config, addresses.remote) { + Ok(token) => token, + Err(InvalidRetryTokenError) => { + debug!("rejecting invalid retry token"); + return Some(DatagramEvent::Response(self.initial_close( + header.version, + addresses, + &crypto, + &header.src_cid, + TransportError::INVALID_TOKEN(""), + buf, + ))); + } + }; + + let incoming_idx = self.incoming_buffers.insert(IncomingBuffer::default()); + self.index + .insert_initial_incoming(header.dst_cid, incoming_idx); + + Some(DatagramEvent::NewConnection(Incoming { + received_at: event.now, + addresses, + ecn: event.ecn, + packet: InitialPacket { + header, + header_data: packet.header_data, + payload: packet.payload, + }, + rest: event.remaining, + crypto, + token, + incoming_idx, + improper_drop_warner: IncomingImproperDropWarner, + })) + } + + /// Attempt to accept this incoming connection (an error may still occur) + // AcceptError cannot be made smaller without semver breakage + #[allow(clippy::result_large_err)] + pub fn accept( + &mut self, + mut incoming: Incoming, + now: Instant, + buf: &mut Vec, + server_config: Option>, + ) -> Result<(ConnectionHandle, Connection), AcceptError> { + let remote_address_validated = incoming.remote_address_validated(); + incoming.improper_drop_warner.dismiss(); + let incoming_buffer = self.incoming_buffers.remove(incoming.incoming_idx); + self.all_incoming_buffers_total_bytes -= incoming_buffer.total_bytes; + + let packet_number = incoming.packet.header.number.expand(0); + let InitialHeader { + src_cid, + dst_cid, + version, + .. + } = incoming.packet.header; + let server_config = + server_config.unwrap_or_else(|| self.server_config.as_ref().unwrap().clone()); + + if server_config + .transport + .max_idle_timeout + .is_some_and(|timeout| { + incoming.received_at + Duration::from_millis(timeout.into()) <= now + }) + { + debug!("abandoning accept of stale initial"); + self.index.remove_initial(dst_cid); + return Err(AcceptError { + cause: ConnectionError::TimedOut, + response: None, + }); + } + + if self.cids_exhausted() { + debug!("refusing connection"); + self.index.remove_initial(dst_cid); + return Err(AcceptError { + cause: ConnectionError::CidsExhausted, + response: Some(self.initial_close( + version, + incoming.addresses, + &incoming.crypto, + &src_cid, + TransportError::CONNECTION_REFUSED(""), + buf, + )), + }); + } + + if incoming + .crypto + .packet + .remote + .decrypt( + packet_number, + &incoming.packet.header_data, + &mut incoming.packet.payload, + ) + .is_err() + { + debug!(packet_number, "failed to authenticate initial packet"); + self.index.remove_initial(dst_cid); + return Err(AcceptError { + cause: TransportError::PROTOCOL_VIOLATION("authentication failed").into(), + response: None, + }); + }; + + let ch = ConnectionHandle(self.connections.vacant_key()); + let loc_cid = self.new_cid(ch); + let mut params = TransportParameters::new( + &server_config.transport, + &self.config, + self.local_cid_generator.as_ref(), + loc_cid, + Some(&server_config), + &mut self.rng, + ); + params.stateless_reset_token = Some(ResetToken::new(&*self.config.reset_key, loc_cid)); + params.original_dst_cid = Some(incoming.token.orig_dst_cid); + params.retry_src_cid = incoming.token.retry_src_cid; + let mut pref_addr_cid = None; + if server_config.has_preferred_address() { + let cid = self.new_cid(ch); + pref_addr_cid = Some(cid); + params.preferred_address = Some(PreferredAddress { + address_v4: server_config.preferred_address_v4, + address_v6: server_config.preferred_address_v6, + connection_id: cid, + stateless_reset_token: ResetToken::new(&*self.config.reset_key, cid), + }); + } + + let tls = server_config.crypto.clone().start_session(version, ¶ms); + let transport_config = server_config.transport.clone(); + let mut conn = self.add_connection( + ch, + version, + dst_cid, + loc_cid, + src_cid, + incoming.addresses, + incoming.received_at, + tls, + transport_config, + SideArgs::Server { + server_config, + pref_addr_cid, + path_validated: remote_address_validated, + }, + ); + self.index.insert_initial(dst_cid, ch); + + match conn.handle_first_packet( + incoming.received_at, + incoming.addresses.remote, + incoming.ecn, + packet_number, + incoming.packet, + incoming.rest, + ) { + Ok(()) => { + trace!(id = ch.0, icid = %dst_cid, "new connection"); + + for event in incoming_buffer.datagrams { + conn.handle_event(ConnectionEvent(ConnectionEventInner::Datagram(event))) + } + + Ok((ch, conn)) + } + Err(e) => { + debug!("handshake failed: {}", e); + self.handle_event(ch, EndpointEvent(EndpointEventInner::Drained)); + let response = match e { + ConnectionError::TransportError(ref e) => Some(self.initial_close( + version, + incoming.addresses, + &incoming.crypto, + &src_cid, + e.clone(), + buf, + )), + _ => None, + }; + Err(AcceptError { cause: e, response }) + } + } + } + + /// Check if we should refuse a connection attempt regardless of the packet's contents + fn early_validate_first_packet( + &mut self, + header: &ProtectedInitialHeader, + ) -> Result<(), TransportError> { + let config = &self.server_config.as_ref().unwrap(); + if self.cids_exhausted() || self.incoming_buffers.len() >= config.max_incoming { + return Err(TransportError::CONNECTION_REFUSED("")); + } + + // RFC9000 §7.2 dictates that initial (client-chosen) destination CIDs must be at least 8 + // bytes. If this is a Retry packet, then the length must instead match our usual CID + // length. If we ever issue non-Retry address validation tokens via `NEW_TOKEN`, then we'll + // also need to validate CID length for those after decoding the token. + if header.dst_cid.len() < 8 + && (header.token_pos.is_empty() + || header.dst_cid.len() != self.local_cid_generator.cid_len()) + { + debug!( + "rejecting connection due to invalid DCID length {}", + header.dst_cid.len() + ); + return Err(TransportError::PROTOCOL_VIOLATION( + "invalid destination CID length", + )); + } + + Ok(()) + } + + /// Reject this incoming connection attempt + pub fn refuse(&mut self, incoming: Incoming, buf: &mut Vec) -> Transmit { + self.clean_up_incoming(&incoming); + incoming.improper_drop_warner.dismiss(); + + self.initial_close( + incoming.packet.header.version, + incoming.addresses, + &incoming.crypto, + &incoming.packet.header.src_cid, + TransportError::CONNECTION_REFUSED(""), + buf, + ) + } + + /// Respond with a retry packet, requiring the client to retry with address validation + /// + /// Errors if `incoming.may_retry()` is false. + pub fn retry(&mut self, incoming: Incoming, buf: &mut Vec) -> Result { + if !incoming.may_retry() { + return Err(RetryError(Box::new(incoming))); + } + + self.clean_up_incoming(&incoming); + incoming.improper_drop_warner.dismiss(); + + let server_config = self.server_config.as_ref().unwrap(); + + // First Initial + // The peer will use this as the DCID of its following Initials. Initial DCIDs are + // looked up separately from Handshake/Data DCIDs, so there is no risk of collision + // with established connections. In the unlikely event that a collision occurs + // between two connections in the initial phase, both will fail fast and may be + // retried by the application layer. + let loc_cid = self.local_cid_generator.generate_cid(); + + let payload = TokenPayload::Retry { + address: incoming.addresses.remote, + orig_dst_cid: incoming.packet.header.dst_cid, + issued: server_config.time_source.now(), + }; + let token = Token::new(payload, &mut self.rng).encode(&*server_config.token_key); + + let header = Header::Retry { + src_cid: loc_cid, + dst_cid: incoming.packet.header.src_cid, + version: incoming.packet.header.version, + }; + + let encode = header.encode(buf); + buf.put_slice(&token); + buf.extend_from_slice(&server_config.crypto.retry_tag( + incoming.packet.header.version, + &incoming.packet.header.dst_cid, + buf, + )); + encode.finish(buf, &*incoming.crypto.header.local, None); + + Ok(Transmit { + destination: incoming.addresses.remote, + ecn: None, + size: buf.len(), + segment_size: None, + src_ip: incoming.addresses.local_ip, + }) + } + + /// Ignore this incoming connection attempt, not sending any packet in response + /// + /// Doing this actively, rather than merely dropping the [`Incoming`], is necessary to prevent + /// memory leaks due to state within [`Endpoint`] tracking the incoming connection. + pub fn ignore(&mut self, incoming: Incoming) { + self.clean_up_incoming(&incoming); + incoming.improper_drop_warner.dismiss(); + } + + /// Clean up endpoint data structures associated with an `Incoming`. + fn clean_up_incoming(&mut self, incoming: &Incoming) { + self.index.remove_initial(incoming.packet.header.dst_cid); + let incoming_buffer = self.incoming_buffers.remove(incoming.incoming_idx); + self.all_incoming_buffers_total_bytes -= incoming_buffer.total_bytes; + } + + fn add_connection( + &mut self, + ch: ConnectionHandle, + version: u32, + init_cid: ConnectionId, + loc_cid: ConnectionId, + rem_cid: ConnectionId, + addresses: FourTuple, + now: Instant, + tls: Box, + transport_config: Arc, + side_args: SideArgs, + ) -> Connection { + let mut rng_seed = [0; 32]; + self.rng.fill_bytes(&mut rng_seed); + let side = side_args.side(); + let pref_addr_cid = side_args.pref_addr_cid(); + let conn = Connection::new( + self.config.clone(), + transport_config, + init_cid, + loc_cid, + rem_cid, + addresses.remote, + addresses.local_ip, + tls, + self.local_cid_generator.as_ref(), + now, + version, + self.allow_mtud, + rng_seed, + side_args, + ); + + let mut cids_issued = 0; + let mut loc_cids = FxHashMap::default(); + + loc_cids.insert(cids_issued, loc_cid); + cids_issued += 1; + + if let Some(cid) = pref_addr_cid { + debug_assert_eq!(cids_issued, 1, "preferred address cid seq must be 1"); + loc_cids.insert(cids_issued, cid); + cids_issued += 1; + } + + let id = self.connections.insert(ConnectionMeta { + init_cid, + cids_issued, + loc_cids, + addresses, + side, + reset_token: None, + }); + debug_assert_eq!(id, ch.0, "connection handle allocation out of sync"); + + self.index.insert_conn(addresses, loc_cid, ch, side); + + conn + } + + fn initial_close( + &mut self, + version: u32, + addresses: FourTuple, + crypto: &Keys, + remote_id: &ConnectionId, + reason: TransportError, + buf: &mut Vec, + ) -> Transmit { + // We don't need to worry about CID collisions in initial closes because the peer + // shouldn't respond, and if it does, and the CID collides, we'll just drop the + // unexpected response. + let local_id = self.local_cid_generator.generate_cid(); + let number = PacketNumber::U8(0); + let header = Header::Initial(InitialHeader { + dst_cid: *remote_id, + src_cid: local_id, + number, + token: Bytes::new(), + version, + }); + + let partial_encode = header.encode(buf); + let max_len = + INITIAL_MTU as usize - partial_encode.header_len - crypto.packet.local.tag_len(); + frame::Close::from(reason).encode(buf, max_len); + buf.resize(buf.len() + crypto.packet.local.tag_len(), 0); + partial_encode.finish(buf, &*crypto.header.local, Some((0, &*crypto.packet.local))); + Transmit { + destination: addresses.remote, + ecn: None, + size: buf.len(), + segment_size: None, + src_ip: addresses.local_ip, + } + } + + /// Access the configuration used by this endpoint + pub fn config(&self) -> &EndpointConfig { + &self.config + } + + /// Number of connections that are currently open + pub fn open_connections(&self) -> usize { + self.connections.len() + } + + /// Counter for the number of bytes currently used + /// in the buffers for Initial and 0-RTT messages for pending incoming connections + pub fn incoming_buffer_bytes(&self) -> u64 { + self.all_incoming_buffers_total_bytes + } + + #[cfg(test)] + pub(crate) fn known_connections(&self) -> usize { + let x = self.connections.len(); + debug_assert_eq!(x, self.index.connection_ids_initial.len()); + // Not all connections have known reset tokens + debug_assert!(x >= self.index.connection_reset_tokens.0.len()); + // Not all connections have unique remotes, and 0-length CIDs might not be in use. + debug_assert!(x >= self.index.incoming_connection_remotes.len()); + debug_assert!(x >= self.index.outgoing_connection_remotes.len()); + x + } + + #[cfg(test)] + pub(crate) fn known_cids(&self) -> usize { + self.index.connection_ids.len() + } + + /// Whether we've used up 3/4 of the available CID space + /// + /// We leave some space unused so that `new_cid` can be relied upon to finish quickly. We don't + /// bother to check when CID longer than 4 bytes are used because 2^40 connections is a lot. + fn cids_exhausted(&self) -> bool { + self.local_cid_generator.cid_len() <= 4 + && self.local_cid_generator.cid_len() != 0 + && (2usize.pow(self.local_cid_generator.cid_len() as u32 * 8) + - self.index.connection_ids.len()) + < 2usize.pow(self.local_cid_generator.cid_len() as u32 * 8 - 2) + } +} + +impl fmt::Debug for Endpoint { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("Endpoint") + .field("rng", &self.rng) + .field("index", &self.index) + .field("connections", &self.connections) + .field("config", &self.config) + .field("server_config", &self.server_config) + // incoming_buffers too large + .field("incoming_buffers.len", &self.incoming_buffers.len()) + .field( + "all_incoming_buffers_total_bytes", + &self.all_incoming_buffers_total_bytes, + ) + .finish() + } +} + +/// Buffered Initial and 0-RTT messages for a pending incoming connection +#[derive(Default)] +struct IncomingBuffer { + datagrams: Vec, + total_bytes: u64, +} + +/// Part of protocol state incoming datagrams can be routed to +#[derive(Copy, Clone, Debug)] +enum RouteDatagramTo { + Incoming(usize), + Connection(ConnectionHandle), +} + +/// Maps packets to existing connections +#[derive(Default, Debug)] +struct ConnectionIndex { + /// Identifies connections based on the initial DCID the peer utilized + /// + /// Uses a standard `HashMap` to protect against hash collision attacks. + /// + /// Used by the server, not the client. + connection_ids_initial: HashMap, + /// Identifies connections based on locally created CIDs + /// + /// Uses a cheaper hash function since keys are locally created + connection_ids: FxHashMap, + /// Identifies incoming connections with zero-length CIDs + /// + /// Uses a standard `HashMap` to protect against hash collision attacks. + incoming_connection_remotes: HashMap, + /// Identifies outgoing connections with zero-length CIDs + /// + /// We don't yet support explicit source addresses for client connections, and zero-length CIDs + /// require a unique four-tuple, so at most one client connection with zero-length local CIDs + /// may be established per remote. We must omit the local address from the key because we don't + /// necessarily know what address we're sending from, and hence receiving at. + /// + /// Uses a standard `HashMap` to protect against hash collision attacks. + outgoing_connection_remotes: HashMap, + /// Reset tokens provided by the peer for the CID each connection is currently sending to + /// + /// Incoming stateless resets do not have correct CIDs, so we need this to identify the correct + /// recipient, if any. + connection_reset_tokens: ResetTokenTable, +} + +impl ConnectionIndex { + /// Associate an incoming connection with its initial destination CID + fn insert_initial_incoming(&mut self, dst_cid: ConnectionId, incoming_key: usize) { + if dst_cid.is_empty() { + return; + } + self.connection_ids_initial + .insert(dst_cid, RouteDatagramTo::Incoming(incoming_key)); + } + + /// Remove an association with an initial destination CID + fn remove_initial(&mut self, dst_cid: ConnectionId) { + if dst_cid.is_empty() { + return; + } + let removed = self.connection_ids_initial.remove(&dst_cid); + debug_assert!(removed.is_some()); + } + + /// Associate a connection with its initial destination CID + fn insert_initial(&mut self, dst_cid: ConnectionId, connection: ConnectionHandle) { + if dst_cid.is_empty() { + return; + } + self.connection_ids_initial + .insert(dst_cid, RouteDatagramTo::Connection(connection)); + } + + /// Associate a connection with its first locally-chosen destination CID if used, or otherwise + /// its current 4-tuple + fn insert_conn( + &mut self, + addresses: FourTuple, + dst_cid: ConnectionId, + connection: ConnectionHandle, + side: Side, + ) { + match dst_cid.len() { + 0 => match side { + Side::Server => { + self.incoming_connection_remotes + .insert(addresses, connection); + } + Side::Client => { + self.outgoing_connection_remotes + .insert(addresses.remote, connection); + } + }, + _ => { + self.connection_ids.insert(dst_cid, connection); + } + } + } + + /// Discard a connection ID + fn retire(&mut self, dst_cid: ConnectionId) { + self.connection_ids.remove(&dst_cid); + } + + /// Remove all references to a connection + fn remove(&mut self, conn: &ConnectionMeta) { + if conn.side.is_server() { + self.remove_initial(conn.init_cid); + } + for cid in conn.loc_cids.values() { + self.connection_ids.remove(cid); + } + self.incoming_connection_remotes.remove(&conn.addresses); + self.outgoing_connection_remotes + .remove(&conn.addresses.remote); + if let Some((remote, token)) = conn.reset_token { + self.connection_reset_tokens.remove(remote, token); + } + } + + /// Find the existing connection that `datagram` should be routed to, if any + fn get(&self, addresses: &FourTuple, datagram: &PartialDecode) -> Option { + if !datagram.dst_cid().is_empty() { + if let Some(&ch) = self.connection_ids.get(datagram.dst_cid()) { + return Some(RouteDatagramTo::Connection(ch)); + } + } + if datagram.is_initial() || datagram.is_0rtt() { + if let Some(&ch) = self.connection_ids_initial.get(datagram.dst_cid()) { + return Some(ch); + } + } + if datagram.dst_cid().is_empty() { + if let Some(&ch) = self.incoming_connection_remotes.get(addresses) { + return Some(RouteDatagramTo::Connection(ch)); + } + if let Some(&ch) = self.outgoing_connection_remotes.get(&addresses.remote) { + return Some(RouteDatagramTo::Connection(ch)); + } + } + let data = datagram.data(); + if data.len() < RESET_TOKEN_SIZE { + return None; + } + self.connection_reset_tokens + .get(addresses.remote, &data[data.len() - RESET_TOKEN_SIZE..]) + .cloned() + .map(RouteDatagramTo::Connection) + } +} + +#[derive(Debug)] +pub(crate) struct ConnectionMeta { + init_cid: ConnectionId, + /// Number of local connection IDs that have been issued in NEW_CONNECTION_ID frames. + cids_issued: u64, + loc_cids: FxHashMap, + /// Remote/local addresses the connection began with + /// + /// Only needed to support connections with zero-length CIDs, which cannot migrate, so we don't + /// bother keeping it up to date. + addresses: FourTuple, + side: Side, + /// Reset token provided by the peer for the CID we're currently sending to, and the address + /// being sent to + reset_token: Option<(SocketAddr, ResetToken)>, +} + +/// Internal identifier for a `Connection` currently associated with an endpoint +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct ConnectionHandle(pub usize); + +impl From for usize { + fn from(x: ConnectionHandle) -> Self { + x.0 + } +} + +impl Index for Slab { + type Output = ConnectionMeta; + fn index(&self, ch: ConnectionHandle) -> &ConnectionMeta { + &self[ch.0] + } +} + +impl IndexMut for Slab { + fn index_mut(&mut self, ch: ConnectionHandle) -> &mut ConnectionMeta { + &mut self[ch.0] + } +} + +/// Event resulting from processing a single datagram +pub enum DatagramEvent { + /// The datagram is redirected to its `Connection` + ConnectionEvent(ConnectionHandle, ConnectionEvent), + /// The datagram may result in starting a new `Connection` + NewConnection(Incoming), + /// Response generated directly by the endpoint + Response(Transmit), +} + +/// An incoming connection for which the server has not yet begun its part of the handshake. +pub struct Incoming { + received_at: Instant, + addresses: FourTuple, + ecn: Option, + packet: InitialPacket, + rest: Option, + crypto: Keys, + token: IncomingToken, + incoming_idx: usize, + improper_drop_warner: IncomingImproperDropWarner, +} + +impl Incoming { + /// The local IP address which was used when the peer established the connection + /// + /// This has the same behavior as [`Connection::local_ip`]. + pub fn local_ip(&self) -> Option { + self.addresses.local_ip + } + + /// The peer's UDP address + pub fn remote_address(&self) -> SocketAddr { + self.addresses.remote + } + + /// Whether the socket address that is initiating this connection has been validated + /// + /// This means that the sender of the initial packet has proved that they can receive traffic + /// sent to `self.remote_address()`. + /// + /// If `self.remote_address_validated()` is false, `self.may_retry()` is guaranteed to be true. + /// The inverse is not guaranteed. + pub fn remote_address_validated(&self) -> bool { + self.token.validated + } + + /// Whether it is legal to respond with a retry packet + /// + /// If `self.remote_address_validated()` is false, `self.may_retry()` is guaranteed to be true. + /// The inverse is not guaranteed. + pub fn may_retry(&self) -> bool { + self.token.retry_src_cid.is_none() + } + + /// The original destination connection ID sent by the client + pub fn orig_dst_cid(&self) -> &ConnectionId { + &self.token.orig_dst_cid + } +} + +impl fmt::Debug for Incoming { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Incoming") + .field("addresses", &self.addresses) + .field("ecn", &self.ecn) + // packet doesn't implement debug + // rest is too big and not meaningful enough + .field("token", &self.token) + .field("incoming_idx", &self.incoming_idx) + // improper drop warner contains no information + .finish_non_exhaustive() + } +} + +struct IncomingImproperDropWarner; + +impl IncomingImproperDropWarner { + fn dismiss(self) { + mem::forget(self); + } +} + +impl Drop for IncomingImproperDropWarner { + fn drop(&mut self) { + warn!( + "quinn_proto::Incoming dropped without passing to Endpoint::accept/refuse/retry/ignore \ + (may cause memory leak and eventual inability to accept new connections)" + ); + } +} + +/// Errors in the parameters being used to create a new connection +/// +/// These arise before any I/O has been performed. +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ConnectError { + /// The endpoint can no longer create new connections + /// + /// Indicates that a necessary component of the endpoint has been dropped or otherwise disabled. + #[error("endpoint stopping")] + EndpointStopping, + /// The connection could not be created because not enough of the CID space is available + /// + /// Try using longer connection IDs + #[error("CIDs exhausted")] + CidsExhausted, + /// The given server name was malformed + #[error("invalid server name: {0}")] + InvalidServerName(String), + /// The remote [`SocketAddr`] supplied was malformed + /// + /// Examples include attempting to connect to port 0, or using an inappropriate address family. + #[error("invalid remote address: {0}")] + InvalidRemoteAddress(SocketAddr), + /// No default client configuration was set up + /// + /// Use `Endpoint::connect_with` to specify a client configuration. + #[error("no default client config")] + NoDefaultClientConfig, + /// The local endpoint does not support the QUIC version specified in the client configuration + #[error("unsupported QUIC version")] + UnsupportedVersion, +} + +/// Error type for attempting to accept an [`Incoming`] +#[derive(Debug)] +pub struct AcceptError { + /// Underlying error describing reason for failure + pub cause: ConnectionError, + /// Optional response to transmit back + pub response: Option, +} + +/// Error for attempting to retry an [`Incoming`] which already bears a token from a previous retry +#[derive(Debug, Error)] +#[error("retry() with validated Incoming")] +pub struct RetryError(Box); + +impl RetryError { + /// Get the [`Incoming`] + pub fn into_incoming(self) -> Incoming { + *self.0 + } +} + +/// Reset Tokens which are associated with peer socket addresses +/// +/// The standard `HashMap` is used since both `SocketAddr` and `ResetToken` are +/// peer generated and might be usable for hash collision attacks. +#[derive(Default, Debug)] +struct ResetTokenTable(HashMap>); + +impl ResetTokenTable { + fn insert(&mut self, remote: SocketAddr, token: ResetToken, ch: ConnectionHandle) -> bool { + self.0 + .entry(remote) + .or_default() + .insert(token, ch) + .is_some() + } + + fn remove(&mut self, remote: SocketAddr, token: ResetToken) { + use std::collections::hash_map::Entry; + match self.0.entry(remote) { + Entry::Vacant(_) => {} + Entry::Occupied(mut e) => { + e.get_mut().remove(&token); + if e.get().is_empty() { + e.remove_entry(); + } + } + } + } + + fn get(&self, remote: SocketAddr, token: &[u8]) -> Option<&ConnectionHandle> { + let token = ResetToken::from(<[u8; RESET_TOKEN_SIZE]>::try_from(token).ok()?); + self.0.get(&remote)?.get(&token) + } +} + +/// Identifies a connection by the combination of remote and local addresses +/// +/// Including the local ensures good behavior when the host has multiple IP addresses on the same +/// subnet and zero-length connection IDs are in use. +#[derive(Hash, Eq, PartialEq, Debug, Copy, Clone)] +struct FourTuple { + remote: SocketAddr, + // A single socket can only listen on a single port, so no need to store it explicitly + local_ip: Option, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/frame.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/frame.rs new file mode 100644 index 0000000..01d9a02 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/frame.rs @@ -0,0 +1,1008 @@ +use std::{ + fmt::{self, Write}, + mem, + ops::{Range, RangeInclusive}, +}; + +use bytes::{Buf, BufMut, Bytes}; +use tinyvec::TinyVec; + +use crate::{ + Dir, MAX_CID_SIZE, RESET_TOKEN_SIZE, ResetToken, StreamId, TransportError, TransportErrorCode, + VarInt, + coding::{self, BufExt, BufMutExt, UnexpectedEnd}, + range_set::ArrayRangeSet, + shared::{ConnectionId, EcnCodepoint}, +}; + +#[cfg(feature = "arbitrary")] +use arbitrary::Arbitrary; + +/// A QUIC frame type +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct FrameType(u64); + +impl FrameType { + fn stream(self) -> Option { + if STREAM_TYS.contains(&self.0) { + Some(StreamInfo(self.0 as u8)) + } else { + None + } + } + fn datagram(self) -> Option { + if DATAGRAM_TYS.contains(&self.0) { + Some(DatagramInfo(self.0 as u8)) + } else { + None + } + } +} + +impl coding::Codec for FrameType { + fn decode(buf: &mut B) -> coding::Result { + Ok(Self(buf.get_var()?)) + } + fn encode(&self, buf: &mut B) { + buf.write_var(self.0); + } +} + +pub(crate) trait FrameStruct { + /// Smallest number of bytes this type of frame is guaranteed to fit within. + const SIZE_BOUND: usize; +} + +macro_rules! frame_types { + {$($name:ident = $val:expr,)*} => { + impl FrameType { + $(pub(crate) const $name: FrameType = FrameType($val);)* + } + + impl fmt::Debug for FrameType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + $($val => f.write_str(stringify!($name)),)* + _ => write!(f, "Type({:02x})", self.0) + } + } + } + + impl fmt::Display for FrameType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + $($val => f.write_str(stringify!($name)),)* + x if STREAM_TYS.contains(&x) => f.write_str("STREAM"), + x if DATAGRAM_TYS.contains(&x) => f.write_str("DATAGRAM"), + _ => write!(f, "", self.0), + } + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct StreamInfo(u8); + +impl StreamInfo { + fn fin(self) -> bool { + self.0 & 0x01 != 0 + } + fn len(self) -> bool { + self.0 & 0x02 != 0 + } + fn off(self) -> bool { + self.0 & 0x04 != 0 + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct DatagramInfo(u8); + +impl DatagramInfo { + fn len(self) -> bool { + self.0 & 0x01 != 0 + } +} + +frame_types! { + PADDING = 0x00, + PING = 0x01, + ACK = 0x02, + ACK_ECN = 0x03, + RESET_STREAM = 0x04, + STOP_SENDING = 0x05, + CRYPTO = 0x06, + NEW_TOKEN = 0x07, + // STREAM + MAX_DATA = 0x10, + MAX_STREAM_DATA = 0x11, + MAX_STREAMS_BIDI = 0x12, + MAX_STREAMS_UNI = 0x13, + DATA_BLOCKED = 0x14, + STREAM_DATA_BLOCKED = 0x15, + STREAMS_BLOCKED_BIDI = 0x16, + STREAMS_BLOCKED_UNI = 0x17, + NEW_CONNECTION_ID = 0x18, + RETIRE_CONNECTION_ID = 0x19, + PATH_CHALLENGE = 0x1a, + PATH_RESPONSE = 0x1b, + CONNECTION_CLOSE = 0x1c, + APPLICATION_CLOSE = 0x1d, + HANDSHAKE_DONE = 0x1e, + // ACK Frequency + ACK_FREQUENCY = 0xaf, + IMMEDIATE_ACK = 0x1f, + // DATAGRAM +} + +const STREAM_TYS: RangeInclusive = RangeInclusive::new(0x08, 0x0f); +const DATAGRAM_TYS: RangeInclusive = RangeInclusive::new(0x30, 0x31); + +#[derive(Debug)] +pub(crate) enum Frame { + Padding, + Ping, + Ack(Ack), + ResetStream(ResetStream), + StopSending(StopSending), + Crypto(Crypto), + NewToken(NewToken), + Stream(Stream), + MaxData(VarInt), + MaxStreamData { id: StreamId, offset: u64 }, + MaxStreams { dir: Dir, count: u64 }, + DataBlocked { offset: u64 }, + StreamDataBlocked { id: StreamId, offset: u64 }, + StreamsBlocked { dir: Dir, limit: u64 }, + NewConnectionId(NewConnectionId), + RetireConnectionId { sequence: u64 }, + PathChallenge(u64), + PathResponse(u64), + Close(Close), + Datagram(Datagram), + AckFrequency(AckFrequency), + ImmediateAck, + HandshakeDone, +} + +impl Frame { + pub(crate) fn ty(&self) -> FrameType { + use Frame::*; + match *self { + Padding => FrameType::PADDING, + ResetStream(_) => FrameType::RESET_STREAM, + Close(self::Close::Connection(_)) => FrameType::CONNECTION_CLOSE, + Close(self::Close::Application(_)) => FrameType::APPLICATION_CLOSE, + MaxData(_) => FrameType::MAX_DATA, + MaxStreamData { .. } => FrameType::MAX_STREAM_DATA, + MaxStreams { dir: Dir::Bi, .. } => FrameType::MAX_STREAMS_BIDI, + MaxStreams { dir: Dir::Uni, .. } => FrameType::MAX_STREAMS_UNI, + Ping => FrameType::PING, + DataBlocked { .. } => FrameType::DATA_BLOCKED, + StreamDataBlocked { .. } => FrameType::STREAM_DATA_BLOCKED, + StreamsBlocked { dir: Dir::Bi, .. } => FrameType::STREAMS_BLOCKED_BIDI, + StreamsBlocked { dir: Dir::Uni, .. } => FrameType::STREAMS_BLOCKED_UNI, + StopSending { .. } => FrameType::STOP_SENDING, + RetireConnectionId { .. } => FrameType::RETIRE_CONNECTION_ID, + Ack(_) => FrameType::ACK, + Stream(ref x) => { + let mut ty = *STREAM_TYS.start(); + if x.fin { + ty |= 0x01; + } + if x.offset != 0 { + ty |= 0x04; + } + FrameType(ty) + } + PathChallenge(_) => FrameType::PATH_CHALLENGE, + PathResponse(_) => FrameType::PATH_RESPONSE, + NewConnectionId { .. } => FrameType::NEW_CONNECTION_ID, + Crypto(_) => FrameType::CRYPTO, + NewToken(_) => FrameType::NEW_TOKEN, + Datagram(_) => FrameType(*DATAGRAM_TYS.start()), + AckFrequency(_) => FrameType::ACK_FREQUENCY, + ImmediateAck => FrameType::IMMEDIATE_ACK, + HandshakeDone => FrameType::HANDSHAKE_DONE, + } + } + + pub(crate) fn is_ack_eliciting(&self) -> bool { + !matches!(*self, Self::Ack(_) | Self::Padding | Self::Close(_)) + } +} + +#[derive(Clone, Debug)] +pub enum Close { + Connection(ConnectionClose), + Application(ApplicationClose), +} + +impl Close { + pub(crate) fn encode(&self, out: &mut W, max_len: usize) { + match *self { + Self::Connection(ref x) => x.encode(out, max_len), + Self::Application(ref x) => x.encode(out, max_len), + } + } + + pub(crate) fn is_transport_layer(&self) -> bool { + matches!(*self, Self::Connection(_)) + } +} + +impl From for Close { + fn from(x: TransportError) -> Self { + Self::Connection(x.into()) + } +} +impl From for Close { + fn from(x: ConnectionClose) -> Self { + Self::Connection(x) + } +} +impl From for Close { + fn from(x: ApplicationClose) -> Self { + Self::Application(x) + } +} + +/// Reason given by the transport for closing the connection +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConnectionClose { + /// Class of error as encoded in the specification + pub error_code: TransportErrorCode, + /// Type of frame that caused the close + pub frame_type: Option, + /// Human-readable reason for the close + pub reason: Bytes, +} + +impl fmt::Display for ConnectionClose { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.error_code.fmt(f)?; + if !self.reason.as_ref().is_empty() { + f.write_str(": ")?; + f.write_str(&String::from_utf8_lossy(&self.reason))?; + } + Ok(()) + } +} + +impl From for ConnectionClose { + fn from(x: TransportError) -> Self { + Self { + error_code: x.code, + frame_type: x.frame, + reason: x.reason.into(), + } + } +} + +impl FrameStruct for ConnectionClose { + const SIZE_BOUND: usize = 1 + 8 + 8 + 8; +} + +impl ConnectionClose { + pub(crate) fn encode(&self, out: &mut W, max_len: usize) { + out.write(FrameType::CONNECTION_CLOSE); // 1 byte + out.write(self.error_code); // <= 8 bytes + let ty = self.frame_type.map_or(0, |x| x.0); + out.write_var(ty); // <= 8 bytes + let max_len = max_len + - 3 + - VarInt::from_u64(ty).unwrap().size() + - VarInt::from_u64(self.reason.len() as u64).unwrap().size(); + let actual_len = self.reason.len().min(max_len); + out.write_var(actual_len as u64); // <= 8 bytes + out.put_slice(&self.reason[0..actual_len]); // whatever's left + } +} + +/// Reason given by an application for closing the connection +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApplicationClose { + /// Application-specific reason code + pub error_code: VarInt, + /// Human-readable reason for the close + pub reason: Bytes, +} + +impl fmt::Display for ApplicationClose { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.reason.as_ref().is_empty() { + f.write_str(&String::from_utf8_lossy(&self.reason))?; + f.write_str(" (code ")?; + self.error_code.fmt(f)?; + f.write_str(")")?; + } else { + self.error_code.fmt(f)?; + } + Ok(()) + } +} + +impl FrameStruct for ApplicationClose { + const SIZE_BOUND: usize = 1 + 8 + 8; +} + +impl ApplicationClose { + pub(crate) fn encode(&self, out: &mut W, max_len: usize) { + out.write(FrameType::APPLICATION_CLOSE); // 1 byte + out.write(self.error_code); // <= 8 bytes + let max_len = max_len - 3 - VarInt::from_u64(self.reason.len() as u64).unwrap().size(); + let actual_len = self.reason.len().min(max_len); + out.write_var(actual_len as u64); // <= 8 bytes + out.put_slice(&self.reason[0..actual_len]); // whatever's left + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct Ack { + pub largest: u64, + pub delay: u64, + pub additional: Bytes, + pub ecn: Option, +} + +impl fmt::Debug for Ack { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ranges = "[".to_string(); + let mut first = true; + for range in self.iter() { + if !first { + ranges.push(','); + } + write!(ranges, "{range:?}").unwrap(); + first = false; + } + ranges.push(']'); + + f.debug_struct("Ack") + .field("largest", &self.largest) + .field("delay", &self.delay) + .field("ecn", &self.ecn) + .field("ranges", &ranges) + .finish() + } +} + +impl<'a> IntoIterator for &'a Ack { + type Item = RangeInclusive; + type IntoIter = AckIter<'a>; + + fn into_iter(self) -> AckIter<'a> { + AckIter::new(self.largest, &self.additional[..]) + } +} + +impl Ack { + pub fn encode( + delay: u64, + ranges: &ArrayRangeSet, + ecn: Option<&EcnCounts>, + buf: &mut W, + ) { + let mut rest = ranges.iter().rev(); + let first = rest.next().unwrap(); + let largest = first.end - 1; + let first_size = first.end - first.start; + buf.write(if ecn.is_some() { + FrameType::ACK_ECN + } else { + FrameType::ACK + }); + buf.write_var(largest); + buf.write_var(delay); + buf.write_var(ranges.len() as u64 - 1); + buf.write_var(first_size - 1); + let mut prev = first.start; + for block in rest { + let size = block.end - block.start; + buf.write_var(prev - block.end - 1); + buf.write_var(size - 1); + prev = block.start; + } + if let Some(x) = ecn { + x.encode(buf) + } + } + + pub fn iter(&self) -> AckIter<'_> { + self.into_iter() + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct EcnCounts { + pub ect0: u64, + pub ect1: u64, + pub ce: u64, +} + +impl std::ops::AddAssign for EcnCounts { + fn add_assign(&mut self, rhs: EcnCodepoint) { + match rhs { + EcnCodepoint::Ect0 => { + self.ect0 += 1; + } + EcnCodepoint::Ect1 => { + self.ect1 += 1; + } + EcnCodepoint::Ce => { + self.ce += 1; + } + } + } +} + +impl EcnCounts { + pub const ZERO: Self = Self { + ect0: 0, + ect1: 0, + ce: 0, + }; + + pub fn encode(&self, out: &mut W) { + out.write_var(self.ect0); + out.write_var(self.ect1); + out.write_var(self.ce); + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Stream { + pub(crate) id: StreamId, + pub(crate) offset: u64, + pub(crate) fin: bool, + pub(crate) data: Bytes, +} + +impl FrameStruct for Stream { + const SIZE_BOUND: usize = 1 + 8 + 8 + 8; +} + +/// Metadata from a stream frame +#[derive(Debug, Clone)] +pub(crate) struct StreamMeta { + pub(crate) id: StreamId, + pub(crate) offsets: Range, + pub(crate) fin: bool, +} + +// This manual implementation exists because `Default` is not implemented for `StreamId` +impl Default for StreamMeta { + fn default() -> Self { + Self { + id: StreamId(0), + offsets: 0..0, + fin: false, + } + } +} + +impl StreamMeta { + pub(crate) fn encode(&self, length: bool, out: &mut W) { + let mut ty = *STREAM_TYS.start(); + if self.offsets.start != 0 { + ty |= 0x04; + } + if length { + ty |= 0x02; + } + if self.fin { + ty |= 0x01; + } + out.write_var(ty); // 1 byte + out.write(self.id); // <=8 bytes + if self.offsets.start != 0 { + out.write_var(self.offsets.start); // <=8 bytes + } + if length { + out.write_var(self.offsets.end - self.offsets.start); // <=8 bytes + } + } +} + +/// A vector of [`StreamMeta`] with optimization for the single element case +pub(crate) type StreamMetaVec = TinyVec<[StreamMeta; 1]>; + +#[derive(Debug, Clone)] +pub(crate) struct Crypto { + pub(crate) offset: u64, + pub(crate) data: Bytes, +} + +impl Crypto { + pub(crate) const SIZE_BOUND: usize = 17; + + pub(crate) fn encode(&self, out: &mut W) { + out.write(FrameType::CRYPTO); + out.write_var(self.offset); + out.write_var(self.data.len() as u64); + out.put_slice(&self.data); + } +} + +#[derive(Debug, Clone)] +pub(crate) struct NewToken { + pub(crate) token: Bytes, +} + +impl NewToken { + pub(crate) fn encode(&self, out: &mut W) { + out.write(FrameType::NEW_TOKEN); + out.write_var(self.token.len() as u64); + out.put_slice(&self.token); + } + + pub(crate) fn size(&self) -> usize { + 1 + VarInt::from_u64(self.token.len() as u64).unwrap().size() + self.token.len() + } +} + +pub(crate) struct Iter { + bytes: Bytes, + last_ty: Option, +} + +impl Iter { + pub(crate) fn new(payload: Bytes) -> Result { + if payload.is_empty() { + // "An endpoint MUST treat receipt of a packet containing no frames as a + // connection error of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000.html#name-frames-and-frame-types + return Err(TransportError::PROTOCOL_VIOLATION( + "packet payload is empty", + )); + } + + Ok(Self { + bytes: payload, + last_ty: None, + }) + } + + fn take_len(&mut self) -> Result { + let len = self.bytes.get_var()?; + if len > self.bytes.remaining() as u64 { + return Err(UnexpectedEnd); + } + Ok(self.bytes.split_to(len as usize)) + } + + fn try_next(&mut self) -> Result { + let ty = self.bytes.get::()?; + self.last_ty = Some(ty); + Ok(match ty { + FrameType::PADDING => Frame::Padding, + FrameType::RESET_STREAM => Frame::ResetStream(ResetStream { + id: self.bytes.get()?, + error_code: self.bytes.get()?, + final_offset: self.bytes.get()?, + }), + FrameType::CONNECTION_CLOSE => Frame::Close(Close::Connection(ConnectionClose { + error_code: self.bytes.get()?, + frame_type: { + let x = self.bytes.get_var()?; + if x == 0 { None } else { Some(FrameType(x)) } + }, + reason: self.take_len()?, + })), + FrameType::APPLICATION_CLOSE => Frame::Close(Close::Application(ApplicationClose { + error_code: self.bytes.get()?, + reason: self.take_len()?, + })), + FrameType::MAX_DATA => Frame::MaxData(self.bytes.get()?), + FrameType::MAX_STREAM_DATA => Frame::MaxStreamData { + id: self.bytes.get()?, + offset: self.bytes.get_var()?, + }, + FrameType::MAX_STREAMS_BIDI => Frame::MaxStreams { + dir: Dir::Bi, + count: self.bytes.get_var()?, + }, + FrameType::MAX_STREAMS_UNI => Frame::MaxStreams { + dir: Dir::Uni, + count: self.bytes.get_var()?, + }, + FrameType::PING => Frame::Ping, + FrameType::DATA_BLOCKED => Frame::DataBlocked { + offset: self.bytes.get_var()?, + }, + FrameType::STREAM_DATA_BLOCKED => Frame::StreamDataBlocked { + id: self.bytes.get()?, + offset: self.bytes.get_var()?, + }, + FrameType::STREAMS_BLOCKED_BIDI => Frame::StreamsBlocked { + dir: Dir::Bi, + limit: self.bytes.get_var()?, + }, + FrameType::STREAMS_BLOCKED_UNI => Frame::StreamsBlocked { + dir: Dir::Uni, + limit: self.bytes.get_var()?, + }, + FrameType::STOP_SENDING => Frame::StopSending(StopSending { + id: self.bytes.get()?, + error_code: self.bytes.get()?, + }), + FrameType::RETIRE_CONNECTION_ID => Frame::RetireConnectionId { + sequence: self.bytes.get_var()?, + }, + FrameType::ACK | FrameType::ACK_ECN => { + let largest = self.bytes.get_var()?; + let delay = self.bytes.get_var()?; + let extra_blocks = self.bytes.get_var()? as usize; + let n = scan_ack_blocks(&self.bytes, largest, extra_blocks)?; + Frame::Ack(Ack { + delay, + largest, + additional: self.bytes.split_to(n), + ecn: if ty != FrameType::ACK_ECN { + None + } else { + Some(EcnCounts { + ect0: self.bytes.get_var()?, + ect1: self.bytes.get_var()?, + ce: self.bytes.get_var()?, + }) + }, + }) + } + FrameType::PATH_CHALLENGE => Frame::PathChallenge(self.bytes.get()?), + FrameType::PATH_RESPONSE => Frame::PathResponse(self.bytes.get()?), + FrameType::NEW_CONNECTION_ID => { + let sequence = self.bytes.get_var()?; + let retire_prior_to = self.bytes.get_var()?; + if retire_prior_to > sequence { + return Err(IterErr::Malformed); + } + let length = self.bytes.get::()? as usize; + if length > MAX_CID_SIZE || length == 0 { + return Err(IterErr::Malformed); + } + if length > self.bytes.remaining() { + return Err(IterErr::UnexpectedEnd); + } + let mut stage = [0; MAX_CID_SIZE]; + self.bytes.copy_to_slice(&mut stage[0..length]); + let id = ConnectionId::new(&stage[..length]); + if self.bytes.remaining() < 16 { + return Err(IterErr::UnexpectedEnd); + } + let mut reset_token = [0; RESET_TOKEN_SIZE]; + self.bytes.copy_to_slice(&mut reset_token); + Frame::NewConnectionId(NewConnectionId { + sequence, + retire_prior_to, + id, + reset_token: reset_token.into(), + }) + } + FrameType::CRYPTO => Frame::Crypto(Crypto { + offset: self.bytes.get_var()?, + data: self.take_len()?, + }), + FrameType::NEW_TOKEN => Frame::NewToken(NewToken { + token: self.take_len()?, + }), + FrameType::HANDSHAKE_DONE => Frame::HandshakeDone, + FrameType::ACK_FREQUENCY => Frame::AckFrequency(AckFrequency { + sequence: self.bytes.get()?, + ack_eliciting_threshold: self.bytes.get()?, + request_max_ack_delay: self.bytes.get()?, + reordering_threshold: self.bytes.get()?, + }), + FrameType::IMMEDIATE_ACK => Frame::ImmediateAck, + _ => { + if let Some(s) = ty.stream() { + Frame::Stream(Stream { + id: self.bytes.get()?, + offset: if s.off() { self.bytes.get_var()? } else { 0 }, + fin: s.fin(), + data: if s.len() { + self.take_len()? + } else { + self.take_remaining() + }, + }) + } else if let Some(d) = ty.datagram() { + Frame::Datagram(Datagram { + data: if d.len() { + self.take_len()? + } else { + self.take_remaining() + }, + }) + } else { + return Err(IterErr::InvalidFrameId); + } + } + }) + } + + fn take_remaining(&mut self) -> Bytes { + mem::take(&mut self.bytes) + } +} + +impl Iterator for Iter { + type Item = Result; + fn next(&mut self) -> Option { + if !self.bytes.has_remaining() { + return None; + } + match self.try_next() { + Ok(x) => Some(Ok(x)), + Err(e) => { + // Corrupt frame, skip it and everything that follows + self.bytes.clear(); + Some(Err(InvalidFrame { + ty: self.last_ty, + reason: e.reason(), + })) + } + } + } +} + +#[derive(Debug)] +pub(crate) struct InvalidFrame { + pub(crate) ty: Option, + pub(crate) reason: &'static str, +} + +impl From for TransportError { + fn from(err: InvalidFrame) -> Self { + let mut te = Self::FRAME_ENCODING_ERROR(err.reason); + te.frame = err.ty; + te + } +} + +/// Validate exactly `n` ACK ranges in `buf` and return the number of bytes they cover +fn scan_ack_blocks(mut buf: &[u8], largest: u64, n: usize) -> Result { + let total_len = buf.remaining(); + let first_block = buf.get_var()?; + let mut smallest = largest.checked_sub(first_block).ok_or(IterErr::Malformed)?; + for _ in 0..n { + let gap = buf.get_var()?; + smallest = smallest.checked_sub(gap + 2).ok_or(IterErr::Malformed)?; + let block = buf.get_var()?; + smallest = smallest.checked_sub(block).ok_or(IterErr::Malformed)?; + } + Ok(total_len - buf.remaining()) +} + +enum IterErr { + UnexpectedEnd, + InvalidFrameId, + Malformed, +} + +impl IterErr { + fn reason(&self) -> &'static str { + use IterErr::*; + match *self { + UnexpectedEnd => "unexpected end", + InvalidFrameId => "invalid frame ID", + Malformed => "malformed", + } + } +} + +impl From for IterErr { + fn from(_: UnexpectedEnd) -> Self { + Self::UnexpectedEnd + } +} + +#[derive(Debug, Clone)] +pub struct AckIter<'a> { + largest: u64, + data: &'a [u8], +} + +impl<'a> AckIter<'a> { + fn new(largest: u64, data: &'a [u8]) -> Self { + Self { largest, data } + } +} + +impl Iterator for AckIter<'_> { + type Item = RangeInclusive; + fn next(&mut self) -> Option> { + if !self.data.has_remaining() { + return None; + } + let block = self.data.get_var().unwrap(); + let largest = self.largest; + if let Ok(gap) = self.data.get_var() { + self.largest -= block + gap + 2; + } + Some(largest - block..=largest) + } +} + +#[allow(unreachable_pub)] // fuzzing only +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Debug, Copy, Clone)] +pub struct ResetStream { + pub(crate) id: StreamId, + pub(crate) error_code: VarInt, + pub(crate) final_offset: VarInt, +} + +impl FrameStruct for ResetStream { + const SIZE_BOUND: usize = 1 + 8 + 8 + 8; +} + +impl ResetStream { + pub(crate) fn encode(&self, out: &mut W) { + out.write(FrameType::RESET_STREAM); // 1 byte + out.write(self.id); // <= 8 bytes + out.write(self.error_code); // <= 8 bytes + out.write(self.final_offset); // <= 8 bytes + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct StopSending { + pub(crate) id: StreamId, + pub(crate) error_code: VarInt, +} + +impl FrameStruct for StopSending { + const SIZE_BOUND: usize = 1 + 8 + 8; +} + +impl StopSending { + pub(crate) fn encode(&self, out: &mut W) { + out.write(FrameType::STOP_SENDING); // 1 byte + out.write(self.id); // <= 8 bytes + out.write(self.error_code) // <= 8 bytes + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct NewConnectionId { + pub(crate) sequence: u64, + pub(crate) retire_prior_to: u64, + pub(crate) id: ConnectionId, + pub(crate) reset_token: ResetToken, +} + +impl NewConnectionId { + pub(crate) fn encode(&self, out: &mut W) { + out.write(FrameType::NEW_CONNECTION_ID); + out.write_var(self.sequence); + out.write_var(self.retire_prior_to); + out.write(self.id.len() as u8); + out.put_slice(&self.id); + out.put_slice(&self.reset_token); + } +} + +impl FrameStruct for NewConnectionId { + const SIZE_BOUND: usize = 1 + 8 + 8 + 1 + MAX_CID_SIZE + RESET_TOKEN_SIZE; +} + +/// Smallest number of bytes this type of frame is guaranteed to fit within. +pub(crate) const RETIRE_CONNECTION_ID_SIZE_BOUND: usize = 9; + +/// An unreliable datagram +#[derive(Debug, Clone)] +pub struct Datagram { + /// Payload + pub data: Bytes, +} + +impl FrameStruct for Datagram { + const SIZE_BOUND: usize = 1 + 8; +} + +impl Datagram { + pub(crate) fn encode(&self, length: bool, out: &mut Vec) { + out.write(FrameType(*DATAGRAM_TYS.start() | u64::from(length))); // 1 byte + if length { + // Safe to unwrap because we check length sanity before queueing datagrams + out.write(VarInt::from_u64(self.data.len() as u64).unwrap()); // <= 8 bytes + } + out.extend_from_slice(&self.data); + } + + pub(crate) fn size(&self, length: bool) -> usize { + 1 + if length { + VarInt::from_u64(self.data.len() as u64).unwrap().size() + } else { + 0 + } + self.data.len() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) struct AckFrequency { + pub(crate) sequence: VarInt, + pub(crate) ack_eliciting_threshold: VarInt, + pub(crate) request_max_ack_delay: VarInt, + pub(crate) reordering_threshold: VarInt, +} + +impl AckFrequency { + pub(crate) fn encode(&self, buf: &mut W) { + buf.write(FrameType::ACK_FREQUENCY); + buf.write(self.sequence); + buf.write(self.ack_eliciting_threshold); + buf.write(self.request_max_ack_delay); + buf.write(self.reordering_threshold); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::coding::Codec; + use assert_matches::assert_matches; + + fn frames(buf: Vec) -> Vec { + Iter::new(Bytes::from(buf)) + .unwrap() + .collect::, _>>() + .unwrap() + } + + #[test] + fn ack_coding() { + const PACKETS: &[u64] = &[1, 2, 3, 5, 10, 11, 14]; + let mut ranges = ArrayRangeSet::new(); + for &packet in PACKETS { + ranges.insert(packet..packet + 1); + } + let mut buf = Vec::new(); + const ECN: EcnCounts = EcnCounts { + ect0: 42, + ect1: 24, + ce: 12, + }; + Ack::encode(42, &ranges, Some(&ECN), &mut buf); + let frames = frames(buf); + assert_eq!(frames.len(), 1); + match frames[0] { + Frame::Ack(ref ack) => { + let mut packets = ack.iter().flatten().collect::>(); + packets.sort_unstable(); + assert_eq!(&packets[..], PACKETS); + assert_eq!(ack.ecn, Some(ECN)); + } + ref x => panic!("incorrect frame {x:?}"), + } + } + + #[test] + fn ack_frequency_coding() { + let mut buf = Vec::new(); + let original = AckFrequency { + sequence: VarInt(42), + ack_eliciting_threshold: VarInt(20), + request_max_ack_delay: VarInt(50_000), + reordering_threshold: VarInt(1), + }; + original.encode(&mut buf); + let frames = frames(buf); + assert_eq!(frames.len(), 1); + match &frames[0] { + Frame::AckFrequency(decoded) => assert_eq!(decoded, &original), + x => panic!("incorrect frame {x:?}"), + } + } + + #[test] + fn immediate_ack_coding() { + let mut buf = Vec::new(); + FrameType::IMMEDIATE_ACK.encode(&mut buf); + let frames = frames(buf); + assert_eq!(frames.len(), 1); + assert_matches!(&frames[0], Frame::ImmediateAck); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/lib.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/lib.rs new file mode 100644 index 0000000..5982b69 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/lib.rs @@ -0,0 +1,336 @@ +//! Low-level protocol logic for the QUIC protoocol +//! +//! quinn-proto contains a fully deterministic implementation of QUIC protocol logic. It contains +//! no networking code and does not get any relevant timestamps from the operating system. Most +//! users may want to use the futures-based quinn API instead. +//! +//! The quinn-proto API might be of interest if you want to use it from a C or C++ project +//! through C bindings or if you want to use a different event loop than the one tokio provides. +//! +//! The most important types are `Endpoint`, which conceptually represents the protocol state for +//! a single socket and mostly manages configuration and dispatches incoming datagrams to the +//! related `Connection`. `Connection` types contain the bulk of the protocol logic related to +//! managing a single connection and all the related state (such as streams). + +#![cfg_attr(not(fuzzing), warn(missing_docs))] +#![cfg_attr(test, allow(dead_code))] +// Fixes welcome: +#![warn(unreachable_pub)] +#![allow(clippy::cognitive_complexity)] +#![allow(clippy::too_many_arguments)] +#![warn(clippy::use_self)] + +use std::{ + fmt, + net::{IpAddr, SocketAddr}, + ops, +}; + +mod cid_queue; +pub mod coding; +mod constant_time; +mod range_set; +#[cfg(all(test, any(feature = "rustls-aws-lc-rs", feature = "rustls-ring")))] +mod tests; +pub mod transport_parameters; +mod varint; + +pub use varint::{VarInt, VarIntBoundsExceeded}; + +#[cfg(feature = "bloom")] +mod bloom_token_log; +#[cfg(feature = "bloom")] +pub use bloom_token_log::BloomTokenLog; + +mod connection; +pub use crate::connection::{ + Chunk, Chunks, ClosedStream, Connection, ConnectionError, ConnectionStats, Datagrams, Event, + FinishError, FrameStats, PathStats, ReadError, ReadableError, RecvStream, RttEstimator, + SendDatagramError, SendStream, ShouldTransmit, StreamEvent, Streams, UdpStats, WriteError, + Written, +}; +#[cfg(feature = "qlog")] +pub use connection::qlog::QlogStream; + +#[cfg(feature = "rustls")] +pub use rustls; + +mod config; +#[cfg(feature = "qlog")] +pub use config::QlogConfig; +pub use config::{ + AckFrequencyConfig, ClientConfig, ConfigError, EndpointConfig, IdleTimeout, MtuDiscoveryConfig, + ServerConfig, StdSystemTime, TimeSource, TransportConfig, ValidationTokenConfig, +}; + +pub mod crypto; + +mod frame; +use crate::frame::Frame; +pub use crate::frame::{ApplicationClose, ConnectionClose, Datagram, FrameType}; + +mod endpoint; +pub use crate::endpoint::{ + AcceptError, ConnectError, ConnectionHandle, DatagramEvent, Endpoint, Incoming, RetryError, +}; + +mod packet; +pub use packet::{ + ConnectionIdParser, FixedLengthConnectionIdParser, LongType, PacketDecodeError, PartialDecode, + ProtectedHeader, ProtectedInitialHeader, +}; + +mod shared; +pub use crate::shared::{ConnectionEvent, ConnectionId, EcnCodepoint, EndpointEvent}; + +mod transport_error; +pub use crate::transport_error::{Code as TransportErrorCode, Error as TransportError}; + +pub mod congestion; + +mod cid_generator; +pub use crate::cid_generator::{ + ConnectionIdGenerator, HashedConnectionIdGenerator, InvalidCid, RandomConnectionIdGenerator, +}; + +mod token; +use token::ResetToken; +pub use token::{NoneTokenLog, NoneTokenStore, TokenLog, TokenReuseError, TokenStore}; + +mod token_memory_cache; +pub use token_memory_cache::TokenMemoryCache; + +#[cfg(feature = "arbitrary")] +use arbitrary::Arbitrary; + +// Deal with time +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] +pub(crate) use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +pub(crate) use web_time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +#[cfg(fuzzing)] +pub mod fuzzing { + pub use crate::connection::{Retransmits, State as ConnectionState, StreamsState}; + pub use crate::frame::ResetStream; + pub use crate::packet::PartialDecode; + pub use crate::transport_parameters::TransportParameters; + pub use bytes::{BufMut, BytesMut}; + + #[cfg(feature = "arbitrary")] + use arbitrary::{Arbitrary, Result, Unstructured}; + + #[cfg(feature = "arbitrary")] + impl<'arbitrary> Arbitrary<'arbitrary> for TransportParameters { + fn arbitrary(u: &mut Unstructured<'arbitrary>) -> Result { + Ok(Self { + initial_max_streams_bidi: u.arbitrary()?, + initial_max_streams_uni: u.arbitrary()?, + ack_delay_exponent: u.arbitrary()?, + max_udp_payload_size: u.arbitrary()?, + ..Self::default() + }) + } + } + + #[derive(Debug)] + pub struct PacketParams { + pub local_cid_len: usize, + pub buf: BytesMut, + pub grease_quic_bit: bool, + } + + #[cfg(feature = "arbitrary")] + impl<'arbitrary> Arbitrary<'arbitrary> for PacketParams { + fn arbitrary(u: &mut Unstructured<'arbitrary>) -> Result { + let local_cid_len: usize = u.int_in_range(0..=crate::MAX_CID_SIZE)?; + let bytes: Vec = Vec::arbitrary(u)?; + let mut buf = BytesMut::new(); + buf.put_slice(&bytes[..]); + Ok(Self { + local_cid_len, + buf, + grease_quic_bit: bool::arbitrary(u)?, + }) + } + } +} + +/// The QUIC protocol version implemented. +pub const DEFAULT_SUPPORTED_VERSIONS: &[u32] = &[ + 0x00000001, + 0xff00_001d, + 0xff00_001e, + 0xff00_001f, + 0xff00_0020, + 0xff00_0021, + 0xff00_0022, +]; + +/// Whether an endpoint was the initiator of a connection +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Side { + /// The initiator of a connection + Client = 0, + /// The acceptor of a connection + Server = 1, +} + +impl Side { + #[inline] + /// Shorthand for `self == Side::Client` + pub fn is_client(self) -> bool { + self == Self::Client + } + + #[inline] + /// Shorthand for `self == Side::Server` + pub fn is_server(self) -> bool { + self == Self::Server + } +} + +impl ops::Not for Side { + type Output = Self; + fn not(self) -> Self { + match self { + Self::Client => Self::Server, + Self::Server => Self::Client, + } + } +} + +/// Whether a stream communicates data in both directions or only from the initiator +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Dir { + /// Data flows in both directions + Bi = 0, + /// Data flows only from the stream's initiator + Uni = 1, +} + +impl Dir { + fn iter() -> impl Iterator { + [Self::Bi, Self::Uni].iter().cloned() + } +} + +impl fmt::Display for Dir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Dir::*; + f.pad(match *self { + Bi => "bidirectional", + Uni => "unidirectional", + }) + } +} + +/// Identifier for a stream within a particular connection +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct StreamId(u64); + +impl fmt::Display for StreamId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let initiator = match self.initiator() { + Side::Client => "client", + Side::Server => "server", + }; + let dir = match self.dir() { + Dir::Uni => "uni", + Dir::Bi => "bi", + }; + write!( + f, + "{} {}directional stream {}", + initiator, + dir, + self.index() + ) + } +} + +impl StreamId { + /// Create a new StreamId + pub fn new(initiator: Side, dir: Dir, index: u64) -> Self { + Self((index << 2) | ((dir as u64) << 1) | initiator as u64) + } + /// Which side of a connection initiated the stream + pub fn initiator(self) -> Side { + if self.0 & 0x1 == 0 { + Side::Client + } else { + Side::Server + } + } + /// Which directions data flows in + pub fn dir(self) -> Dir { + if self.0 & 0x2 == 0 { Dir::Bi } else { Dir::Uni } + } + /// Distinguishes streams of the same initiator and directionality + pub fn index(self) -> u64 { + self.0 >> 2 + } +} + +impl From for VarInt { + fn from(x: StreamId) -> Self { + unsafe { Self::from_u64_unchecked(x.0) } + } +} + +impl From for StreamId { + fn from(v: VarInt) -> Self { + Self(v.0) + } +} + +impl From for u64 { + fn from(x: StreamId) -> Self { + x.0 + } +} + +impl coding::Codec for StreamId { + fn decode(buf: &mut B) -> coding::Result { + VarInt::decode(buf).map(|x| Self(x.into_inner())) + } + fn encode(&self, buf: &mut B) { + VarInt::from_u64(self.0).unwrap().encode(buf); + } +} + +/// An outgoing packet +#[derive(Debug)] +#[must_use] +pub struct Transmit { + /// The socket this datagram should be sent to + pub destination: SocketAddr, + /// Explicit congestion notification bits to set on the packet + pub ecn: Option, + /// Amount of data written to the caller-supplied buffer + pub size: usize, + /// The segment size if this transmission contains multiple datagrams. + /// This is `None` if the transmit only contains a single datagram + pub segment_size: Option, + /// Optional source IP address for the datagram + pub src_ip: Option, +} + +// +// Useful internal constants +// + +/// The maximum number of CIDs we bother to issue per connection +const LOC_CID_COUNT: u64 = 8; +const RESET_TOKEN_SIZE: usize = 16; +const MAX_CID_SIZE: usize = 20; +const MIN_INITIAL_SIZE: u16 = 1200; +/// +const INITIAL_MTU: u16 = 1200; +const MAX_UDP_PAYLOAD: u16 = 65527; +const TIMER_GRANULARITY: Duration = Duration::from_millis(1); +/// Maximum number of streams that can be uniquely identified by a stream ID +const MAX_STREAM_COUNT: u64 = 1 << 60; diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/packet.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/packet.rs new file mode 100644 index 0000000..b5ef0c4 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/packet.rs @@ -0,0 +1,1014 @@ +use std::{cmp::Ordering, io, ops::Range, str}; + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use thiserror::Error; + +use crate::{ + ConnectionId, + coding::{self, BufExt, BufMutExt}, + crypto, +}; + +/// Decodes a QUIC packet's invariant header +/// +/// Due to packet number encryption, it is impossible to fully decode a header +/// (which includes a variable-length packet number) without crypto context. +/// The crypto context (represented by the `Crypto` type in Quinn) is usually +/// part of the `Connection`, or can be derived from the destination CID for +/// Initial packets. +/// +/// To cope with this, we decode the invariant header (which should be stable +/// across QUIC versions), which gives us the destination CID and allows us +/// to inspect the version and packet type (which depends on the version). +/// This information allows us to fully decode and decrypt the packet. +#[cfg_attr(test, derive(Clone))] +#[derive(Debug)] +pub struct PartialDecode { + plain_header: ProtectedHeader, + buf: io::Cursor, +} + +#[allow(clippy::len_without_is_empty)] +impl PartialDecode { + /// Begin decoding a QUIC packet from `bytes`, returning any trailing data not part of that packet + pub fn new( + bytes: BytesMut, + cid_parser: &(impl ConnectionIdParser + ?Sized), + supported_versions: &[u32], + grease_quic_bit: bool, + ) -> Result<(Self, Option), PacketDecodeError> { + let mut buf = io::Cursor::new(bytes); + let plain_header = + ProtectedHeader::decode(&mut buf, cid_parser, supported_versions, grease_quic_bit)?; + let dgram_len = buf.get_ref().len(); + let packet_len = plain_header + .payload_len() + .map(|len| (buf.position() + len) as usize) + .unwrap_or(dgram_len); + match dgram_len.cmp(&packet_len) { + Ordering::Equal => Ok((Self { plain_header, buf }, None)), + Ordering::Less => Err(PacketDecodeError::InvalidHeader( + "packet too short to contain payload length", + )), + Ordering::Greater => { + let rest = Some(buf.get_mut().split_off(packet_len)); + Ok((Self { plain_header, buf }, rest)) + } + } + } + + /// The underlying partially-decoded packet data + pub(crate) fn data(&self) -> &[u8] { + self.buf.get_ref() + } + + pub(crate) fn initial_header(&self) -> Option<&ProtectedInitialHeader> { + self.plain_header.as_initial() + } + + pub(crate) fn has_long_header(&self) -> bool { + !matches!(self.plain_header, ProtectedHeader::Short { .. }) + } + + pub(crate) fn is_initial(&self) -> bool { + self.space() == Some(SpaceId::Initial) + } + + pub(crate) fn space(&self) -> Option { + use ProtectedHeader::*; + match self.plain_header { + Initial { .. } => Some(SpaceId::Initial), + Long { + ty: LongType::Handshake, + .. + } => Some(SpaceId::Handshake), + Long { + ty: LongType::ZeroRtt, + .. + } => Some(SpaceId::Data), + Short { .. } => Some(SpaceId::Data), + _ => None, + } + } + + pub(crate) fn is_0rtt(&self) -> bool { + match self.plain_header { + ProtectedHeader::Long { ty, .. } => ty == LongType::ZeroRtt, + _ => false, + } + } + + /// The destination connection ID of the packet + pub fn dst_cid(&self) -> &ConnectionId { + self.plain_header.dst_cid() + } + + /// Length of QUIC packet being decoded + #[allow(unreachable_pub)] // fuzzing only + pub fn len(&self) -> usize { + self.buf.get_ref().len() + } + + pub(crate) fn finish( + self, + header_crypto: Option<&dyn crypto::HeaderKey>, + ) -> Result { + use ProtectedHeader::*; + let Self { + plain_header, + mut buf, + } = self; + + if let Initial(ProtectedInitialHeader { + dst_cid, + src_cid, + token_pos, + version, + .. + }) = plain_header + { + let number = Self::decrypt_header(&mut buf, header_crypto.unwrap())?; + let header_len = buf.position() as usize; + let mut bytes = buf.into_inner(); + + let header_data = bytes.split_to(header_len).freeze(); + let token = header_data.slice(token_pos.start..token_pos.end); + return Ok(Packet { + header: Header::Initial(InitialHeader { + dst_cid, + src_cid, + token, + number, + version, + }), + header_data, + payload: bytes, + }); + } + + let header = match plain_header { + Long { + ty, + dst_cid, + src_cid, + version, + .. + } => Header::Long { + ty, + dst_cid, + src_cid, + number: Self::decrypt_header(&mut buf, header_crypto.unwrap())?, + version, + }, + Retry { + dst_cid, + src_cid, + version, + } => Header::Retry { + dst_cid, + src_cid, + version, + }, + Short { spin, dst_cid, .. } => { + let number = Self::decrypt_header(&mut buf, header_crypto.unwrap())?; + let key_phase = buf.get_ref()[0] & KEY_PHASE_BIT != 0; + Header::Short { + spin, + key_phase, + dst_cid, + number, + } + } + VersionNegotiate { + random, + dst_cid, + src_cid, + } => Header::VersionNegotiate { + random, + dst_cid, + src_cid, + }, + Initial { .. } => unreachable!(), + }; + + let header_len = buf.position() as usize; + let mut bytes = buf.into_inner(); + Ok(Packet { + header, + header_data: bytes.split_to(header_len).freeze(), + payload: bytes, + }) + } + + fn decrypt_header( + buf: &mut io::Cursor, + header_crypto: &dyn crypto::HeaderKey, + ) -> Result { + let packet_length = buf.get_ref().len(); + let pn_offset = buf.position() as usize; + if packet_length < pn_offset + 4 + header_crypto.sample_size() { + return Err(PacketDecodeError::InvalidHeader( + "packet too short to extract header protection sample", + )); + } + + header_crypto.decrypt(pn_offset, buf.get_mut()); + + let len = PacketNumber::decode_len(buf.get_ref()[0]); + PacketNumber::decode(len, buf) + } +} + +pub(crate) struct Packet { + pub(crate) header: Header, + pub(crate) header_data: Bytes, + pub(crate) payload: BytesMut, +} + +impl Packet { + pub(crate) fn reserved_bits_valid(&self) -> bool { + let mask = match self.header { + Header::Short { .. } => SHORT_RESERVED_BITS, + _ => LONG_RESERVED_BITS, + }; + self.header_data[0] & mask == 0 + } +} + +pub(crate) struct InitialPacket { + pub(crate) header: InitialHeader, + pub(crate) header_data: Bytes, + pub(crate) payload: BytesMut, +} + +impl From for Packet { + fn from(x: InitialPacket) -> Self { + Self { + header: Header::Initial(x.header), + header_data: x.header_data, + payload: x.payload, + } + } +} + +#[cfg_attr(test, derive(Clone))] +#[derive(Debug)] +pub(crate) enum Header { + Initial(InitialHeader), + Long { + ty: LongType, + dst_cid: ConnectionId, + src_cid: ConnectionId, + number: PacketNumber, + version: u32, + }, + Retry { + dst_cid: ConnectionId, + src_cid: ConnectionId, + version: u32, + }, + Short { + spin: bool, + key_phase: bool, + dst_cid: ConnectionId, + number: PacketNumber, + }, + VersionNegotiate { + random: u8, + src_cid: ConnectionId, + dst_cid: ConnectionId, + }, +} + +impl Header { + pub(crate) fn encode(&self, w: &mut Vec) -> PartialEncode { + use Header::*; + let start = w.len(); + match *self { + Initial(InitialHeader { + ref dst_cid, + ref src_cid, + ref token, + number, + version, + }) => { + w.write(u8::from(LongHeaderType::Initial) | number.tag()); + w.write(version); + dst_cid.encode_long(w); + src_cid.encode_long(w); + w.write_var(token.len() as u64); + w.put_slice(token); + w.write::(0); // Placeholder for payload length; see `set_payload_length` + number.encode(w); + PartialEncode { + start, + header_len: w.len() - start, + pn: Some((number.len(), true)), + } + } + Long { + ty, + ref dst_cid, + ref src_cid, + number, + version, + } => { + w.write(u8::from(LongHeaderType::Standard(ty)) | number.tag()); + w.write(version); + dst_cid.encode_long(w); + src_cid.encode_long(w); + w.write::(0); // Placeholder for payload length; see `set_payload_length` + number.encode(w); + PartialEncode { + start, + header_len: w.len() - start, + pn: Some((number.len(), true)), + } + } + Retry { + ref dst_cid, + ref src_cid, + version, + } => { + w.write(u8::from(LongHeaderType::Retry)); + w.write(version); + dst_cid.encode_long(w); + src_cid.encode_long(w); + PartialEncode { + start, + header_len: w.len() - start, + pn: None, + } + } + Short { + spin, + key_phase, + ref dst_cid, + number, + } => { + w.write( + FIXED_BIT + | if key_phase { KEY_PHASE_BIT } else { 0 } + | if spin { SPIN_BIT } else { 0 } + | number.tag(), + ); + w.put_slice(dst_cid); + number.encode(w); + PartialEncode { + start, + header_len: w.len() - start, + pn: Some((number.len(), false)), + } + } + VersionNegotiate { + ref random, + ref dst_cid, + ref src_cid, + } => { + w.write(0x80u8 | random); + w.write::(0); + dst_cid.encode_long(w); + src_cid.encode_long(w); + PartialEncode { + start, + header_len: w.len() - start, + pn: None, + } + } + } + } + + /// Whether the packet is encrypted on the wire + pub(crate) fn is_protected(&self) -> bool { + !matches!(*self, Self::Retry { .. } | Self::VersionNegotiate { .. }) + } + + pub(crate) fn number(&self) -> Option { + use Header::*; + Some(match *self { + Initial(InitialHeader { number, .. }) => number, + Long { number, .. } => number, + Short { number, .. } => number, + _ => { + return None; + } + }) + } + + pub(crate) fn space(&self) -> SpaceId { + use Header::*; + match *self { + Short { .. } => SpaceId::Data, + Long { + ty: LongType::ZeroRtt, + .. + } => SpaceId::Data, + Long { + ty: LongType::Handshake, + .. + } => SpaceId::Handshake, + _ => SpaceId::Initial, + } + } + + pub(crate) fn key_phase(&self) -> bool { + match *self { + Self::Short { key_phase, .. } => key_phase, + _ => false, + } + } + + pub(crate) fn is_short(&self) -> bool { + matches!(*self, Self::Short { .. }) + } + + pub(crate) fn is_1rtt(&self) -> bool { + self.is_short() + } + + pub(crate) fn is_0rtt(&self) -> bool { + matches!( + *self, + Self::Long { + ty: LongType::ZeroRtt, + .. + } + ) + } + + pub(crate) fn dst_cid(&self) -> ConnectionId { + use Header::*; + match *self { + Initial(InitialHeader { dst_cid, .. }) => dst_cid, + Long { dst_cid, .. } => dst_cid, + Retry { dst_cid, .. } => dst_cid, + Short { dst_cid, .. } => dst_cid, + VersionNegotiate { dst_cid, .. } => dst_cid, + } + } + + /// Whether the payload of this packet contains QUIC frames + pub(crate) fn has_frames(&self) -> bool { + use Header::*; + match *self { + Initial(_) => true, + Long { .. } => true, + Retry { .. } => false, + Short { .. } => true, + VersionNegotiate { .. } => false, + } + } +} + +pub(crate) struct PartialEncode { + pub(crate) start: usize, + pub(crate) header_len: usize, + // Packet number length, payload length needed + pn: Option<(usize, bool)>, +} + +impl PartialEncode { + pub(crate) fn finish( + self, + buf: &mut [u8], + header_crypto: &dyn crypto::HeaderKey, + crypto: Option<(u64, &dyn crypto::PacketKey)>, + ) { + let Self { header_len, pn, .. } = self; + let (pn_len, write_len) = match pn { + Some((pn_len, write_len)) => (pn_len, write_len), + None => return, + }; + + let pn_pos = header_len - pn_len; + if write_len { + let len = buf.len() - header_len + pn_len; + assert!(len < 2usize.pow(14)); // Fits in reserved space + let mut slice = &mut buf[pn_pos - 2..pn_pos]; + slice.put_u16(len as u16 | (0b01 << 14)); + } + + if let Some((number, crypto)) = crypto { + crypto.encrypt(number, buf, header_len); + } + + debug_assert!( + pn_pos + 4 + header_crypto.sample_size() <= buf.len(), + "packet must be padded to at least {} bytes for header protection sampling", + pn_pos + 4 + header_crypto.sample_size() + ); + header_crypto.encrypt(pn_pos, buf); + } +} + +/// Plain packet header +#[derive(Clone, Debug)] +pub enum ProtectedHeader { + /// An Initial packet header + Initial(ProtectedInitialHeader), + /// A Long packet header, as used during the handshake + Long { + /// Type of the Long header packet + ty: LongType, + /// Destination Connection ID + dst_cid: ConnectionId, + /// Source Connection ID + src_cid: ConnectionId, + /// Length of the packet payload + len: u64, + /// QUIC version + version: u32, + }, + /// A Retry packet header + Retry { + /// Destination Connection ID + dst_cid: ConnectionId, + /// Source Connection ID + src_cid: ConnectionId, + /// QUIC version + version: u32, + }, + /// A short packet header, as used during the data phase + Short { + /// Spin bit + spin: bool, + /// Destination Connection ID + dst_cid: ConnectionId, + }, + /// A Version Negotiation packet header + VersionNegotiate { + /// Random value + random: u8, + /// Destination Connection ID + dst_cid: ConnectionId, + /// Source Connection ID + src_cid: ConnectionId, + }, +} + +impl ProtectedHeader { + fn as_initial(&self) -> Option<&ProtectedInitialHeader> { + match self { + Self::Initial(x) => Some(x), + _ => None, + } + } + + /// The destination Connection ID of the packet + pub fn dst_cid(&self) -> &ConnectionId { + use ProtectedHeader::*; + match self { + Initial(header) => &header.dst_cid, + Long { dst_cid, .. } => dst_cid, + Retry { dst_cid, .. } => dst_cid, + Short { dst_cid, .. } => dst_cid, + VersionNegotiate { dst_cid, .. } => dst_cid, + } + } + + fn payload_len(&self) -> Option { + use ProtectedHeader::*; + match self { + Initial(ProtectedInitialHeader { len, .. }) | Long { len, .. } => Some(*len), + _ => None, + } + } + + /// Decode a plain header from given buffer, with given [`ConnectionIdParser`]. + pub fn decode( + buf: &mut io::Cursor, + cid_parser: &(impl ConnectionIdParser + ?Sized), + supported_versions: &[u32], + grease_quic_bit: bool, + ) -> Result { + let first = buf.get::()?; + if !grease_quic_bit && first & FIXED_BIT == 0 { + return Err(PacketDecodeError::InvalidHeader("fixed bit unset")); + } + if first & LONG_HEADER_FORM == 0 { + let spin = first & SPIN_BIT != 0; + + Ok(Self::Short { + spin, + dst_cid: cid_parser.parse(buf)?, + }) + } else { + let version = buf.get::()?; + + let dst_cid = ConnectionId::decode_long(buf) + .ok_or(PacketDecodeError::InvalidHeader("malformed cid"))?; + let src_cid = ConnectionId::decode_long(buf) + .ok_or(PacketDecodeError::InvalidHeader("malformed cid"))?; + + // TODO: Support long CIDs for compatibility with future QUIC versions + if version == 0 { + let random = first & !LONG_HEADER_FORM; + return Ok(Self::VersionNegotiate { + random, + dst_cid, + src_cid, + }); + } + + if !supported_versions.contains(&version) { + return Err(PacketDecodeError::UnsupportedVersion { + src_cid, + dst_cid, + version, + }); + } + + match LongHeaderType::from_byte(first)? { + LongHeaderType::Initial => { + let token_len = buf.get_var()? as usize; + let token_start = buf.position() as usize; + if token_len > buf.remaining() { + return Err(PacketDecodeError::InvalidHeader("token out of bounds")); + } + buf.advance(token_len); + + let len = buf.get_var()?; + Ok(Self::Initial(ProtectedInitialHeader { + dst_cid, + src_cid, + token_pos: token_start..token_start + token_len, + len, + version, + })) + } + LongHeaderType::Retry => Ok(Self::Retry { + dst_cid, + src_cid, + version, + }), + LongHeaderType::Standard(ty) => Ok(Self::Long { + ty, + dst_cid, + src_cid, + len: buf.get_var()?, + version, + }), + } + } + } +} + +/// Header of an Initial packet, before decryption +#[derive(Clone, Debug)] +pub struct ProtectedInitialHeader { + /// Destination Connection ID + pub dst_cid: ConnectionId, + /// Source Connection ID + pub src_cid: ConnectionId, + /// The position of a token in the packet buffer + pub token_pos: Range, + /// Length of the packet payload + pub len: u64, + /// QUIC version + pub version: u32, +} + +#[derive(Clone, Debug)] +pub(crate) struct InitialHeader { + pub(crate) dst_cid: ConnectionId, + pub(crate) src_cid: ConnectionId, + pub(crate) token: Bytes, + pub(crate) number: PacketNumber, + pub(crate) version: u32, +} + +// An encoded packet number +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum PacketNumber { + U8(u8), + U16(u16), + U24(u32), + U32(u32), +} + +impl PacketNumber { + pub(crate) fn new(n: u64, largest_acked: u64) -> Self { + let range = (n - largest_acked) * 2; + if range < 1 << 8 { + Self::U8(n as u8) + } else if range < 1 << 16 { + Self::U16(n as u16) + } else if range < 1 << 24 { + Self::U24(n as u32) + } else if range < 1 << 32 { + Self::U32(n as u32) + } else { + panic!("packet number too large to encode") + } + } + + pub(crate) fn len(self) -> usize { + use PacketNumber::*; + match self { + U8(_) => 1, + U16(_) => 2, + U24(_) => 3, + U32(_) => 4, + } + } + + pub(crate) fn encode(self, w: &mut W) { + use PacketNumber::*; + match self { + U8(x) => w.write(x), + U16(x) => w.write(x), + U24(x) => w.put_uint(u64::from(x), 3), + U32(x) => w.write(x), + } + } + + pub(crate) fn decode(len: usize, r: &mut R) -> Result { + use PacketNumber::*; + let pn = match len { + 1 => U8(r.get()?), + 2 => U16(r.get()?), + 3 => U24(r.get_uint(3) as u32), + 4 => U32(r.get()?), + _ => unreachable!(), + }; + Ok(pn) + } + + pub(crate) fn decode_len(tag: u8) -> usize { + 1 + (tag & 0x03) as usize + } + + fn tag(self) -> u8 { + use PacketNumber::*; + match self { + U8(_) => 0b00, + U16(_) => 0b01, + U24(_) => 0b10, + U32(_) => 0b11, + } + } + + pub(crate) fn expand(self, expected: u64) -> u64 { + // From Appendix A + use PacketNumber::*; + let truncated = match self { + U8(x) => u64::from(x), + U16(x) => u64::from(x), + U24(x) => u64::from(x), + U32(x) => u64::from(x), + }; + let nbits = self.len() * 8; + let win = 1 << nbits; + let hwin = win / 2; + let mask = win - 1; + // The incoming packet number should be greater than expected - hwin and less than or equal + // to expected + hwin + // + // This means we can't just strip the trailing bits from expected and add the truncated + // because that might yield a value outside the window. + // + // The following code calculates a candidate value and makes sure it's within the packet + // number window. + let candidate = (expected & !mask) | truncated; + if expected.checked_sub(hwin).is_some_and(|x| candidate <= x) { + candidate + win + } else if candidate > expected + hwin && candidate > win { + candidate - win + } else { + candidate + } + } +} + +/// A [`ConnectionIdParser`] implementation that assumes the connection ID is of fixed length +pub struct FixedLengthConnectionIdParser { + expected_len: usize, +} + +impl FixedLengthConnectionIdParser { + /// Create a new instance of `FixedLengthConnectionIdParser` + pub fn new(expected_len: usize) -> Self { + Self { expected_len } + } +} + +impl ConnectionIdParser for FixedLengthConnectionIdParser { + fn parse(&self, buffer: &mut dyn Buf) -> Result { + (buffer.remaining() >= self.expected_len) + .then(|| ConnectionId::from_buf(buffer, self.expected_len)) + .ok_or(PacketDecodeError::InvalidHeader("packet too small")) + } +} + +/// Parse connection id in short header packet +pub trait ConnectionIdParser { + /// Parse a connection id from given buffer + fn parse(&self, buf: &mut dyn Buf) -> Result; +} + +/// Long packet type including non-uniform cases +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum LongHeaderType { + Initial, + Retry, + Standard(LongType), +} + +impl LongHeaderType { + fn from_byte(b: u8) -> Result { + use {LongHeaderType::*, LongType::*}; + debug_assert!(b & LONG_HEADER_FORM != 0, "not a long packet"); + Ok(match (b & 0x30) >> 4 { + 0x0 => Initial, + 0x1 => Standard(ZeroRtt), + 0x2 => Standard(Handshake), + 0x3 => Retry, + _ => unreachable!(), + }) + } +} + +impl From for u8 { + fn from(ty: LongHeaderType) -> Self { + use {LongHeaderType::*, LongType::*}; + match ty { + Initial => LONG_HEADER_FORM | FIXED_BIT, + Standard(ZeroRtt) => LONG_HEADER_FORM | FIXED_BIT | (0x1 << 4), + Standard(Handshake) => LONG_HEADER_FORM | FIXED_BIT | (0x2 << 4), + Retry => LONG_HEADER_FORM | FIXED_BIT | (0x3 << 4), + } + } +} + +/// Long packet types with uniform header structure +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LongType { + /// Handshake packet + Handshake, + /// 0-RTT packet + ZeroRtt, +} + +/// Packet decode error +#[derive(Debug, Error, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum PacketDecodeError { + /// Packet uses a QUIC version that is not supported + #[error("unsupported version {version:x}")] + UnsupportedVersion { + /// Source Connection ID + src_cid: ConnectionId, + /// Destination Connection ID + dst_cid: ConnectionId, + /// The version that was unsupported + version: u32, + }, + /// The packet header is invalid + #[error("invalid header: {0}")] + InvalidHeader(&'static str), +} + +impl From for PacketDecodeError { + fn from(_: coding::UnexpectedEnd) -> Self { + Self::InvalidHeader("unexpected end of packet") + } +} + +pub(crate) const LONG_HEADER_FORM: u8 = 0x80; +pub(crate) const FIXED_BIT: u8 = 0x40; +pub(crate) const SPIN_BIT: u8 = 0x20; +const SHORT_RESERVED_BITS: u8 = 0x18; +const LONG_RESERVED_BITS: u8 = 0x0c; +const KEY_PHASE_BIT: u8 = 0x04; + +/// Packet number space identifiers +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum SpaceId { + /// Unprotected packets, used to bootstrap the handshake + Initial = 0, + Handshake = 1, + /// Application data space, used for 0-RTT and post-handshake/1-RTT packets + Data = 2, +} + +impl SpaceId { + pub fn iter() -> impl Iterator { + [Self::Initial, Self::Handshake, Self::Data].iter().cloned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use std::io; + + fn check_pn(typed: PacketNumber, encoded: &[u8]) { + let mut buf = Vec::new(); + typed.encode(&mut buf); + assert_eq!(&buf[..], encoded); + let decoded = PacketNumber::decode(typed.len(), &mut io::Cursor::new(&buf)).unwrap(); + assert_eq!(typed, decoded); + } + + #[test] + fn roundtrip_packet_numbers() { + check_pn(PacketNumber::U8(0x7f), &hex!("7f")); + check_pn(PacketNumber::U16(0x80), &hex!("0080")); + check_pn(PacketNumber::U16(0x3fff), &hex!("3fff")); + check_pn(PacketNumber::U32(0x0000_4000), &hex!("0000 4000")); + check_pn(PacketNumber::U32(0xffff_ffff), &hex!("ffff ffff")); + } + + #[test] + fn pn_encode() { + check_pn(PacketNumber::new(0x10, 0), &hex!("10")); + check_pn(PacketNumber::new(0x100, 0), &hex!("0100")); + check_pn(PacketNumber::new(0x10000, 0), &hex!("010000")); + } + + #[test] + fn pn_expand_roundtrip() { + for expected in 0..1024 { + for actual in expected..1024 { + assert_eq!(actual, PacketNumber::new(actual, expected).expand(expected)); + } + } + } + + #[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] + #[test] + fn header_encoding() { + use crate::Side; + use crate::crypto::rustls::{initial_keys, initial_suite_from_provider}; + #[cfg(all(feature = "rustls-aws-lc-rs", not(feature = "rustls-ring")))] + use rustls::crypto::aws_lc_rs::default_provider; + #[cfg(feature = "rustls-ring")] + use rustls::crypto::ring::default_provider; + use rustls::quic::Version; + + let dcid = ConnectionId::new(&hex!("06b858ec6f80452b")); + let provider = default_provider(); + + let suite = initial_suite_from_provider(&std::sync::Arc::new(provider)).unwrap(); + let client = initial_keys(Version::V1, dcid, Side::Client, &suite); + let mut buf = Vec::new(); + let header = Header::Initial(InitialHeader { + number: PacketNumber::U8(0), + src_cid: ConnectionId::new(&[]), + dst_cid: dcid, + token: Bytes::new(), + version: crate::DEFAULT_SUPPORTED_VERSIONS[0], + }); + let encode = header.encode(&mut buf); + let header_len = buf.len(); + buf.resize(header_len + 16 + client.packet.local.tag_len(), 0); + encode.finish( + &mut buf, + &*client.header.local, + Some((0, &*client.packet.local)), + ); + + for byte in &buf { + print!("{byte:02x}"); + } + println!(); + assert_eq!( + buf[..], + hex!( + "c8000000010806b858ec6f80452b00004021be + 3ef50807b84191a196f760a6dad1e9d1c430c48952cba0148250c21c0a6a70e1" + )[..] + ); + + let server = initial_keys(Version::V1, dcid, Side::Server, &suite); + let supported_versions = crate::DEFAULT_SUPPORTED_VERSIONS.to_vec(); + let decode = PartialDecode::new( + buf.as_slice().into(), + &FixedLengthConnectionIdParser::new(0), + &supported_versions, + false, + ) + .unwrap() + .0; + let mut packet = decode.finish(Some(&*server.header.remote)).unwrap(); + assert_eq!( + packet.header_data[..], + hex!("c0000000010806b858ec6f80452b0000402100")[..] + ); + server + .packet + .remote + .decrypt(0, &packet.header_data, &mut packet.payload) + .unwrap(); + assert_eq!(packet.payload[..], [0; 16]); + match packet.header { + Header::Initial(InitialHeader { + number: PacketNumber::U8(0), + .. + }) => {} + _ => { + panic!("unexpected header {:?}", packet.header); + } + } + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/array_range_set.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/array_range_set.rs new file mode 100644 index 0000000..f273391 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/array_range_set.rs @@ -0,0 +1,209 @@ +use std::ops::Range; + +use tinyvec::TinyVec; + +/// A set of u64 values optimized for long runs and random insert/delete/contains +/// +/// `ArrayRangeSet` uses an array representation, where each array entry represents +/// a range. +/// +/// The array-based RangeSet provides 2 benefits: +/// - There exists an inline representation, which avoids the need of heap +/// allocating ACK ranges for SentFrames for small ranges. +/// - Iterating over ranges should usually be faster since there is only +/// a single cache-friendly contiguous range. +/// +/// `ArrayRangeSet` is especially useful for tracking ACK ranges where the amount +/// of ranges is usually very low (since ACK numbers are in consecutive fashion +/// unless reordering or packet loss occur). +#[derive(Debug, Default)] +pub struct ArrayRangeSet(TinyVec<[Range; ARRAY_RANGE_SET_INLINE_CAPACITY]>); + +/// The capacity of elements directly stored in [`ArrayRangeSet`] +/// +/// An inline capacity of 2 is chosen to keep `SentFrame` below 128 bytes. +const ARRAY_RANGE_SET_INLINE_CAPACITY: usize = 2; + +impl Clone for ArrayRangeSet { + fn clone(&self) -> Self { + // tinyvec keeps the heap representation after clones. + // We rather prefer the inline representation for clones if possible, + // since clones (e.g. for storage in `SentFrames`) are rarely mutated + if self.0.is_inline() || self.0.len() > ARRAY_RANGE_SET_INLINE_CAPACITY { + return Self(self.0.clone()); + } + + let mut vec = TinyVec::new(); + vec.extend_from_slice(self.0.as_slice()); + Self(vec) + } +} + +impl ArrayRangeSet { + pub fn new() -> Self { + Default::default() + } + + pub fn iter(&self) -> impl DoubleEndedIterator> + '_ { + self.0.iter().cloned() + } + + pub fn elts(&self) -> impl Iterator + '_ { + self.iter().flatten() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn contains(&self, x: u64) -> bool { + for range in self.0.iter() { + if range.start > x { + // We only get here if there was no prior range that contained x + return false; + } else if range.contains(&x) { + return true; + } + } + false + } + + pub fn subtract(&mut self, other: &Self) { + // TODO: This can potentially be made more efficient, since the we know + // individual ranges are not overlapping, and the next range must start + // after the last one finished + for range in &other.0 { + self.remove(range.clone()); + } + } + + pub fn insert_one(&mut self, x: u64) -> bool { + self.insert(x..x + 1) + } + + pub fn insert(&mut self, x: Range) -> bool { + let mut result = false; + + if x.is_empty() { + // Don't try to deal with ranges where x.end <= x.start + return false; + } + + let mut idx = 0; + while idx != self.0.len() { + let range = &mut self.0[idx]; + + if range.start > x.end { + // The range is fully before this range and therefore not extensible. + // Add a new range to the left + self.0.insert(idx, x); + return true; + } else if range.start > x.start { + // The new range starts before this range but overlaps. + // Extend the current range to the left + // Note that we don't have to merge a potential left range, since + // this case would have been captured by merging the right range + // in the previous loop iteration + result = true; + range.start = x.start; + } + + // At this point we have handled all parts of the new range which + // are in front of the current range. Now we handle everything from + // the start of the current range + + if x.end <= range.end { + // Fully contained + return result; + } else if x.start <= range.end { + // Extend the current range to the end of the new range. + // Since it's not contained it must be bigger + range.end = x.end; + + // Merge all follow-up ranges which overlap + while idx != self.0.len() - 1 { + let curr = self.0[idx].clone(); + let next = self.0[idx + 1].clone(); + if curr.end >= next.start { + self.0[idx].end = next.end.max(curr.end); + self.0.remove(idx + 1); + } else { + break; + } + } + + return true; + } + + idx += 1; + } + + // Insert a range at the end + self.0.push(x); + true + } + + pub fn remove(&mut self, x: Range) -> bool { + let mut result = false; + + if x.is_empty() { + // Don't try to deal with ranges where x.end <= x.start + return false; + } + + let mut idx = 0; + while idx != self.0.len() && x.start != x.end { + let range = self.0[idx].clone(); + + if x.end <= range.start { + // The range is fully before this range + return result; + } else if x.start >= range.end { + // The range is fully after this range + idx += 1; + continue; + } + + // The range overlaps with this range + result = true; + + let left = range.start..x.start; + let right = x.end..range.end; + if left.is_empty() && right.is_empty() { + self.0.remove(idx); + } else if left.is_empty() { + self.0[idx] = right; + idx += 1; + } else if right.is_empty() { + self.0[idx] = left; + idx += 1; + } else { + self.0[idx] = right; + self.0.insert(idx, left); + idx += 2; + } + } + + result + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn pop_min(&mut self) -> Option> { + if !self.0.is_empty() { + Some(self.0.remove(0)) + } else { + None + } + } + + pub fn min(&self) -> Option { + self.iter().next().map(|x| x.start) + } + + pub fn max(&self) -> Option { + self.iter().next_back().map(|x| x.end - 1) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/btree_range_set.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/btree_range_set.rs new file mode 100644 index 0000000..9121bd9 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/btree_range_set.rs @@ -0,0 +1,381 @@ +use std::{ + cmp, + cmp::Ordering, + collections::{BTreeMap, btree_map}, + ops::{ + Bound::{Excluded, Included}, + Range, + }, +}; + +/// A set of u64 values optimized for long runs and random insert/delete/contains +#[derive(Debug, Default, Clone)] +pub struct RangeSet(BTreeMap); + +impl RangeSet { + pub fn new() -> Self { + Default::default() + } + + pub fn contains(&self, x: u64) -> bool { + self.pred(x).is_some_and(|(_, end)| end > x) + } + + pub fn insert_one(&mut self, x: u64) -> bool { + if let Some((start, end)) = self.pred(x) { + match end.cmp(&x) { + // Wholly contained + Ordering::Greater => { + return false; + } + Ordering::Equal => { + // Extend existing + self.0.remove(&start); + let mut new_end = x + 1; + if let Some((next_start, next_end)) = self.succ(x) { + if next_start == new_end { + self.0.remove(&next_start); + new_end = next_end; + } + } + self.0.insert(start, new_end); + return true; + } + _ => {} + } + } + let mut new_end = x + 1; + if let Some((next_start, next_end)) = self.succ(x) { + if next_start == new_end { + self.0.remove(&next_start); + new_end = next_end; + } + } + self.0.insert(x, new_end); + true + } + + pub fn insert(&mut self, mut x: Range) -> bool { + if x.is_empty() { + return false; + } + if let Some((start, end)) = self.pred(x.start) { + if end >= x.end { + // Wholly contained + return false; + } else if end >= x.start { + // Extend overlapping predecessor + self.0.remove(&start); + x.start = start; + } + } + while let Some((next_start, next_end)) = self.succ(x.start) { + if next_start > x.end { + break; + } + // Overlaps with successor + self.0.remove(&next_start); + x.end = cmp::max(next_end, x.end); + } + self.0.insert(x.start, x.end); + true + } + + /// Find closest range to `x` that begins at or before it + fn pred(&self, x: u64) -> Option<(u64, u64)> { + self.0 + .range((Included(0), Included(x))) + .next_back() + .map(|(&x, &y)| (x, y)) + } + + /// Find the closest range to `x` that begins after it + fn succ(&self, x: u64) -> Option<(u64, u64)> { + self.0 + .range((Excluded(x), Included(u64::MAX))) + .next() + .map(|(&x, &y)| (x, y)) + } + + pub fn remove(&mut self, x: Range) -> bool { + if x.is_empty() { + return false; + } + + let before = match self.pred(x.start) { + Some((start, end)) if end > x.start => { + self.0.remove(&start); + if start < x.start { + self.0.insert(start, x.start); + } + if end > x.end { + self.0.insert(x.end, end); + } + // Short-circuit if we cannot possibly overlap with another range + if end >= x.end { + return true; + } + true + } + Some(_) | None => false, + }; + let mut after = false; + while let Some((start, end)) = self.succ(x.start) { + if start >= x.end { + break; + } + after = true; + self.0.remove(&start); + if end > x.end { + self.0.insert(x.end, end); + break; + } + } + before || after + } + + /// Add a range to the set, returning the intersection of current ranges with the new one + pub fn replace(&mut self, mut range: Range) -> Replace<'_> { + let pred = if let Some((prev_start, prev_end)) = self + .pred(range.start) + .filter(|&(_, end)| end >= range.start) + { + self.0.remove(&prev_start); + let replaced_start = range.start; + range.start = range.start.min(prev_start); + let replaced_end = range.end.min(prev_end); + range.end = range.end.max(prev_end); + if replaced_start != replaced_end { + Some(replaced_start..replaced_end) + } else { + None + } + } else { + None + }; + Replace { + set: self, + range, + pred, + } + } + + pub fn add(&mut self, other: &Self) { + for (&start, &end) in &other.0 { + self.insert(start..end); + } + } + + pub fn subtract(&mut self, other: &Self) { + for (&start, &end) in &other.0 { + self.remove(start..end); + } + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn min(&self) -> Option { + self.0.first_key_value().map(|(&start, _)| start) + } + + pub fn max(&self) -> Option { + self.0.last_key_value().map(|(_, &end)| end - 1) + } + + pub fn len(&self) -> usize { + self.0.len() + } + pub fn iter(&self) -> Iter<'_> { + Iter(self.0.iter()) + } + pub fn elts(&self) -> EltIter<'_> { + EltIter { + inner: self.0.iter(), + next: 0, + end: 0, + } + } + + pub fn peek_min(&self) -> Option> { + let (&start, &end) = self.0.iter().next()?; + Some(start..end) + } + + pub fn pop_min(&mut self) -> Option> { + let result = self.peek_min()?; + self.0.remove(&result.start); + Some(result) + } +} + +pub struct Iter<'a>(btree_map::Iter<'a, u64, u64>); + +impl Iterator for Iter<'_> { + type Item = Range; + fn next(&mut self) -> Option> { + let (&start, &end) = self.0.next()?; + Some(start..end) + } +} + +impl DoubleEndedIterator for Iter<'_> { + fn next_back(&mut self) -> Option> { + let (&start, &end) = self.0.next_back()?; + Some(start..end) + } +} + +impl<'a> IntoIterator for &'a RangeSet { + type Item = Range; + type IntoIter = Iter<'a>; + fn into_iter(self) -> Iter<'a> { + self.iter() + } +} + +pub struct EltIter<'a> { + inner: btree_map::Iter<'a, u64, u64>, + next: u64, + end: u64, +} + +impl Iterator for EltIter<'_> { + type Item = u64; + fn next(&mut self) -> Option { + if self.next == self.end { + let (&start, &end) = self.inner.next()?; + self.next = start; + self.end = end; + } + let x = self.next; + self.next += 1; + Some(x) + } +} + +impl DoubleEndedIterator for EltIter<'_> { + fn next_back(&mut self) -> Option { + if self.next == self.end { + let (&start, &end) = self.inner.next_back()?; + self.next = start; + self.end = end; + } + self.end -= 1; + Some(self.end) + } +} + +/// Iterator returned by `RangeSet::replace` +pub struct Replace<'a> { + set: &'a mut RangeSet, + /// Portion of the intersection arising from a range beginning at or before the newly inserted + /// range + pred: Option>, + /// Union of the input range and all ranges that have been visited by the iterator so far + range: Range, +} + +impl Iterator for Replace<'_> { + type Item = Range; + fn next(&mut self) -> Option> { + if let Some(pred) = self.pred.take() { + // If a range starting before the inserted range overlapped with it, return the + // corresponding overlap first + return Some(pred); + } + + let (next_start, next_end) = self.set.succ(self.range.start)?; + if next_start > self.range.end { + // If the next successor range starts after the current range ends, there can be no more + // overlaps. This is sound even when `self.range.end` is increased because `RangeSet` is + // guaranteed not to contain pairs of ranges that could be simplified. + return None; + } + // Remove the redundant range... + self.set.0.remove(&next_start); + // ...and handle the case where the redundant range ends later than the new range. + let replaced_end = self.range.end.min(next_end); + self.range.end = self.range.end.max(next_end); + if next_start == replaced_end { + // If the redundant range started exactly where the new range ended, there was no + // overlap with it or any later range. + None + } else { + Some(next_start..replaced_end) + } + } +} + +impl Drop for Replace<'_> { + fn drop(&mut self) { + // Ensure we drain all remaining overlapping ranges + for _ in &mut *self {} + // Insert the final aggregate range + self.set.0.insert(self.range.start, self.range.end); + } +} + +/// This module contains tests which only apply for this `RangeSet` implementation +/// +/// Tests which apply for all implementations can be found in the `tests.rs` module +#[cfg(test)] +mod tests { + #![allow(clippy::single_range_in_vec_init)] // https://github.com/rust-lang/rust-clippy/issues/11086 + use super::*; + + #[test] + fn replace_contained() { + let mut set = RangeSet::new(); + set.insert(2..4); + assert_eq!(set.replace(1..5).collect::>(), &[2..4]); + assert_eq!(set.len(), 1); + assert_eq!(set.peek_min().unwrap(), 1..5); + } + + #[test] + fn replace_contains() { + let mut set = RangeSet::new(); + set.insert(1..5); + assert_eq!(set.replace(2..4).collect::>(), &[2..4]); + assert_eq!(set.len(), 1); + assert_eq!(set.peek_min().unwrap(), 1..5); + } + + #[test] + fn replace_pred() { + let mut set = RangeSet::new(); + set.insert(2..4); + assert_eq!(set.replace(3..5).collect::>(), &[3..4]); + assert_eq!(set.len(), 1); + assert_eq!(set.peek_min().unwrap(), 2..5); + } + + #[test] + fn replace_succ() { + let mut set = RangeSet::new(); + set.insert(2..4); + assert_eq!(set.replace(1..3).collect::>(), &[2..3]); + assert_eq!(set.len(), 1); + assert_eq!(set.peek_min().unwrap(), 1..4); + } + + #[test] + fn replace_exact_pred() { + let mut set = RangeSet::new(); + set.insert(2..4); + assert_eq!(set.replace(4..6).collect::>(), &[]); + assert_eq!(set.len(), 1); + assert_eq!(set.peek_min().unwrap(), 2..6); + } + + #[test] + fn replace_exact_succ() { + let mut set = RangeSet::new(); + set.insert(2..4); + assert_eq!(set.replace(0..2).collect::>(), &[]); + assert_eq!(set.len(), 1); + assert_eq!(set.peek_min().unwrap(), 0..4); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/mod.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/mod.rs new file mode 100644 index 0000000..9f16e7e --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/mod.rs @@ -0,0 +1,7 @@ +mod array_range_set; +mod btree_range_set; +#[cfg(test)] +mod tests; + +pub(crate) use array_range_set::ArrayRangeSet; +pub(crate) use btree_range_set::RangeSet; diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/tests.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/tests.rs new file mode 100644 index 0000000..1e75da4 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/range_set/tests.rs @@ -0,0 +1,263 @@ +use std::ops::Range; + +use super::*; + +macro_rules! common_set_tests { + ($set_name:ident, $set_type:ident) => { + mod $set_name { + use super::*; + + #[test] + fn merge_and_split() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(set.insert(2..4)); + assert!(!set.insert(1..3)); + assert_eq!(set.len(), 1); + assert_eq!(&set.elts().collect::>()[..], [0, 1, 2, 3]); + assert!(!set.contains(4)); + assert!(set.remove(2..3)); + assert_eq!(set.len(), 2); + assert!(!set.contains(2)); + assert_eq!(&set.elts().collect::>()[..], [0, 1, 3]); + } + + #[test] + fn double_merge_exact() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(set.insert(4..6)); + assert_eq!(set.len(), 2); + assert!(set.insert(2..4)); + assert_eq!(set.len(), 1); + assert_eq!(&set.elts().collect::>()[..], [0, 1, 2, 3, 4, 5]); + } + + #[test] + fn single_merge_low() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(set.insert(4..6)); + assert_eq!(set.len(), 2); + assert!(set.insert(2..3)); + assert_eq!(set.len(), 2); + assert_eq!(&set.elts().collect::>()[..], [0, 1, 2, 4, 5]); + } + + #[test] + fn single_merge_high() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(set.insert(4..6)); + assert_eq!(set.len(), 2); + assert!(set.insert(3..4)); + assert_eq!(set.len(), 2); + assert_eq!(&set.elts().collect::>()[..], [0, 1, 3, 4, 5]); + } + + #[test] + fn double_merge_wide() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(set.insert(4..6)); + assert_eq!(set.len(), 2); + assert!(set.insert(1..5)); + assert_eq!(set.len(), 1); + assert_eq!(&set.elts().collect::>()[..], [0, 1, 2, 3, 4, 5]); + } + + #[test] + fn double_remove() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(set.insert(4..6)); + assert!(set.remove(1..5)); + assert_eq!(set.len(), 2); + assert_eq!(&set.elts().collect::>()[..], [0, 5]); + } + + #[test] + fn insert_multiple() { + let mut set = $set_type::new(); + assert!(set.insert(0..1)); + assert!(set.insert(2..3)); + assert!(set.insert(4..5)); + assert!(set.insert(0..5)); + assert_eq!(set.len(), 1); + } + + #[test] + fn remove_multiple() { + let mut set = $set_type::new(); + assert!(set.insert(0..1)); + assert!(set.insert(2..3)); + assert!(set.insert(4..5)); + assert!(set.remove(0..5)); + assert!(set.is_empty()); + } + + #[test] + fn double_insert() { + let mut set = $set_type::new(); + assert!(set.insert(0..2)); + assert!(!set.insert(0..2)); + assert!(set.insert(2..4)); + assert!(!set.insert(2..4)); + assert!(!set.insert(0..4)); + assert!(!set.insert(1..2)); + assert!(!set.insert(1..3)); + assert!(!set.insert(1..4)); + assert_eq!(set.len(), 1); + } + + #[test] + fn skip_empty_ranges() { + let mut set = $set_type::new(); + assert!(!set.insert(2..2)); + assert_eq!(set.len(), 0); + assert!(!set.insert(4..4)); + assert_eq!(set.len(), 0); + assert!(!set.insert(0..0)); + assert_eq!(set.len(), 0); + } + + #[test] + fn compare_insert_to_reference() { + const MAX_RANGE: u64 = 50; + + for start in 0..=MAX_RANGE { + for end in 0..=MAX_RANGE { + println!("insert({}..{})", start, end); + let (mut set, mut reference) = create_initial_sets(MAX_RANGE); + assert_eq!(set.insert(start..end), reference.insert(start..end)); + assert_sets_equal(&set, &reference); + } + } + } + + #[test] + fn compare_remove_to_reference() { + const MAX_RANGE: u64 = 50; + + for start in 0..=MAX_RANGE { + for end in 0..=MAX_RANGE { + println!("remove({}..{})", start, end); + let (mut set, mut reference) = create_initial_sets(MAX_RANGE); + assert_eq!(set.remove(start..end), reference.remove(start..end)); + assert_sets_equal(&set, &reference); + } + } + } + + #[test] + fn min_max() { + let mut set = $set_type::new(); + set.insert(1..3); + set.insert(4..5); + set.insert(6..10); + assert_eq!(set.min(), Some(1)); + assert_eq!(set.max(), Some(9)); + } + + fn create_initial_sets(max_range: u64) -> ($set_type, RefRangeSet) { + let mut set = $set_type::new(); + let mut reference = RefRangeSet::new(max_range as usize); + assert_sets_equal(&set, &reference); + + assert_eq!(set.insert(2..6), reference.insert(2..6)); + assert_eq!(set.insert(10..14), reference.insert(10..14)); + assert_eq!(set.insert(14..14), reference.insert(14..14)); + assert_eq!(set.insert(18..19), reference.insert(18..19)); + assert_eq!(set.insert(20..21), reference.insert(20..21)); + assert_eq!(set.insert(22..24), reference.insert(22..24)); + assert_eq!(set.insert(26..30), reference.insert(26..30)); + assert_eq!(set.insert(34..38), reference.insert(34..38)); + assert_eq!(set.insert(42..44), reference.insert(42..44)); + + assert_sets_equal(&set, &reference); + + (set, reference) + } + + fn assert_sets_equal(set: &$set_type, reference: &RefRangeSet) { + assert_eq!(set.len(), reference.len()); + assert_eq!(set.is_empty(), reference.is_empty()); + assert_eq!(set.elts().collect::>()[..], reference.elts()[..]); + } + } + }; +} + +common_set_tests!(range_set, RangeSet); +common_set_tests!(array_range_set, ArrayRangeSet); + +/// A very simple reference implementation of a RangeSet +struct RefRangeSet { + data: Vec, +} + +impl RefRangeSet { + fn new(capacity: usize) -> Self { + Self { + data: vec![false; capacity], + } + } + + fn len(&self) -> usize { + let mut last = false; + let mut count = 0; + + for v in self.data.iter() { + if !last && *v { + count += 1; + } + last = *v; + } + + count + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn insert(&mut self, x: Range) -> bool { + let mut result = false; + + assert!(x.end <= self.data.len() as u64); + + for i in x { + let i = i as usize; + if !self.data[i] { + result = true; + self.data[i] = true; + } + } + + result + } + + fn remove(&mut self, x: Range) -> bool { + let mut result = false; + + assert!(x.end <= self.data.len() as u64); + + for i in x { + let i = i as usize; + if self.data[i] { + result = true; + self.data[i] = false; + } + } + + result + } + + fn elts(&self) -> Vec { + self.data + .iter() + .enumerate() + .filter_map(|(i, e)| if *e { Some(i as u64) } else { None }) + .collect() + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/shared.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/shared.rs new file mode 100644 index 0000000..f2d0ad5 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/shared.rs @@ -0,0 +1,180 @@ +use std::{fmt, net::SocketAddr}; + +use bytes::{Buf, BufMut, BytesMut}; + +use crate::{Instant, MAX_CID_SIZE, ResetToken, coding::BufExt, packet::PartialDecode}; + +/// Events sent from an Endpoint to a Connection +#[derive(Debug)] +pub struct ConnectionEvent(pub(crate) ConnectionEventInner); + +#[derive(Debug)] +pub(crate) enum ConnectionEventInner { + /// A datagram has been received for the Connection + Datagram(DatagramConnectionEvent), + /// New connection identifiers have been issued for the Connection + NewIdentifiers(Vec, Instant), +} + +/// Variant of [`ConnectionEventInner`]. +#[derive(Debug)] +pub(crate) struct DatagramConnectionEvent { + pub(crate) now: Instant, + pub(crate) remote: SocketAddr, + pub(crate) ecn: Option, + pub(crate) first_decode: PartialDecode, + pub(crate) remaining: Option, +} + +/// Events sent from a Connection to an Endpoint +#[derive(Debug)] +pub struct EndpointEvent(pub(crate) EndpointEventInner); + +impl EndpointEvent { + /// Construct an event that indicating that a `Connection` will no longer emit events + /// + /// Useful for notifying an `Endpoint` that a `Connection` has been destroyed outside of the + /// usual state machine flow, e.g. when being dropped by the user. + pub fn drained() -> Self { + Self(EndpointEventInner::Drained) + } + + /// Determine whether this is the last event a `Connection` will emit + /// + /// Useful for determining when connection-related event loop state can be freed. + pub fn is_drained(&self) -> bool { + self.0 == EndpointEventInner::Drained + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum EndpointEventInner { + /// The connection has been drained + Drained, + /// The reset token and/or address eligible for generating resets has been updated + ResetToken(SocketAddr, ResetToken), + /// The connection needs connection identifiers + NeedIdentifiers(Instant, u64), + /// Stop routing connection ID for this sequence number to the connection + /// When `bool == true`, a new connection ID will be issued to peer + RetireConnectionId(Instant, u64, bool), +} + +/// Protocol-level identifier for a connection. +/// +/// Mainly useful for identifying this connection's packets on the wire with tools like Wireshark. +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ConnectionId { + /// length of CID + len: u8, + /// CID in byte array + bytes: [u8; MAX_CID_SIZE], +} + +impl ConnectionId { + /// Construct cid from byte array + pub fn new(bytes: &[u8]) -> Self { + debug_assert!(bytes.len() <= MAX_CID_SIZE); + let mut res = Self { + len: bytes.len() as u8, + bytes: [0; MAX_CID_SIZE], + }; + res.bytes[..bytes.len()].copy_from_slice(bytes); + res + } + + /// Constructs cid by reading `len` bytes from a `Buf` + /// + /// Callers need to assure that `buf.remaining() >= len` + pub fn from_buf(buf: &mut (impl Buf + ?Sized), len: usize) -> Self { + debug_assert!(len <= MAX_CID_SIZE); + let mut res = Self { + len: len as u8, + bytes: [0; MAX_CID_SIZE], + }; + buf.copy_to_slice(&mut res[..len]); + res + } + + /// Decode from long header format + pub(crate) fn decode_long(buf: &mut impl Buf) -> Option { + let len = buf.get::().ok()? as usize; + match len > MAX_CID_SIZE || buf.remaining() < len { + false => Some(Self::from_buf(buf, len)), + true => None, + } + } + + /// Encode in long header format + pub(crate) fn encode_long(&self, buf: &mut impl BufMut) { + buf.put_u8(self.len() as u8); + buf.put_slice(self); + } +} + +impl ::std::ops::Deref for ConnectionId { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.bytes[0..self.len as usize] + } +} + +impl ::std::ops::DerefMut for ConnectionId { + fn deref_mut(&mut self) -> &mut [u8] { + &mut self.bytes[0..self.len as usize] + } +} + +impl fmt::Debug for ConnectionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.bytes[0..self.len as usize].fmt(f) + } +} + +impl fmt::Display for ConnectionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.iter() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +/// Explicit congestion notification codepoint +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum EcnCodepoint { + /// The ECT(0) codepoint, indicating that an endpoint is ECN-capable + Ect0 = 0b10, + /// The ECT(1) codepoint, indicating that an endpoint is ECN-capable + Ect1 = 0b01, + /// The CE codepoint, signalling that congestion was experienced + Ce = 0b11, +} + +impl EcnCodepoint { + /// Create new object from the given bits + pub fn from_bits(x: u8) -> Option { + use EcnCodepoint::*; + Some(match x & 0b11 { + 0b10 => Ect0, + 0b01 => Ect1, + 0b11 => Ce, + _ => { + return None; + } + }) + } + + /// Returns whether the codepoint is a CE, signalling that congestion was experienced + pub fn is_ce(self) -> bool { + matches!(self, Self::Ce) + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct IssuedCid { + pub(crate) sequence: u64, + pub(crate) id: ConnectionId, + pub(crate) reset_token: ResetToken, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/mod.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/mod.rs new file mode 100644 index 0000000..1ecb398 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/mod.rs @@ -0,0 +1,3376 @@ +use std::{ + convert::TryInto, + mem, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::{Arc, Mutex}, +}; + +use assert_matches::assert_matches; +#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] +use aws_lc_rs::hmac; +use bytes::{Bytes, BytesMut}; +use hex_literal::hex; +use rand::RngCore; +#[cfg(feature = "ring")] +use ring::hmac; +#[cfg(all(feature = "rustls-aws-lc-rs", not(feature = "rustls-ring")))] +use rustls::crypto::aws_lc_rs::default_provider; +#[cfg(feature = "rustls-ring")] +use rustls::crypto::ring::default_provider; +use rustls::{ + AlertDescription, RootCertStore, + pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, + server::WebPkiClientVerifier, +}; +use tracing::info; + +use super::*; +use crate::{ + Duration, Instant, + cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator}, + crypto::rustls::QuicServerConfig, + frame::FrameStruct, + transport_parameters::TransportParameters, +}; +mod util; +use util::*; + +mod token; + +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +use wasm_bindgen_test::wasm_bindgen_test as test; + +// Enable this if you want to run these tests in the browser. +// Unfortunately it's either-or: Enable this and you can run in the browser, disable to run in nodejs. +// #[cfg(all(target_family = "wasm", target_os = "unknown"))] +// wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[test] +fn version_negotiate_server() { + let _guard = subscribe(); + let client_addr = "[::2]:7890".parse().unwrap(); + let mut server = Endpoint::new( + Default::default(), + Some(Arc::new(server_config())), + true, + None, + ); + let now = Instant::now(); + let mut buf = Vec::with_capacity(server.config().get_max_udp_payload_size() as usize); + let event = server.handle( + now, + client_addr, + None, + None, + // Long-header packet with reserved version number + hex!("80 0a1a2a3a 04 00000000 04 00000000 00")[..].into(), + &mut buf, + ); + let Some(DatagramEvent::Response(Transmit { .. })) = event else { + panic!("expected a response"); + }; + + assert_ne!(buf[0] & 0x80, 0); + assert_eq!(&buf[1..15], hex!("00000000 04 00000000 04 00000000")); + assert!(buf[15..].chunks(4).any(|x| { + DEFAULT_SUPPORTED_VERSIONS.contains(&u32::from_be_bytes(x.try_into().unwrap())) + })); +} + +#[test] +fn version_negotiate_client() { + let _guard = subscribe(); + let server_addr = "[::2]:7890".parse().unwrap(); + // Configure client to use empty CIDs so we can easily hardcode a server version negotiation + // packet + let cid_generator_factory: fn() -> Box = + || Box::new(RandomConnectionIdGenerator::new(0)); + let mut client = Endpoint::new( + Arc::new(EndpointConfig { + connection_id_generator_factory: Arc::new(cid_generator_factory), + ..Default::default() + }), + None, + true, + None, + ); + let (_, mut client_ch) = client + .connect(Instant::now(), client_config(), server_addr, "localhost") + .unwrap(); + let now = Instant::now(); + let mut buf = Vec::with_capacity(client.config().get_max_udp_payload_size() as usize); + let opt_event = client.handle( + now, + server_addr, + None, + None, + // Version negotiation packet for reserved version, with empty DCID + hex!( + "80 00000000 00 04 00000000 + 0a1a2a3a" + )[..] + .into(), + &mut buf, + ); + if let Some(DatagramEvent::ConnectionEvent(_, event)) = opt_event { + client_ch.handle_event(event); + } + assert_matches!( + client_ch.poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::VersionMismatch, + }) + ); +} + +#[test] +fn lifecycle() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert!(pair.client_conn_mut(client_ch).using_ecn()); + assert!(pair.server_conn_mut(server_ch).using_ecn()); + + const REASON: &[u8] = b"whee"; + info!("closing"); + pair.client.connections.get_mut(&client_ch).unwrap().close( + pair.time, + VarInt(42), + REASON.into(), + ); + pair.drive(); + assert_matches!(pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ApplicationClosed( + ApplicationClose { error_code: VarInt(42), ref reason } + )}) if reason == REASON); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn draft_version_compat() { + let _guard = subscribe(); + + let mut client_config = client_config(); + client_config.version(0xff00_0020); + + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect_with(client_config); + + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert!(pair.client_conn_mut(client_ch).using_ecn()); + assert!(pair.server_conn_mut(server_ch).using_ecn()); + + const REASON: &[u8] = b"whee"; + info!("closing"); + pair.client.connections.get_mut(&client_ch).unwrap().close( + pair.time, + VarInt(42), + REASON.into(), + ); + pair.drive(); + assert_matches!(pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ApplicationClosed( + ApplicationClose { error_code: VarInt(42), ref reason } + )}) if reason == REASON); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn server_stateless_reset() { + let _guard = subscribe(); + let mut key_material = vec![0; 64]; + let mut rng = rand::rng(); + rng.fill_bytes(&mut key_material); + let reset_key = hmac::Key::new(hmac::HMAC_SHA256, &key_material); + rng.fill_bytes(&mut key_material); + + let mut endpoint_config = EndpointConfig::new(Arc::new(reset_key)); + endpoint_config.cid_generator(move || Box::new(HashedConnectionIdGenerator::from_key(0))); + let endpoint_config = Arc::new(endpoint_config); + + let mut pair = Pair::new(endpoint_config.clone(), server_config()); + let (client_ch, _) = pair.connect(); + pair.drive(); // Flush any post-handshake frames + pair.server.endpoint = + Endpoint::new(endpoint_config, Some(Arc::new(server_config())), true, None); + // Force the server to generate the smallest possible stateless reset + pair.client.connections.get_mut(&client_ch).unwrap().ping(); + info!("resetting"); + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::Reset + }) + ); +} + +#[test] +fn client_stateless_reset() { + let _guard = subscribe(); + let mut key_material = vec![0; 64]; + let mut rng = rand::rng(); + rng.fill_bytes(&mut key_material); + let reset_key = hmac::Key::new(hmac::HMAC_SHA256, &key_material); + rng.fill_bytes(&mut key_material); + + let mut endpoint_config = EndpointConfig::new(Arc::new(reset_key)); + endpoint_config.cid_generator(move || Box::new(HashedConnectionIdGenerator::from_key(0))); + let endpoint_config = Arc::new(endpoint_config); + + let mut pair = Pair::new(endpoint_config.clone(), server_config()); + let (_, server_ch) = pair.connect(); + pair.client.endpoint = + Endpoint::new(endpoint_config, Some(Arc::new(server_config())), true, None); + // Send something big enough to allow room for a smaller stateless reset. + pair.server.connections.get_mut(&server_ch).unwrap().close( + pair.time, + VarInt(42), + (&[0xab; 128][..]).into(), + ); + info!("resetting"); + pair.drive(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::Reset + }) + ); +} + +/// Verify that stateless resets are rate-limited +#[test] +fn stateless_reset_limit() { + let _guard = subscribe(); + let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 42); + let mut endpoint_config = EndpointConfig::default(); + endpoint_config.cid_generator(move || Box::new(RandomConnectionIdGenerator::new(8))); + let endpoint_config = Arc::new(endpoint_config); + let mut endpoint = Endpoint::new( + endpoint_config.clone(), + Some(Arc::new(server_config())), + true, + None, + ); + let time = Instant::now(); + let mut buf = Vec::new(); + let event = endpoint.handle(time, remote, None, None, [0u8; 1024][..].into(), &mut buf); + assert!(matches!(event, Some(DatagramEvent::Response(_)))); + let event = endpoint.handle(time, remote, None, None, [0u8; 1024][..].into(), &mut buf); + assert!(event.is_none()); + let event = endpoint.handle( + time + endpoint_config.min_reset_interval - Duration::from_nanos(1), + remote, + None, + None, + [0u8; 1024][..].into(), + &mut buf, + ); + assert!(event.is_none()); + let event = endpoint.handle( + time + endpoint_config.min_reset_interval, + remote, + None, + None, + [0u8; 1024][..].into(), + &mut buf, + ); + assert!(matches!(event, Some(DatagramEvent::Response(_)))); +} + +#[test] +fn export_keying_material() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + const LABEL: &[u8] = b"test_label"; + const CONTEXT: &[u8] = b"test_context"; + + // client keying material + let mut client_buf = [0u8; 64]; + pair.client_conn_mut(client_ch) + .crypto_session() + .export_keying_material(&mut client_buf, LABEL, CONTEXT) + .unwrap(); + + // server keying material + let mut server_buf = [0u8; 64]; + pair.server_conn_mut(server_ch) + .crypto_session() + .export_keying_material(&mut server_buf, LABEL, CONTEXT) + .unwrap(); + + assert_eq!(&client_buf[..], &server_buf[..]); +} + +#[test] +fn finish_stream_simple() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + assert_eq!(pair.client_streams(client_ch).send_streams(), 1); + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive(); + + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Finished { id })) if id == s + ); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_eq!(pair.client_streams(client_ch).send_streams(), 0); + assert_eq!(pair.server_conn_mut(client_ch).streams().send_streams(), 0); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + // Receive-only streams do not get `StreamFinished` events + assert_eq!(pair.server_conn_mut(client_ch).streams().send_streams(), 0); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); +} + +#[test] +fn reset_stream() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive(); + + info!("resetting stream"); + const ERROR: VarInt = VarInt(42); + pair.client_send(client_ch, s).reset(ERROR).unwrap(); + pair.drive(); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!(chunks.next(usize::MAX), Err(ReadError::Reset(ERROR))); + let _ = chunks.finalize(); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); +} + +#[test] +fn stop_stream() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive(); + + info!("stopping stream"); + const ERROR: VarInt = VarInt(42); + pair.server_recv(server_ch, s).stop(ERROR).unwrap(); + pair.drive(); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + + assert_matches!( + pair.client_send(client_ch, s).write(b"foo"), + Err(WriteError::Stopped(ERROR)) + ); + assert_matches!( + pair.client_send(client_ch, s).finish(), + Err(FinishError::Stopped(ERROR)) + ); +} + +#[test] +fn reject_self_signed_server_cert() { + let _guard = subscribe(); + let mut pair = Pair::default(); + info!("connecting"); + + // Create a self-signed certificate with a different distinguished name than the default one, + // such that path building cannot confuse the default root the server is using and the one + // the client is trusting (in which case we'd get a different error). + let mut cert = rcgen::CertificateParams::new(["localhost".into()]).unwrap(); + let mut issuer = rcgen::DistinguishedName::new(); + issuer.push( + rcgen::DnType::OrganizationName, + "Crazy Quinn's House of Certificates", + ); + cert.distinguished_name = issuer; + let cert = cert + .self_signed(&rcgen::KeyPair::generate().unwrap()) + .unwrap(); + let client_ch = pair.begin_connect(client_config_with_certs(vec![cert.into()])); + + pair.drive(); + + assert_matches!(pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::TransportError(ref error)}) + if error.code == TransportErrorCode::crypto(AlertDescription::UnknownCA.into())); +} + +#[test] +fn reject_missing_client_cert() { + let _guard = subscribe(); + + let mut store = RootCertStore::empty(); + // `WebPkiClientVerifier` requires a non-empty store, so we stick our own certificate into it + // because it's convenient. + store.add(CERTIFIED_KEY.cert.der().clone()).unwrap(); + + let key = PrivatePkcs8KeyDer::from(CERTIFIED_KEY.signing_key.serialize_der()); + let cert = CERTIFIED_KEY.cert.der().clone(); + + let provider = Arc::new(default_provider()); + let config = rustls::ServerConfig::builder_with_provider(provider.clone()) + .with_protocol_versions(&[&rustls::version::TLS13]) + .unwrap() + .with_client_cert_verifier( + WebPkiClientVerifier::builder_with_provider(Arc::new(store), provider) + .build() + .unwrap(), + ) + .with_single_cert(vec![cert], PrivateKeyDer::from(key)) + .unwrap(); + let config = QuicServerConfig::try_from(config).unwrap(); + + let mut pair = Pair::new( + Default::default(), + ServerConfig::with_crypto(Arc::new(config)), + ); + + info!("connecting"); + let client_ch = pair.begin_connect(client_config()); + pair.drive(); + + // The client completes the connection, but finds it immediately closed + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); + assert_matches!(pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ConnectionClosed(ref close)}) + if close.error_code == TransportErrorCode::crypto(AlertDescription::CertificateRequired.into())); + + // The server never completes the connection + let server_ch = pair.server.assert_accept(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!(pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::TransportError(ref error)}) + if error.code == TransportErrorCode::crypto(AlertDescription::CertificateRequired.into())); +} + +#[test] +fn congestion() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + + const TARGET: u64 = 2048; + assert!(pair.client_conn_mut(client_ch).congestion_window() > TARGET); + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + // Send data without receiving ACKs until the congestion state falls below target + while pair.client_conn_mut(client_ch).congestion_window() > TARGET { + let n = pair.client_send(client_ch, s).write(&[42; 1024]).unwrap(); + assert_eq!(n, 1024); + pair.drive_client(); + } + // Ensure that the congestion state recovers after receiving the ACKs + pair.drive(); + assert!(pair.client_conn_mut(client_ch).congestion_window() >= TARGET); + pair.client_send(client_ch, s).write(&[42; 1024]).unwrap(); +} + +#[test] +fn high_latency_handshake() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.latency = Duration::from_micros(200 * 1000); + let (client_ch, server_ch) = pair.connect(); + assert_eq!(pair.client_conn_mut(client_ch).bytes_in_flight(), 0); + assert_eq!(pair.server_conn_mut(server_ch).bytes_in_flight(), 0); + assert!(pair.client_conn_mut(client_ch).using_ecn()); + assert!(pair.server_conn_mut(server_ch).using_ecn()); +} + +#[test] +fn zero_rtt_happypath() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.server.handle_incoming = Box::new(validate_incoming); + let config = client_config(); + + // Establish normal connection + let client_ch = pair.begin_connect(config.clone()); + pair.drive(); + pair.server.assert_accept(); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(0), [][..].into()); + pair.drive(); + + pair.client.addr = SocketAddr::new( + Ipv6Addr::LOCALHOST.into(), + CLIENT_PORTS.lock().unwrap().next().unwrap(), + ); + info!("resuming session"); + let client_ch = pair.begin_connect(config); + assert!(pair.client_conn_mut(client_ch).has_0rtt()); + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + const MSG: &[u8] = b"Hello, 0-RTT!"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive(); + + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); + + assert!(pair.client_conn_mut(client_ch).accepted_0rtt()); + let server_ch = pair.server.assert_accept(); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + // We don't currently preserve stream event order wrt. connection events + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + let _ = chunks.finalize(); + assert_eq!(pair.client_conn_mut(client_ch).stats().path.lost_packets, 0); +} + +#[test] +fn zero_rtt_rejection() { + let _guard = subscribe(); + let server_config = ServerConfig::with_crypto(Arc::new(server_crypto_with_alpn(vec![ + "foo".into(), + "bar".into(), + ]))); + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + let mut client_crypto = Arc::new(client_crypto_with_alpn(vec!["foo".into()])); + let client_config = ClientConfig::new(client_crypto.clone()); + + // Establish normal connection + let client_ch = pair.begin_connect(client_config); + pair.drive(); + let server_ch = pair.server.assert_accept(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(0), [][..].into()); + pair.drive(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { .. }) + ); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + pair.client.connections.clear(); + pair.server.connections.clear(); + + // We want to have a TLS client config with the existing session cache (so resumption could + // happen), but with different ALPN protocols (so that the server must reject it). Reuse + // the existing `ClientConfig` and change the ALPN protocols to make that happen. + let this = Arc::get_mut(&mut client_crypto).expect("QuicClientConfig is shared"); + let inner = Arc::get_mut(&mut this.inner).expect("QuicClientConfig.inner is shared"); + inner.alpn_protocols = vec!["bar".into()]; + + // Changing protocols invalidates 0-RTT + let client_config = ClientConfig::new(client_crypto); + info!("resuming session"); + let client_ch = pair.begin_connect(client_config); + assert!(pair.client_conn_mut(client_ch).has_0rtt()); + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + const MSG: &[u8] = b"Hello, 0-RTT!"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive(); + assert!(!pair.client_conn_mut(client_ch).accepted_0rtt()); + let server_ch = pair.server.assert_accept(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + let s2 = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + assert_eq!(s, s2); + + let mut recv = pair.server_recv(server_ch, s2); + let mut chunks = recv.read(false).unwrap(); + assert_eq!(chunks.next(usize::MAX), Err(ReadError::Blocked)); + let _ = chunks.finalize(); + assert_eq!(pair.client_conn_mut(client_ch).stats().path.lost_packets, 0); +} + +fn test_zero_rtt_incoming_limit(configure_server: F) { + // caller sets the server limit to 4000 bytes + // the client writes 8000 bytes + const CLIENT_WRITES: usize = 8000; + // this gets split across 8 packets + // the first packet is stored in the Incoming + // the next three are incoming-buffered, bringing the incoming buffer size to 3600 bytes + // the last four are dropped due to the buffering limit and must be retransmitted + const EXPECTED_DROPPED: u64 = 4; + + let _guard = subscribe(); + let mut server_config = server_config(); + configure_server(&mut server_config); + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + let config = client_config(); + + // Establish normal connection + let client_ch = pair.begin_connect(config.clone()); + pair.drive(); + pair.server.assert_accept(); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(0), [][..].into()); + pair.drive(); + + pair.client.addr = SocketAddr::new( + Ipv6Addr::LOCALHOST.into(), + CLIENT_PORTS.lock().unwrap().next().unwrap(), + ); + info!("resuming session"); + pair.server.handle_incoming = Box::new(|_| IncomingConnectionBehavior::Wait); + let client_ch = pair.begin_connect(config); + assert!(pair.client_conn_mut(client_ch).has_0rtt()); + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + pair.client_send(client_ch, s) + .write(&vec![0; CLIENT_WRITES]) + .unwrap(); + pair.drive(); + let incoming = pair.server.waiting_incoming.pop().unwrap(); + assert!(pair.server.waiting_incoming.is_empty()); + let _ = pair.server.try_accept(incoming, pair.time); + pair.drive(); + + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); + + assert!(pair.client_conn_mut(client_ch).accepted_0rtt()); + let server_ch = pair.server.assert_accept(); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + // We don't currently preserve stream event order wrt. connection events + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + let mut offset = 0; + loop { + match chunks.next(usize::MAX) { + Ok(Some(chunk)) => { + assert_eq!(chunk.offset as usize, offset); + offset += chunk.bytes.len(); + } + Err(ReadError::Blocked) => break, + Ok(None) => panic!("unexpected stream end"), + Err(e) => panic!("{}", e), + } + } + assert_eq!(offset, CLIENT_WRITES); + let _ = chunks.finalize(); + assert_eq!( + pair.client_conn_mut(client_ch).stats().path.lost_packets, + EXPECTED_DROPPED + ); +} + +#[test] +fn zero_rtt_incoming_buffer_size() { + test_zero_rtt_incoming_limit(|config| { + config.incoming_buffer_size(4000); + }); +} + +#[test] +fn zero_rtt_incoming_buffer_size_total() { + test_zero_rtt_incoming_limit(|config| { + config.incoming_buffer_size_total(4000); + }); +} + +#[test] +fn alpn_success() { + let _guard = subscribe(); + let server_config = ServerConfig::with_crypto(Arc::new(server_crypto_with_alpn(vec![ + "foo".into(), + "bar".into(), + "baz".into(), + ]))); + + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + let client_config = ClientConfig::new(Arc::new(client_crypto_with_alpn(vec![ + "bar".into(), + "quux".into(), + "corge".into(), + ]))); + + // Establish normal connection + let client_ch = pair.begin_connect(client_config); + pair.drive(); + let server_ch = pair.server.assert_accept(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); + + let hd = pair + .client_conn_mut(client_ch) + .crypto_session() + .handshake_data() + .unwrap() + .downcast::() + .unwrap(); + assert_eq!(hd.protocol.unwrap(), &b"bar"[..]); +} + +#[test] +fn server_alpn_unset() { + let _guard = subscribe(); + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config()); + let client_config = ClientConfig::new(Arc::new(client_crypto_with_alpn(vec!["foo".into()]))); + + let client_ch = pair.begin_connect(client_config); + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ConnectionClosed(err) }) if err.error_code == TransportErrorCode::crypto(0x78) + ); +} + +#[test] +fn client_alpn_unset() { + let _guard = subscribe(); + let server_config = ServerConfig::with_crypto(Arc::new(server_crypto_with_alpn(vec![ + "foo".into(), + "bar".into(), + "baz".into(), + ]))); + + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + let client_ch = pair.begin_connect(client_config()); + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ConnectionClosed(err) }) if err.error_code == TransportErrorCode::crypto(0x78) + ); +} + +#[test] +fn alpn_mismatch() { + let _guard = subscribe(); + let server_config = ServerConfig::with_crypto(Arc::new(server_crypto_with_alpn(vec![ + "foo".into(), + "bar".into(), + "baz".into(), + ]))); + + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + let client_ch = pair.begin_connect(ClientConfig::new(Arc::new(client_crypto_with_alpn(vec![ + "quux".into(), + "corge".into(), + ])))); + + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ConnectionClosed(err) }) if err.error_code == TransportErrorCode::crypto(0x78) + ); +} + +#[test] +fn stream_id_limit() { + let _guard = subscribe(); + let server = ServerConfig { + transport: Arc::new(TransportConfig { + max_concurrent_uni_streams: 1u32.into(), + ..TransportConfig::default() + }), + ..server_config() + }; + let mut pair = Pair::new(Default::default(), server); + let (client_ch, server_ch) = pair.connect(); + + let s = pair + .client + .connections + .get_mut(&client_ch) + .unwrap() + .streams() + .open(Dir::Uni) + .expect("couldn't open first stream"); + assert_eq!( + pair.client_streams(client_ch).open(Dir::Uni), + None, + "only one stream is permitted at a time" + ); + // Generate some activity to allow the server to see the stream + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Finished { id })) if id == s + ); + assert_eq!( + pair.client_streams(client_ch).open(Dir::Uni), + None, + "server does not immediately grant additional credit" + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + assert_eq!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); + + // Server will only send MAX_STREAM_ID now that the application's been notified + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Available { dir: Dir::Uni })) + ); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + + // Try opening the second stream again, now that we've made room + let s = pair + .client + .connections + .get_mut(&client_ch) + .unwrap() + .streams() + .open(Dir::Uni) + .expect("didn't get stream id budget"); + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive(); + // Make sure the server actually processes data on the newly-available stream + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); +} + +#[test] +fn key_update_simple() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + let s = pair + .client + .connections + .get_mut(&client_ch) + .unwrap() + .streams() + .open(Dir::Bi) + .expect("couldn't open first stream"); + + const MSG1: &[u8] = b"hello1"; + pair.client_send(client_ch, s).write(MSG1).unwrap(); + pair.drive(); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Bi })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Bi), Some(stream) if stream == s); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG1 + ); + let _ = chunks.finalize(); + + info!("initiating key update"); + pair.client_conn_mut(client_ch).force_key_update(); + + const MSG2: &[u8] = b"hello2"; + pair.client_send(client_ch, s).write(MSG2).unwrap(); + pair.drive(); + + assert_matches!(pair.server_conn_mut(server_ch).poll(), Some(Event::Stream(StreamEvent::Readable { id })) if id == s); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 6 && chunk.bytes == MSG2 + ); + let _ = chunks.finalize(); + + assert_eq!(pair.client_conn_mut(client_ch).stats().path.lost_packets, 0); + assert_eq!(pair.server_conn_mut(server_ch).stats().path.lost_packets, 0); +} + +#[test] +fn key_update_reordered() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + let s = pair + .client + .connections + .get_mut(&client_ch) + .unwrap() + .streams() + .open(Dir::Bi) + .expect("couldn't open first stream"); + + const MSG1: &[u8] = b"1"; + pair.client_send(client_ch, s).write(MSG1).unwrap(); + pair.client.drive(pair.time, pair.server.addr); + assert!(!pair.client.outbound.is_empty()); + pair.client.delay_outbound(); + + pair.client_conn_mut(client_ch).force_key_update(); + info!("updated keys"); + + const MSG2: &[u8] = b"two"; + pair.client_send(client_ch, s).write(MSG2).unwrap(); + pair.client.drive(pair.time, pair.server.addr); + pair.client.finish_delay(); + pair.drive(); + + assert_eq!(pair.client_conn_mut(client_ch).stats().path.lost_packets, 0); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Bi })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Bi), Some(stream) if stream == s); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(true).unwrap(); + let buf1 = chunks.next(usize::MAX).unwrap().unwrap(); + assert_matches!(&*buf1.bytes, MSG1); + let buf2 = chunks.next(usize::MAX).unwrap().unwrap(); + assert_eq!(buf2.bytes, MSG2); + let _ = chunks.finalize(); + + assert_eq!(pair.client_conn_mut(client_ch).stats().path.lost_packets, 0); + assert_eq!(pair.server_conn_mut(server_ch).stats().path.lost_packets, 0); +} + +#[test] +fn initial_retransmit() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let client_ch = pair.begin_connect(client_config()); + pair.client.drive(pair.time, pair.server.addr); + pair.client.outbound.clear(); // Drop initial + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); +} + +#[test] +fn instant_close_1() { + let _guard = subscribe(); + let mut pair = Pair::default(); + info!("connecting"); + let client_ch = pair.begin_connect(client_config()); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(0), Bytes::new()); + pair.drive(); + let server_ch = pair.server.assert_accept(); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::ConnectionClosed(ConnectionClose { + error_code: TransportErrorCode::APPLICATION_ERROR, + .. + }), + }) + ); +} + +#[test] +fn instant_close_2() { + let _guard = subscribe(); + let mut pair = Pair::default(); + info!("connecting"); + let client_ch = pair.begin_connect(client_config()); + // Unlike `instant_close`, the server sees a valid Initial packet first. + pair.drive_client(); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + let server_ch = pair.server.assert_accept(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::ConnectionClosed(ConnectionClose { + error_code: TransportErrorCode::APPLICATION_ERROR, + .. + }), + }) + ); +} + +#[test] +fn instant_server_close() { + let _guard = subscribe(); + let mut pair = Pair::default(); + info!("connecting"); + pair.begin_connect(client_config()); + pair.drive_client(); + pair.server.drive_incoming(pair.time, pair.client.addr); + let server_ch = pair.server.assert_accept(); + info!("closing"); + pair.server + .connections + .get_mut(&server_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_matches!( + pair.client_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::ConnectionClosed(ConnectionClose { + error_code: TransportErrorCode::APPLICATION_ERROR, + .. + }), + }) + ); +} + +#[test] +fn idle_timeout() { + let _guard = subscribe(); + const IDLE_TIMEOUT: u64 = 100; + let server = ServerConfig { + transport: Arc::new(TransportConfig { + max_idle_timeout: Some(VarInt(IDLE_TIMEOUT)), + ..TransportConfig::default() + }), + ..server_config() + }; + let mut pair = Pair::new(Default::default(), server); + let (client_ch, server_ch) = pair.connect(); + pair.client_conn_mut(client_ch).ping(); + let start = pair.time; + + while !pair.client_conn_mut(client_ch).is_closed() + || !pair.server_conn_mut(server_ch).is_closed() + { + if !pair.step() { + if let Some(t) = min_opt(pair.client.next_wakeup(), pair.server.next_wakeup()) { + pair.time = t; + } + } + pair.client.inbound.clear(); // Simulate total S->C packet loss + } + + assert!(pair.time - start < Duration::from_millis(2 * IDLE_TIMEOUT)); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::TimedOut, + }) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::TimedOut, + }) + ); +} + +#[test] +fn connection_close_sends_acks() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _server_ch) = pair.connect(); + + let client_acks = pair.client_conn_mut(client_ch).stats().frame_rx.acks; + + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + let time = pair.time; + pair.server_conn_mut(client_ch) + .close(time, VarInt(42), Bytes::new()); + + pair.drive(); + + let client_acks_2 = pair.client_conn_mut(client_ch).stats().frame_rx.acks; + assert!( + client_acks_2 > client_acks, + "Connection close should send pending ACKs" + ); +} + +#[test] +fn server_hs_retransmit() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let client_ch = pair.begin_connect(client_config()); + pair.step(); + assert!(!pair.client.inbound.is_empty()); // Initial + Handshakes + pair.client.inbound.clear(); + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); +} + +#[test] +fn migration() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + pair.drive(); + + let client_stats_after_connect = pair.client_conn_mut(client_ch).stats(); + + pair.client.addr = SocketAddr::new( + Ipv4Addr::new(127, 0, 0, 1).into(), + CLIENT_PORTS.lock().unwrap().next().unwrap(), + ); + pair.client_conn_mut(client_ch).ping(); + + // Assert that just receiving the ping message is accounted into the servers + // anti-amplification budget + pair.drive_client(); + pair.drive_server(); + assert_ne!(pair.server_conn_mut(server_ch).total_recvd(), 0); + + pair.drive(); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_eq!( + pair.server_conn_mut(server_ch).remote_address(), + pair.client.addr + ); + + // Assert that the client's response to the PATH_CHALLENGE was an IMMEDIATE_ACK, instead of a + // second ping + let client_stats_after_migrate = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + client_stats_after_migrate.frame_tx.ping - client_stats_after_connect.frame_tx.ping, + 1 + ); + assert_eq!( + client_stats_after_migrate.frame_tx.immediate_ack + - client_stats_after_connect.frame_tx.immediate_ack, + 1 + ); +} + +fn test_flow_control(config: TransportConfig, window_size: usize) { + let _guard = subscribe(); + let mut pair = Pair::new( + Default::default(), + ServerConfig { + transport: Arc::new(config), + ..server_config() + }, + ); + let (client_ch, server_ch) = pair.connect(); + let msg = vec![0xAB; window_size + 10]; + + // Stream reset before read + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + info!("writing"); + assert_eq!(pair.client_send(client_ch, s).write(&msg), Ok(window_size)); + assert_eq!( + pair.client_send(client_ch, s).write(&msg[window_size..]), + Err(WriteError::Blocked) + ); + pair.drive(); + info!("resetting"); + pair.client_send(client_ch, s).reset(VarInt(42)).unwrap(); + pair.drive(); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(true).unwrap(); + assert_eq!( + chunks.next(usize::MAX).err(), + Some(ReadError::Reset(VarInt(42))) + ); + let _ = chunks.finalize(); + + // Happy path + info!("writing"); + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + assert_eq!(pair.client_send(client_ch, s).write(&msg), Ok(window_size)); + assert_eq!( + pair.client_send(client_ch, s).write(&msg[window_size..]), + Err(WriteError::Blocked) + ); + + pair.drive(); + let mut cursor = 0; + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(true).unwrap(); + loop { + match chunks.next(usize::MAX) { + Ok(Some(chunk)) => { + cursor += chunk.bytes.len(); + } + Ok(None) => { + panic!("end of stream"); + } + Err(ReadError::Blocked) => { + break; + } + Err(e) => { + panic!("{}", e); + } + } + } + let _ = chunks.finalize(); + + info!("finished reading"); + assert_eq!(cursor, window_size); + pair.drive(); + info!("writing"); + assert_eq!(pair.client_send(client_ch, s).write(&msg), Ok(window_size)); + assert_eq!( + pair.client_send(client_ch, s).write(&msg[window_size..]), + Err(WriteError::Blocked) + ); + + pair.drive(); + let mut cursor = 0; + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(true).unwrap(); + loop { + match chunks.next(usize::MAX) { + Ok(Some(chunk)) => { + cursor += chunk.bytes.len(); + } + Ok(None) => { + panic!("end of stream"); + } + Err(ReadError::Blocked) => { + break; + } + Err(e) => { + panic!("{}", e); + } + } + } + assert_eq!(cursor, window_size); + let _ = chunks.finalize(); + info!("finished reading"); +} + +#[test] +fn stream_flow_control() { + test_flow_control( + TransportConfig { + stream_receive_window: 2000u32.into(), + ..TransportConfig::default() + }, + 2000, + ); +} + +#[test] +fn conn_flow_control() { + test_flow_control( + TransportConfig { + receive_window: 2000u32.into(), + ..TransportConfig::default() + }, + 2000, + ); +} + +#[test] +fn stop_opens_bidi() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + assert_eq!(pair.client_streams(client_ch).send_streams(), 0); + let s = pair.client_streams(client_ch).open(Dir::Bi).unwrap(); + assert_eq!(pair.client_streams(client_ch).send_streams(), 1); + const ERROR: VarInt = VarInt(42); + pair.client + .connections + .get_mut(&server_ch) + .unwrap() + .recv_stream(s) + .stop(ERROR) + .unwrap(); + pair.drive(); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Bi })) + ); + assert_eq!(pair.server_conn_mut(client_ch).streams().send_streams(), 0); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Bi), Some(stream) if stream == s); + assert_eq!(pair.server_conn_mut(client_ch).streams().send_streams(), 1); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!(chunks.next(usize::MAX), Err(ReadError::Blocked)); + let _ = chunks.finalize(); + + assert_matches!( + pair.server_send(server_ch, s).write(b"foo"), + Err(WriteError::Stopped(ERROR)) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Stopped { + id: _, + error_code: ERROR + })) + ); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); +} + +#[test] +fn implicit_open() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + let s1 = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + let s2 = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + pair.client_send(client_ch, s2).write(b"hello").unwrap(); + pair.drive(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_eq!(pair.server_streams(server_ch).accept(Dir::Uni), Some(s1)); + assert_eq!(pair.server_streams(server_ch).accept(Dir::Uni), Some(s2)); + assert_eq!(pair.server_streams(server_ch).accept(Dir::Uni), None); +} + +#[test] +fn zero_length_cid() { + let _guard = subscribe(); + let cid_generator_factory: fn() -> Box = + || Box::new(RandomConnectionIdGenerator::new(0)); + let mut pair = Pair::new( + Arc::new(EndpointConfig { + connection_id_generator_factory: Arc::new(cid_generator_factory), + ..EndpointConfig::default() + }), + server_config(), + ); + let (client_ch, server_ch) = pair.connect(); + // Ensure we can reconnect after a previous connection is cleaned up + info!("closing"); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + pair.server + .connections + .get_mut(&server_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.connect(); +} + +#[test] +fn keep_alive() { + let _guard = subscribe(); + const IDLE_TIMEOUT: u64 = 10; + let server = ServerConfig { + transport: Arc::new(TransportConfig { + keep_alive_interval: Some(Duration::from_millis(IDLE_TIMEOUT / 2)), + max_idle_timeout: Some(VarInt(IDLE_TIMEOUT)), + ..TransportConfig::default() + }), + ..server_config() + }; + let mut pair = Pair::new(Default::default(), server); + let (client_ch, server_ch) = pair.connect(); + // Run a good while longer than the idle timeout + let end = pair.time + Duration::from_millis(20 * IDLE_TIMEOUT); + while pair.time < end { + if !pair.step() { + if let Some(time) = min_opt(pair.client.next_wakeup(), pair.server.next_wakeup()) { + pair.time = time; + } + } + assert!(!pair.client_conn_mut(client_ch).is_closed()); + assert!(!pair.server_conn_mut(server_ch).is_closed()); + } +} + +#[test] +fn cid_rotation() { + let _guard = subscribe(); + const CID_TIMEOUT: Duration = Duration::from_secs(2); + + let cid_generator_factory: fn() -> Box = + || Box::new(*RandomConnectionIdGenerator::new(8).set_lifetime(CID_TIMEOUT)); + + // Only test cid rotation on server side to have a clear output trace + let server = Endpoint::new( + Arc::new(EndpointConfig { + connection_id_generator_factory: Arc::new(cid_generator_factory), + ..EndpointConfig::default() + }), + Some(Arc::new(server_config())), + true, + None, + ); + let client = Endpoint::new(Arc::new(EndpointConfig::default()), None, true, None); + + let mut pair = Pair::new_from_endpoint(client, server); + let (_, server_ch) = pair.connect(); + + let mut round: u64 = 1; + let mut stop = pair.time; + let end = pair.time + 5 * CID_TIMEOUT; + + use crate::LOC_CID_COUNT; + use crate::cid_queue::CidQueue; + let mut active_cid_num = CidQueue::LEN as u64 + 1; + active_cid_num = active_cid_num.min(LOC_CID_COUNT); + let mut left_bound = 0; + let mut right_bound = active_cid_num - 1; + + while pair.time < end { + stop += CID_TIMEOUT; + // Run a while until PushNewCID timer fires + while pair.time < stop { + if !pair.step() { + if let Some(time) = min_opt(pair.client.next_wakeup(), pair.server.next_wakeup()) { + pair.time = time; + } + } + } + info!( + "Checking active cid sequence range before {:?} seconds", + round * CID_TIMEOUT.as_secs() + ); + let _bound = (left_bound, right_bound); + assert_matches!( + pair.server_conn_mut(server_ch).active_local_cid_seq(), + _bound + ); + round += 1; + left_bound += active_cid_num; + right_bound += active_cid_num; + pair.drive_server(); + } +} + +#[test] +fn cid_retirement() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + // Server retires current active remote CIDs + pair.server_conn_mut(server_ch) + .rotate_local_cid(1, Instant::now()); + pair.drive(); + // Any unexpected behavior may trigger TransportError::CONNECTION_ID_LIMIT_ERROR + assert!(!pair.client_conn_mut(client_ch).is_closed()); + assert!(!pair.server_conn_mut(server_ch).is_closed()); + assert_matches!(pair.client_conn_mut(client_ch).active_rem_cid_seq(), 1); + + use crate::LOC_CID_COUNT; + use crate::cid_queue::CidQueue; + let mut active_cid_num = CidQueue::LEN as u64; + active_cid_num = active_cid_num.min(LOC_CID_COUNT); + + let next_retire_prior_to = active_cid_num + 1; + pair.client_conn_mut(client_ch).ping(); + // Server retires all valid remote CIDs + pair.server_conn_mut(server_ch) + .rotate_local_cid(next_retire_prior_to, Instant::now()); + pair.drive(); + assert!(!pair.client_conn_mut(client_ch).is_closed()); + assert!(!pair.server_conn_mut(server_ch).is_closed()); + + assert_eq!( + pair.client_conn_mut(client_ch).active_rem_cid_seq(), + next_retire_prior_to, + ); +} + +#[test] +fn finish_stream_flow_control_reordered() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive_client(); // Send stream data + pair.server.drive(pair.time, pair.client.addr); // Receive + + // Issue flow control credit + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + let _ = chunks.finalize(); + + pair.server.drive(pair.time, pair.client.addr); + pair.server.delay_outbound(); // Delay it + + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive_client(); // Send FIN + pair.server.drive(pair.time, pair.client.addr); // Acknowledge + pair.server.finish_delay(); // Add flow control packets after + pair.drive(); + + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Finished { id })) if id == s + ); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); +} + +#[test] +fn handshake_1rtt_handling() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let client_ch = pair.begin_connect(client_config()); + pair.drive_client(); + pair.drive_server(); + let server_ch = pair.server.assert_accept(); + // Server now has 1-RTT keys, but remains in Handshake state until the TLS CFIN has + // authenticated the client. Delay the final client handshake flight so that doesn't happen yet. + pair.client.drive(pair.time, pair.server.addr); + pair.client.delay_outbound(); + + // Send some 1-RTT data which will be received first. + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.client_send(client_ch, s).finish().unwrap(); + pair.client.drive(pair.time, pair.server.addr); + + // Add the handshake flight back on. + pair.client.finish_delay(); + + pair.drive(); + + assert!(pair.client_conn_mut(client_ch).stats().path.lost_packets != 0); + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + let _ = chunks.finalize(); +} + +#[test] +fn stop_before_finish() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive(); + + info!("stopping stream"); + const ERROR: VarInt = VarInt(42); + pair.server_recv(server_ch, s).stop(ERROR).unwrap(); + pair.drive(); + + assert_matches!( + pair.client_send(client_ch, s).finish(), + Err(FinishError::Stopped(ERROR)) + ); +} + +#[test] +fn stop_during_finish() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive(); + + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + info!("stopping and finishing stream"); + const ERROR: VarInt = VarInt(42); + pair.server_recv(server_ch, s).stop(ERROR).unwrap(); + pair.drive_server(); + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive_client(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Stopped { id, error_code: ERROR })) if id == s + ); +} + +// Ensure we can recover from loss of tail packets when the congestion window is full +#[test] +fn congested_tail_loss() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + + const TARGET: u64 = 2048; + assert!(pair.client_conn_mut(client_ch).congestion_window() > TARGET); + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + // Send data without receiving ACKs until the congestion state falls below target + while pair.client_conn_mut(client_ch).congestion_window() > TARGET { + let n = pair.client_send(client_ch, s).write(&[42; 1024]).unwrap(); + assert_eq!(n, 1024); + pair.drive_client(); + } + assert!(!pair.server.inbound.is_empty()); + pair.server.inbound.clear(); + // Ensure that the congestion state recovers after retransmits occur and are ACKed + info!("recovering"); + pair.drive(); + assert!(pair.client_conn_mut(client_ch).congestion_window() > TARGET); + pair.client_send(client_ch, s).write(&[42; 1024]).unwrap(); +} + +// Send a tail-loss probe when GSO segment_size is less than INITIAL_MTU +#[test] +fn tail_loss_small_segment_size() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + // No datagrams frames received in the handshake. + let server_stats = pair.server_conn_mut(server_ch).stats(); + assert_eq!(server_stats.frame_rx.datagram, 0); + + const DGRAM_LEN: usize = 1000; // Below INITIAL_MTU after packet overhead. + const DGRAM_NUM: u64 = 5; // Enough to build a GSO batch. + + info!("Sending an ack-eliciting datagram"); + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Drop these packets on the server side. + assert!(!pair.server.inbound.is_empty()); + pair.server.inbound.clear(); + + // Doing one step makes the client advance time to the PTO fire time. + info!("stepping forward to PTO"); + pair.step(); + + // Still no datagrams frames received by the server. + let server_stats = pair.server_conn_mut(server_ch).stats(); + assert_eq!(server_stats.frame_rx.datagram, 0); + + // Now we can send another batch of datagrams, so the PTO can send them instead of + // sending a ping. These are small enough that the segment_size is less than the + // INITIAL_MTU. + info!("Sending datagram batch"); + for _ in 0..DGRAM_NUM { + pair.client_datagrams(client_ch) + .send(vec![0; DGRAM_LEN].into(), false) + .unwrap(); + } + + // If this succeeds the datagrams are received by the server and the client did not + // crash. + pair.drive(); + + // Finally the server should have received some datagrams. + let server_stats = pair.server_conn_mut(server_ch).stats(); + assert_eq!(server_stats.frame_rx.datagram, DGRAM_NUM); +} + +// Respect max_datagrams when TLP happens +#[test] +fn tail_loss_respect_max_datagrams() { + let _guard = subscribe(); + let client_config = { + let mut c_config = client_config(); + let mut t_config = TransportConfig::default(); + //Disabling GSO, so only a single segment should be sent per iops + t_config.enable_segmentation_offload(false); + c_config.transport_config(t_config.into()); + c_config + }; + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect_with(client_config); + + const DGRAM_LEN: usize = 1000; // High enough so GSO batch could be built + const DGRAM_NUM: u64 = 5; // Enough to build a GSO batch. + + info!("Sending an ack-eliciting datagram"); + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Drop these packets on the server side. + assert!(!pair.server.inbound.is_empty()); + pair.server.inbound.clear(); + + // Doing one step makes the client advance time to the PTO fire time. + info!("stepping forward to PTO"); + pair.step(); + + // start sending datagram batches but the first should be a TLP + info!("Sending datagram batch"); + for _ in 0..DGRAM_NUM { + pair.client_datagrams(client_ch) + .send(vec![0; DGRAM_LEN].into(), false) + .unwrap(); + } + + pair.drive(); + + // Finally checking the number of sent udp datagrams match the number of iops + let client_stats = pair.client_conn_mut(client_ch).stats(); + assert_eq!(client_stats.udp_tx.ios, client_stats.udp_tx.datagrams); +} + +#[test] +fn datagram_send_recv() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + assert_matches!(pair.client_datagrams(client_ch).max_size(), Some(x) if x > 0); + + const DATA: &[u8] = b"whee"; + pair.client_datagrams(client_ch) + .send(DATA.into(), true) + .unwrap(); + pair.drive(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::DatagramReceived) + ); + assert_eq!(pair.server_datagrams(server_ch).recv().unwrap(), DATA); + assert_matches!(pair.server_datagrams(server_ch).recv(), None); +} + +#[test] +fn datagram_recv_buffer_overflow() { + let _guard = subscribe(); + const WINDOW: usize = 100; + let server = ServerConfig { + transport: Arc::new(TransportConfig { + datagram_receive_buffer_size: Some(WINDOW), + ..TransportConfig::default() + }), + ..server_config() + }; + let mut pair = Pair::new(Default::default(), server); + let (client_ch, server_ch) = pair.connect(); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + assert_eq!( + pair.client_conn_mut(client_ch).datagrams().max_size(), + Some(WINDOW - Datagram::SIZE_BOUND) + ); + + const DATA1: &[u8] = &[0xAB; (WINDOW / 3) + 1]; + const DATA2: &[u8] = &[0xBC; (WINDOW / 3) + 1]; + const DATA3: &[u8] = &[0xCD; (WINDOW / 3) + 1]; + pair.client_datagrams(client_ch) + .send(DATA1.into(), true) + .unwrap(); + pair.client_datagrams(client_ch) + .send(DATA2.into(), true) + .unwrap(); + pair.client_datagrams(client_ch) + .send(DATA3.into(), true) + .unwrap(); + pair.drive(); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::DatagramReceived) + ); + assert_eq!(pair.server_datagrams(server_ch).recv().unwrap(), DATA2); + assert_eq!(pair.server_datagrams(server_ch).recv().unwrap(), DATA3); + assert_matches!(pair.server_datagrams(server_ch).recv(), None); + + pair.client_datagrams(client_ch) + .send(DATA1.into(), true) + .unwrap(); + pair.drive(); + assert_eq!(pair.server_datagrams(server_ch).recv().unwrap(), DATA1); + assert_matches!(pair.server_datagrams(server_ch).recv(), None); +} + +#[test] +fn datagram_unsupported() { + let _guard = subscribe(); + let server = ServerConfig { + transport: Arc::new(TransportConfig { + datagram_receive_buffer_size: None, + ..TransportConfig::default() + }), + ..server_config() + }; + let mut pair = Pair::new(Default::default(), server); + let (client_ch, server_ch) = pair.connect(); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + assert_matches!(pair.client_datagrams(client_ch).max_size(), None); + + match pair.client_datagrams(client_ch).send(Bytes::new(), true) { + Err(SendDatagramError::UnsupportedByPeer) => {} + Err(e) => panic!("unexpected error: {e}"), + Ok(_) => panic!("unexpected success"), + } +} + +#[test] +fn large_initial() { + let _guard = subscribe(); + let server_config = + ServerConfig::with_crypto(Arc::new(server_crypto_with_alpn(vec![vec![0, 0, 0, 42]]))); + + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + let client_crypto = + client_crypto_with_alpn((0..1000u32).map(|x| x.to_be_bytes().to_vec()).collect()); + let cfg = ClientConfig::new(Arc::new(client_crypto)); + let client_ch = pair.begin_connect(cfg); + pair.drive(); + let server_ch = pair.server.assert_accept(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); +} + +#[test] +/// Ensure that we don't yield a finish event before the actual FIN is acked so the peer isn't left +/// hanging +fn finish_acked() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + info!("client sends data to server"); + pair.drive_client(); // send data to server + info!("server acknowledges data"); + pair.drive_server(); // process data and send data ack + + // Receive data + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + assert_matches!(pair.server_conn_mut(server_ch).poll(), None); + + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + assert_matches!(chunks.next(usize::MAX), Err(ReadError::Blocked)); + let _ = chunks.finalize(); + + // Finish before receiving data ack + pair.client_send(client_ch, s).finish().unwrap(); + // Send FIN, receive data ack + info!("client receives ACK, sends FIN"); + pair.drive_client(); + // Check for premature finish from data ack + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + // Process FIN ack + info!("server ACKs FIN"); + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Finished { id })) if id == s + ); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); +} + +#[test] +/// Ensure that we don't yield a finish event while there's still unacknowledged data +fn finish_retransmit() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + const MSG: &[u8] = b"hello"; + pair.client_send(client_ch, s).write(MSG).unwrap(); + pair.drive_client(); // send data to server + pair.server.inbound.clear(); // Lose it + + // Send FIN + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive_client(); + // Process FIN + pair.drive_server(); + // Receive FIN ack, but no data ack + pair.drive_client(); + // Check for premature finish from FIN ack + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + // Recover + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Stream(StreamEvent::Finished { id })) if id == s + ); + + assert_matches!( + pair.server_conn_mut(server_ch).poll(), + Some(Event::Stream(StreamEvent::Opened { dir: Dir::Uni })) + ); + + assert_matches!(pair.server_streams(server_ch).accept(Dir::Uni), Some(stream) if stream == s); + + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == MSG + ); + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); +} + +/// Ensures that exchanging data on a client-initiated bidirectional stream works past the initial +/// stream window. +#[test] +fn repeated_request_response() { + let _guard = subscribe(); + let server = ServerConfig { + transport: Arc::new(TransportConfig { + max_concurrent_bidi_streams: 1u32.into(), + ..TransportConfig::default() + }), + ..server_config() + }; + let mut pair = Pair::new(Default::default(), server); + let (client_ch, server_ch) = pair.connect(); + const REQUEST: &[u8] = b"hello"; + const RESPONSE: &[u8] = b"world"; + for _ in 0..3 { + let s = pair.client_streams(client_ch).open(Dir::Bi).unwrap(); + + pair.client_send(client_ch, s).write(REQUEST).unwrap(); + pair.client_send(client_ch, s).finish().unwrap(); + + pair.drive(); + + assert_eq!(pair.server_streams(server_ch).accept(Dir::Bi), Some(s)); + let mut recv = pair.server_recv(server_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == REQUEST + ); + + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); + pair.server_send(server_ch, s).write(RESPONSE).unwrap(); + pair.server_send(server_ch, s).finish().unwrap(); + + pair.drive(); + + let mut recv = pair.client_recv(client_ch, s); + let mut chunks = recv.read(false).unwrap(); + assert_matches!( + chunks.next(usize::MAX), + Ok(Some(chunk)) if chunk.offset == 0 && chunk.bytes == RESPONSE + ); + assert_matches!(chunks.next(usize::MAX), Ok(None)); + let _ = chunks.finalize(); + } +} + +/// Ensures that the client sends an anti-deadlock probe after an incomplete server's first flight +#[test] +fn handshake_anti_deadlock_probe() { + let _guard = subscribe(); + + let (cert, key) = big_cert_and_key(); + let server = server_config_with_cert(cert.clone(), key); + let client = client_config_with_certs(vec![cert]); + let mut pair = Pair::new(Default::default(), server); + + let client_ch = pair.begin_connect(client); + // Client sends initial + pair.drive_client(); + // Server sends first flight, gets blocked on anti-amplification + pair.drive_server(); + // Client acks... + pair.drive_client(); + // ...but it's lost, so the server doesn't get anti-amplification credit from it + pair.server.inbound.clear(); + // Client sends an anti-deadlock probe, and the handshake completes as usual. + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); +} + +/// Ensures that the server can respond with 3 initial packets during the handshake +/// before the anti-amplification limit kicks in when MTUs are similar. +#[test] +fn server_can_send_3_inital_packets() { + let _guard = subscribe(); + + let (cert, key) = big_cert_and_key(); + let server = server_config_with_cert(cert.clone(), key); + let client = client_config_with_certs(vec![cert]); + let mut pair = Pair::new(Default::default(), server); + + let client_ch = pair.begin_connect(client); + // Client sends initial + pair.drive_client(); + // Server sends first flight, gets blocked on anti-amplification + pair.drive_server(); + // Server should have queued 3 packets at this time + assert_eq!(pair.client.inbound.len(), 3); + + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); +} + +/// Generate a big fat certificate that can't fit inside the initial anti-amplification limit +fn big_cert_and_key() -> (CertificateDer<'static>, PrivateKeyDer<'static>) { + let cert = rcgen::generate_simple_self_signed( + Some("localhost".into()) + .into_iter() + .chain((0..1000).map(|x| format!("foo_{x}"))) + .collect::>(), + ) + .unwrap(); + + ( + cert.cert.into(), + PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into()), + ) +} + +#[test] +fn malformed_token_len() { + let _guard = subscribe(); + let client_addr = "[::2]:7890".parse().unwrap(); + let mut server = Endpoint::new( + Default::default(), + Some(Arc::new(server_config())), + true, + None, + ); + let mut buf = Vec::with_capacity(server.config().get_max_udp_payload_size() as usize); + server.handle( + Instant::now(), + client_addr, + None, + None, + hex!("8900 0000 0101 0000 1b1b 841b 0000 0000 3f00")[..].into(), + &mut buf, + ); +} + +#[test] +fn loss_probe_requests_immediate_ack() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + pair.drive(); + + let stats_after_connect = pair.client_conn_mut(client_ch).stats(); + + // Lose a ping + let default_mtu = mem::replace(&mut pair.mtu, 0); + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + pair.mtu = default_mtu; + + // Drive the connection further so a loss probe is sent + pair.drive(); + + // Assert that two IMMEDIATE_ACKs were sent (two loss probes) + let stats_after_recovery = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + stats_after_recovery.frame_tx.immediate_ack - stats_after_connect.frame_tx.immediate_ack, + 2 + ); +} + +#[test] +/// This is mostly a sanity check to ensure our testing code is correctly dropping packets above the +/// pmtu +fn connect_too_low_mtu() { + let _guard = subscribe(); + let mut pair = Pair::default(); + + // The maximum payload size is lower than 1200, so no packages will get through! + pair.mtu = 1000; + + pair.begin_connect(client_config()); + pair.drive(); + pair.server.assert_no_accept(); +} + +#[test] +fn connect_lost_mtu_probes_do_not_trigger_congestion_control() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.mtu = 1200; + + let (client_ch, server_ch) = pair.connect(); + pair.drive(); + + let client_stats = pair.client_conn_mut(client_ch).stats(); + let server_stats = pair.server_conn_mut(server_ch).stats(); + + // Sanity check (all MTU probes should have been lost) + assert_eq!(client_stats.path.sent_plpmtud_probes, 9); + assert_eq!(client_stats.path.lost_plpmtud_probes, 9); + assert_eq!(server_stats.path.sent_plpmtud_probes, 9); + assert_eq!(server_stats.path.lost_plpmtud_probes, 9); + + // No congestion events + assert_eq!(client_stats.path.congestion_events, 0); + assert_eq!(server_stats.path.congestion_events, 0); +} + +#[test] +fn connect_detects_mtu() { + let _guard = subscribe(); + let max_udp_payload_and_expected_mtu = &[(1200, 1200), (1400, 1389), (1500, 1452)]; + + for &(pair_max_udp, expected_mtu) in max_udp_payload_and_expected_mtu { + let mut pair = Pair::default(); + pair.mtu = pair_max_udp; + let (client_ch, server_ch) = pair.connect(); + pair.drive(); + + assert_eq!(pair.client_conn_mut(client_ch).path_mtu(), expected_mtu); + assert_eq!(pair.server_conn_mut(server_ch).path_mtu(), expected_mtu); + } +} + +#[test] +fn migrate_detects_new_mtu_and_respects_original_peer_max_udp_payload_size() { + let _guard = subscribe(); + + let client_max_udp_payload_size: u16 = 1400; + + // Set up a client with a max payload size of 1400 (and use the defaults for the server) + let server_endpoint_config = EndpointConfig::default(); + let server = Endpoint::new( + Arc::new(server_endpoint_config), + Some(Arc::new(server_config())), + true, + None, + ); + let client_endpoint_config = EndpointConfig { + max_udp_payload_size: VarInt::from(client_max_udp_payload_size), + ..EndpointConfig::default() + }; + let client = Endpoint::new(Arc::new(client_endpoint_config), None, true, None); + let mut pair = Pair::new_from_endpoint(client, server); + pair.mtu = 1300; + + // Connect + let (client_ch, server_ch) = pair.connect(); + pair.drive(); + + // Sanity check: MTUD ran to completion (the numbers differ because binary search stops when + // changes are smaller than 20, otherwise both endpoints would converge at the same MTU of 1300) + assert_eq!(pair.client_conn_mut(client_ch).path_mtu(), 1293); + assert_eq!(pair.server_conn_mut(server_ch).path_mtu(), 1300); + + // Migrate client to a different port (and simulate a higher path MTU) + pair.mtu = 1500; + pair.client.addr = SocketAddr::new( + Ipv4Addr::new(127, 0, 0, 1).into(), + CLIENT_PORTS.lock().unwrap().next().unwrap(), + ); + pair.client_conn_mut(client_ch).ping(); + pair.drive(); + + // Sanity check: the server saw that the client address was updated + assert_eq!( + pair.server_conn_mut(server_ch).remote_address(), + pair.client.addr + ); + + // MTU detection has successfully run after migrating + assert_eq!( + pair.server_conn_mut(server_ch).path_mtu(), + client_max_udp_payload_size + ); + + // Sanity check: the client keeps the old MTU, because migration is triggered by incoming + // packets from a different address + assert_eq!(pair.client_conn_mut(client_ch).path_mtu(), 1293); +} + +#[test] +fn connect_runs_mtud_again_after_600_seconds() { + let _guard = subscribe(); + let mut server_config = server_config(); + let mut client_config = client_config(); + + // Note: we use an infinite idle timeout to ensure we can wait 600 seconds without the + // connection closing + Arc::get_mut(&mut server_config.transport) + .unwrap() + .max_idle_timeout(None); + Arc::get_mut(&mut client_config.transport) + .unwrap() + .max_idle_timeout(None); + + let mut pair = Pair::new(Default::default(), server_config); + pair.mtu = 1400; + let (client_ch, server_ch) = pair.connect_with(client_config); + pair.drive(); + + // Sanity check: the mtu has been discovered + let client_conn = pair.client_conn_mut(client_ch); + assert_eq!(client_conn.path_mtu(), 1389); + assert_eq!(client_conn.stats().path.sent_plpmtud_probes, 5); + assert_eq!(client_conn.stats().path.lost_plpmtud_probes, 3); + let server_conn = pair.server_conn_mut(server_ch); + assert_eq!(server_conn.path_mtu(), 1389); + assert_eq!(server_conn.stats().path.sent_plpmtud_probes, 5); + assert_eq!(server_conn.stats().path.lost_plpmtud_probes, 3); + + // Sanity check: the mtu does not change after the fact, even though the link now supports a + // higher udp payload size + pair.mtu = 1500; + pair.drive(); + assert_eq!(pair.client_conn_mut(client_ch).path_mtu(), 1389); + assert_eq!(pair.server_conn_mut(server_ch).path_mtu(), 1389); + + // The MTU changes after 600 seconds, because now MTUD runs for the second time + pair.time += Duration::from_secs(600); + pair.drive(); + assert!(!pair.client_conn_mut(client_ch).is_closed()); + assert!(!pair.server_conn_mut(client_ch).is_closed()); + assert_eq!(pair.client_conn_mut(client_ch).path_mtu(), 1452); + assert_eq!(pair.server_conn_mut(server_ch).path_mtu(), 1452); +} + +#[test] +fn blackhole_after_mtu_change_repairs_itself() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.mtu = 1500; + let (client_ch, server_ch) = pair.connect(); + pair.drive(); + + // Sanity check + assert_eq!(pair.client_conn_mut(client_ch).path_mtu(), 1452); + assert_eq!(pair.server_conn_mut(server_ch).path_mtu(), 1452); + + // Back to the base MTU + pair.mtu = 1200; + + // The payload will be sent in a single packet, because the detected MTU was 1444, but it will + // be dropped because the link no longer supports that packet size! + let payload = vec![42; 1300]; + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + pair.client_send(client_ch, s).write(&payload).unwrap(); + let out_of_bounds = pair.drive_bounded(); + + if out_of_bounds { + panic!("Connections never reached an idle state"); + } + + let recv = pair.server_recv(server_ch, s); + let buf = stream_chunks(recv); + + // The whole packet arrived in the end + assert_eq!(buf.len(), 1300); + + // Sanity checks (black hole detected after 3 lost packets) + let client_stats = pair.client_conn_mut(client_ch).stats(); + assert!(client_stats.path.lost_packets >= 3); + assert!(client_stats.path.congestion_events >= 3); + assert_eq!(client_stats.path.black_holes_detected, 1); +} + +#[test] +fn mtud_probes_include_immediate_ack() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + pair.drive(); + + let stats = pair.client_conn_mut(client_ch).stats(); + assert_eq!(stats.path.sent_plpmtud_probes, 4); + + // Each probe contains a ping and an immediate ack + assert_eq!(stats.frame_tx.ping, 4); + assert_eq!(stats.frame_tx.immediate_ack, 4); +} + +#[test] +fn packet_splitting_with_default_mtu() { + let _guard = subscribe(); + + // The payload needs to be split in 2 in order to be sent, because it is higher than the max MTU + let payload = vec![42; 1300]; + + let mut pair = Pair::default(); + pair.mtu = 1200; + let (client_ch, _) = pair.connect(); + pair.drive(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + pair.client_send(client_ch, s).write(&payload).unwrap(); + pair.client.drive(pair.time, pair.server.addr); + assert_eq!(pair.client.outbound.len(), 2); + + pair.drive_client(); + assert_eq!(pair.server.inbound.len(), 2); +} + +#[test] +fn packet_splitting_not_necessary_after_higher_mtu_discovered() { + let _guard = subscribe(); + let payload = vec![42; 1300]; + + let mut pair = Pair::default(); + pair.mtu = 1500; + + let (client_ch, _) = pair.connect(); + pair.drive(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + pair.client_send(client_ch, s).write(&payload).unwrap(); + pair.client.drive(pair.time, pair.server.addr); + assert_eq!(pair.client.outbound.len(), 1); + + pair.drive_client(); + assert_eq!(pair.server.inbound.len(), 1); +} + +#[test] +fn single_ack_eliciting_packet_triggers_ack_after_delay() { + let _guard = subscribe(); + let mut pair = Pair::default_with_deterministic_pns(); + let (client_ch, _) = pair.connect_with(client_config_with_deterministic_pns()); + pair.drive(); + + let stats_after_connect = pair.client_conn_mut(client_ch).stats(); + + let start = pair.time; + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); // Send ping + pair.drive_server(); // Process ping + pair.drive_client(); // Give the client a chance to process an ack, so our assertion can fail + + // Sanity check: the time hasn't advanced in the meantime) + assert_eq!(pair.time, start); + + let stats_after_ping = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + stats_after_ping.frame_tx.ping - stats_after_connect.frame_tx.ping, + 1 + ); + assert_eq!( + stats_after_ping.frame_rx.acks - stats_after_connect.frame_rx.acks, + 0 + ); + + pair.client.capture_inbound_packets = true; + pair.drive(); + let stats_after_drive = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + stats_after_drive.frame_rx.acks - stats_after_ping.frame_rx.acks, + 1 + ); + + // The time is start + max_ack_delay + let default_max_ack_delay_ms = TransportParameters::default().max_ack_delay.into_inner(); + assert_eq!( + pair.time, + start + Duration::from_millis(default_max_ack_delay_ms) + ); + + // The ACK delay is properly calculated + assert_eq!(pair.client.captured_packets.len(), 1); + let mut frames = frame::Iter::new(pair.client.captured_packets.remove(0).into()) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(frames.len(), 1); + if let Frame::Ack(ack) = frames.remove(0) { + let ack_delay_exp = TransportParameters::default().ack_delay_exponent; + let delay = ack.delay << ack_delay_exp.into_inner(); + assert_eq!(delay, default_max_ack_delay_ms * 1_000); + } else { + panic!("Expected ACK frame"); + } + + // Sanity check: no loss probe was sent, because the delayed ACK was received on time + assert_eq!( + stats_after_drive.frame_tx.ping - stats_after_connect.frame_tx.ping, + 1 + ); +} + +#[test] +fn immediate_ack_triggers_ack() { + let _guard = subscribe(); + let mut pair = Pair::default_with_deterministic_pns(); + let (client_ch, _) = pair.connect_with(client_config_with_deterministic_pns()); + pair.drive(); + + let acks_after_connect = pair.client_conn_mut(client_ch).stats().frame_rx.acks; + + pair.client_conn_mut(client_ch).immediate_ack(); + pair.drive_client(); // Send immediate ack + pair.drive_server(); // Process immediate ack + pair.drive_client(); // Give the client a chance to process the ack + + let acks_after_ping = pair.client_conn_mut(client_ch).stats().frame_rx.acks; + + assert_eq!(acks_after_ping - acks_after_connect, 1); +} + +#[test] +fn out_of_order_ack_eliciting_packet_triggers_ack() { + let _guard = subscribe(); + let mut pair = Pair::default_with_deterministic_pns(); + let (client_ch, server_ch) = pair.connect_with(client_config_with_deterministic_pns()); + pair.drive(); + + let default_mtu = pair.mtu; + + let client_stats_after_connect = pair.client_conn_mut(client_ch).stats(); + let server_stats_after_connect = pair.server_conn_mut(server_ch).stats(); + + // Send a packet that won't arrive right away (it will be dropped and be re-sent later) + pair.mtu = 0; + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Sanity check (ping sent, no ACK received) + let client_stats_after_first_ping = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + client_stats_after_first_ping.frame_tx.ping - client_stats_after_connect.frame_tx.ping, + 1 + ); + assert_eq!( + client_stats_after_first_ping.frame_rx.acks - client_stats_after_connect.frame_rx.acks, + 0 + ); + + // Restore the default MTU and send another ping, which will arrive earlier than the dropped one + pair.mtu = default_mtu; + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + pair.drive_server(); + pair.drive_client(); + + // Client sanity check (ping sent, one ACK received) + let client_stats_after_second_ping = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + client_stats_after_second_ping.frame_tx.ping - client_stats_after_connect.frame_tx.ping, + 2 + ); + assert_eq!( + client_stats_after_second_ping.frame_rx.acks - client_stats_after_connect.frame_rx.acks, + 1 + ); + + // Server checks (single ping received, ACK sent) + let server_stats_after_second_ping = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after_second_ping.frame_rx.ping - server_stats_after_connect.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after_second_ping.frame_tx.acks - server_stats_after_connect.frame_tx.acks, + 1 + ); +} + +#[test] +fn single_ack_eliciting_packet_with_ce_bit_triggers_immediate_ack() { + let _guard = subscribe(); + let mut pair = Pair::default_with_deterministic_pns(); + let (client_ch, _) = pair.connect_with(client_config_with_deterministic_pns()); + pair.drive(); + + let stats_after_connect = pair.client_conn_mut(client_ch).stats(); + + let start = pair.time; + + pair.client_conn_mut(client_ch).ping(); + + pair.congestion_experienced = true; + pair.drive_client(); // Send ping + pair.congestion_experienced = false; + + pair.drive_server(); // Process ping, send ACK in response to congestion + pair.drive_client(); // Process ACK + + // Sanity check: the time hasn't advanced in the meantime) + assert_eq!(pair.time, start); + + let stats_after_ping = pair.client_conn_mut(client_ch).stats(); + assert_eq!( + stats_after_ping.frame_tx.ping - stats_after_connect.frame_tx.ping, + 1 + ); + assert_eq!( + stats_after_ping.frame_rx.acks - stats_after_connect.frame_rx.acks, + 1 + ); + assert_eq!( + stats_after_ping.path.congestion_events - stats_after_connect.path.congestion_events, + 1 + ); +} + +fn setup_ack_frequency_test(max_ack_delay: Duration) -> (Pair, ConnectionHandle, ConnectionHandle) { + let mut client_config = client_config_with_deterministic_pns(); + let mut ack_freq_config = AckFrequencyConfig::default(); + ack_freq_config + .ack_eliciting_threshold(10u32.into()) + .max_ack_delay(Some(max_ack_delay)); + Arc::get_mut(&mut client_config.transport) + .unwrap() + .ack_frequency_config(Some(ack_freq_config)) + .mtu_discovery_config(None); // To keep traffic cleaner + + let mut pair = Pair::default_with_deterministic_pns(); + pair.latency = Duration::from_millis(10); // Need latency to avoid an RTT = 0 + let (client_ch, server_ch) = pair.connect_with(client_config); + pair.drive(); + + assert_eq!( + pair.client_conn_mut(client_ch) + .stats() + .frame_tx + .ack_frequency, + 1 + ); + assert_eq!(pair.client_conn_mut(client_ch).stats().frame_tx.ping, 0); + (pair, client_ch, server_ch) +} + +/// Verify that max ACK delay is counted from the first ACK-eliciting packet +#[test] +fn ack_frequency_ack_delayed_from_first_of_flight() { + let _guard = subscribe(); + let (mut pair, client_ch, server_ch) = setup_ack_frequency_test(Duration::from_millis(30)); + + // The client sends the following frames: + // + // * 0 ms: ping + // * 5 ms: ping x2 + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + pair.time += Duration::from_millis(5); + for _ in 0..2 { + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + } + + pair.time += Duration::from_millis(5); + // Server: receive the first ping and send no ACK + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); + + // Server: receive the second and third pings and send no ACK + pair.time += Duration::from_millis(10); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 2 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); + + // Server: Send an ACK after ACK delay expires + pair.time += Duration::from_millis(20); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 1 + ); +} + +#[test] +fn ack_frequency_ack_sent_after_max_ack_delay() { + let _guard = subscribe(); + let max_ack_delay = Duration::from_millis(30); + let (mut pair, client_ch, server_ch) = setup_ack_frequency_test(max_ack_delay); + + // Client sends a ping + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Server: receive the ping, send no ACK + pair.time += pair.latency; + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); + + // Server: send an ack after max_ack_delay has elapsed + pair.time += max_ack_delay; + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 0 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 1 + ); +} + +#[test] +fn ack_frequency_ack_sent_after_packets_above_threshold() { + let _guard = subscribe(); + let max_ack_delay = Duration::from_millis(30); + let (mut pair, client_ch, server_ch) = setup_ack_frequency_test(max_ack_delay); + + // The client sends the following frames: + // + // * 0 ms: ping + // * 5 ms: ping (11x) + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + pair.time += Duration::from_millis(5); + for _ in 0..11 { + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + } + + // Server: receive the first ping, send no ACK + pair.time += Duration::from_millis(5); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); + + // Server: receive the remaining pings, send ACK + pair.time += Duration::from_millis(5); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 11 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 1 + ); +} + +#[test] +fn ack_frequency_ack_sent_after_reordered_packets_below_threshold() { + let _guard = subscribe(); + let max_ack_delay = Duration::from_millis(30); + let (mut pair, client_ch, server_ch) = setup_ack_frequency_test(max_ack_delay); + + // The client sends the following frames: + // + // * 0 ms: ping + // * 5 ms: ping (lost) + // * 5 ms: ping + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + pair.time += Duration::from_millis(5); + + // Send and lose an ack-eliciting packet + pair.mtu = 0; + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Restore the default MTU and send another ping, which will arrive earlier than the dropped one + pair.mtu = DEFAULT_MTU; + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Server: receive first ping, send no ACK + pair.time += Duration::from_millis(5); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); + + // Server: receive second ping, send no ACK + pair.time += Duration::from_millis(5); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); +} + +#[test] +fn ack_frequency_ack_sent_after_reordered_packets_above_threshold() { + let _guard = subscribe(); + let max_ack_delay = Duration::from_millis(30); + let (mut pair, client_ch, server_ch) = setup_ack_frequency_test(max_ack_delay); + + // Send a ping + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Send and lose two ack-eliciting packets + pair.time += Duration::from_millis(5); + pair.mtu = 0; + for _ in 0..2 { + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + } + + // Restore the default MTU and send another ping, which will arrive earlier than the dropped ones + pair.mtu = DEFAULT_MTU; + pair.client_conn_mut(client_ch).ping(); + pair.drive_client(); + + // Server: receive first ping, send no ACK + pair.time += Duration::from_millis(5); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 0 + ); + + // Server: receive remaining ping, send ACK + pair.time += Duration::from_millis(5); + let server_stats_before = pair.server_conn_mut(server_ch).stats(); + pair.drive_server(); + let server_stats_after = pair.server_conn_mut(server_ch).stats(); + assert_eq!( + server_stats_after.frame_rx.ping - server_stats_before.frame_rx.ping, + 1 + ); + assert_eq!( + server_stats_after.frame_tx.acks - server_stats_before.frame_tx.acks, + 1 + ); +} + +#[test] +fn ack_frequency_update_max_delay() { + let _guard = subscribe(); + let (mut pair, client_ch, server_ch) = setup_ack_frequency_test(Duration::from_millis(200)); + + // Ack frequency was sent initially + assert_eq!( + pair.server_conn_mut(server_ch) + .stats() + .frame_rx + .ack_frequency, + 1 + ); + + // Client sends a PING + info!("first ping"); + pair.client_conn_mut(client_ch).ping(); + pair.drive(); + + // No change in ACK frequency + assert_eq!( + pair.server_conn_mut(server_ch) + .stats() + .frame_rx + .ack_frequency, + 1 + ); + + // RTT jumps, client sends another ping + info!("delayed ping"); + pair.latency *= 10; + pair.client_conn_mut(client_ch).ping(); + pair.drive(); + + // ACK frequency updated + assert!( + pair.server_conn_mut(server_ch) + .stats() + .frame_rx + .ack_frequency + >= 2 + ); +} + +fn stream_chunks(mut recv: RecvStream) -> Vec { + let mut buf = Vec::new(); + + let mut chunks = recv.read(true).unwrap(); + while let Ok(Some(chunk)) = chunks.next(usize::MAX) { + buf.extend(chunk.bytes); + } + + let _ = chunks.finalize(); + + buf +} + +/// Verify that an endpoint which receives but does not send ACK-eliciting data still receives ACKs +/// occasionally. This is not required for conformance, but makes loss detection more responsive and +/// reduces receiver memory use. +#[test] +fn pure_sender_voluntarily_acks() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let receiver_acks_initial = pair.server_conn_mut(server_ch).stats().frame_rx.acks; + + for _ in 0..100 { + const MSG: &[u8] = b"hello"; + pair.client_datagrams(client_ch) + .send(Bytes::from_static(MSG), true) + .unwrap(); + pair.drive(); + assert_eq!(pair.server_datagrams(server_ch).recv().unwrap(), MSG); + } + + let receiver_acks_final = pair.server_conn_mut(server_ch).stats().frame_rx.acks; + assert!(receiver_acks_final > receiver_acks_initial); +} + +#[test] +fn reject_manually() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.server.handle_incoming = Box::new(|_| IncomingConnectionBehavior::Reject); + + // The server should now reject incoming connections. + let client_ch = pair.begin_connect(client_config()); + pair.drive(); + pair.server.assert_no_accept(); + let client = pair.client.connections.get_mut(&client_ch).unwrap(); + assert!(client.is_closed()); + assert!(matches!( + client.poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::ConnectionClosed(close) + }) if close.error_code == TransportErrorCode::CONNECTION_REFUSED + )); +} + +#[test] +fn validate_then_reject_manually() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.server.handle_incoming = Box::new({ + let mut i = 0; + move |incoming| { + if incoming.remote_address_validated() { + assert_eq!(i, 1); + i += 1; + IncomingConnectionBehavior::Reject + } else { + assert_eq!(i, 0); + i += 1; + IncomingConnectionBehavior::Retry + } + } + }); + + // The server should now retry and reject incoming connections. + let client_ch = pair.begin_connect(client_config()); + pair.drive(); + pair.server.assert_no_accept(); + let client = pair.client.connections.get_mut(&client_ch).unwrap(); + assert!(client.is_closed()); + assert!(matches!( + client.poll(), + Some(Event::ConnectionLost { + reason: ConnectionError::ConnectionClosed(close) + }) if close.error_code == TransportErrorCode::CONNECTION_REFUSED + )); + pair.drive(); + assert_matches!(pair.client_conn_mut(client_ch).poll(), None); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn endpoint_and_connection_impl_send_sync() { + const fn is_send_sync() {} + is_send_sync::(); + is_send_sync::(); +} + +#[test] +fn stream_gso() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + + let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap(); + + let initial_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + + // Send 20KiB of stream data, which comfortably fits inside two `tests::util::MAX_DATAGRAMS` + // datagram batches + info!("sending"); + for _ in 0..20 { + pair.client_send(client_ch, s).write(&[0; 1024]).unwrap(); + } + pair.client_send(client_ch, s).finish().unwrap(); + pair.drive(); + let final_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + assert_eq!(final_ios - initial_ios, 2); +} + +#[test] +fn datagram_gso() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + + let initial_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + let initial_bytes = pair.client_conn_mut(client_ch).stats().udp_tx.bytes; + + // Send 10 datagrams above half the MTU, which fits inside a `tests::util::MAX_DATAGRAMS` + // datagram batch + info!("sending"); + const DATAGRAM_LEN: usize = 1024; + const DATAGRAMS: usize = 10; + for _ in 0..DATAGRAMS { + pair.client_datagrams(client_ch) + .send(Bytes::from_static(&[0; DATAGRAM_LEN]), false) + .unwrap(); + } + pair.drive(); + let final_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + let final_bytes = pair.client_conn_mut(client_ch).stats().udp_tx.bytes; + assert_eq!(final_ios - initial_ios, 1); + // Expected overhead: flags + CID + PN + tag + frame type + frame length = 1 + 8 + 1 + 16 + 1 + 2 = 29 + assert_eq!( + final_bytes - initial_bytes, + ((29 + DATAGRAM_LEN) * DATAGRAMS) as u64 + ); +} + +#[test] +fn gso_truncation() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + let initial_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + + // Send three application datagrams such that each is large to be combined with another in a + // single MTU, and the second datagram would require an unreasonably large amount of padding to + // produce a QUIC packet of the same length as the first. + info!("sending"); + const SIZES: [usize; 3] = [1024, 768, 768]; + for len in SIZES { + pair.client_datagrams(client_ch) + .send(vec![0; len].into(), false) + .unwrap(); + } + pair.drive(); + let final_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + assert_eq!(final_ios - initial_ios, 2); + for len in SIZES { + assert_eq!( + pair.server_datagrams(server_ch) + .recv() + .expect("datagram lost") + .len(), + len + ); + } +} + +/// Verify that UDP datagrams are padded to MTU if specified in the transport config. +#[test] +fn pad_to_mtu() { + let _guard = subscribe(); + const MTU: u16 = 1333; + let client_config = { + let mut c_config = client_config(); + let t_config = TransportConfig { + initial_mtu: MTU, + mtu_discovery_config: None, + pad_to_mtu: true, + ..TransportConfig::default() + }; + c_config.transport_config(t_config.into()); + c_config + }; + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect_with(client_config); + + let initial_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + pair.server.capture_inbound_packets = true; + + info!("sending"); + // Send two datagrams significantly smaller than MTU, but large enough to require two UDP datagrams. + const LEN_1: usize = 800; + const LEN_2: usize = 600; + pair.client_datagrams(client_ch) + .send(vec![0; LEN_1].into(), false) + .unwrap(); + pair.client_datagrams(client_ch) + .send(vec![0; LEN_2].into(), false) + .unwrap(); + pair.client.drive(pair.time, pair.server.addr); + + // Check padding + assert_eq!(pair.client.outbound.len(), 2); + assert_eq!(pair.client.outbound[0].0.size, usize::from(MTU)); + assert_eq!(pair.client.outbound[0].1.len(), usize::from(MTU)); + assert_eq!(pair.client.outbound[1].0.size, usize::from(MTU)); + assert_eq!(pair.client.outbound[1].1.len(), usize::from(MTU)); + pair.drive_client(); + assert_eq!(pair.server.inbound.len(), 2); + assert_eq!(pair.server.inbound[0].2.len(), usize::from(MTU)); + assert_eq!(pair.server.inbound[1].2.len(), usize::from(MTU)); + pair.drive(); + + // Check that both datagrams ended up in the same GSO batch + let final_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios; + assert_eq!(final_ios - initial_ios, 1); + + assert_eq!( + pair.server_datagrams(server_ch) + .recv() + .expect("datagram lost") + .len(), + LEN_1 + ); + assert_eq!( + pair.server_datagrams(server_ch) + .recv() + .expect("datagram lost") + .len(), + LEN_2 + ); +} + +/// Verify that a large application datagram is sent successfully when an ACK frame too large to fit +/// alongside it is also queued, in exactly 2 UDP datagrams. +#[test] +fn large_datagram_with_acks() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, server_ch) = pair.connect(); + + // Force the client to generate a large ACK frame by dropping several packets + for _ in 0..10 { + pair.server_conn_mut(server_ch).ping(); + pair.drive_server(); + pair.client.inbound.pop_back(); + pair.server_conn_mut(server_ch).ping(); + pair.drive_server(); + } + + let max_size = pair.client_datagrams(client_ch).max_size().unwrap(); + let msg = Bytes::from(vec![0; max_size]); + pair.client_datagrams(client_ch) + .send(msg.clone(), true) + .unwrap(); + let initial_datagrams = pair.client_conn_mut(client_ch).stats().udp_tx.datagrams; + pair.drive(); + let final_datagrams = pair.client_conn_mut(client_ch).stats().udp_tx.datagrams; + assert_eq!(pair.server_datagrams(server_ch).recv().unwrap(), msg); + assert_eq!(final_datagrams - initial_datagrams, 2); +} + +/// Verify that an ACK prompted by receipt of many non-ACK-eliciting packets is sent alongside +/// outgoing application datagrams too large to coexist in the same packet with it. +#[test] +fn voluntary_ack_with_large_datagrams() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let (client_ch, _) = pair.connect(); + + // Prompt many large ACKs from the server + let initial_datagrams = pair.client_conn_mut(client_ch).stats().udp_tx.datagrams; + // Send enough packets that we're confident some packet numbers will be skipped, ensuring that + // larger ACKs occur + const COUNT: usize = 256; + for _ in 0..COUNT { + let max_size = pair.client_datagrams(client_ch).max_size().unwrap(); + pair.client_datagrams(client_ch) + .send(vec![0; max_size].into(), true) + .unwrap(); + pair.drive(); + } + let final_datagrams = pair.client_conn_mut(client_ch).stats().udp_tx.datagrams; + // Failure may indicate `max_size` is too small and ACKs are reliably being packed into the same + // datagram, which is reasonable behavior but makes this test ineffective. + assert_ne!( + final_datagrams - initial_datagrams, + COUNT as u64, + "client should have sent some ACK-only packets" + ); +} + +#[test] +fn reject_short_idcid() { + let _guard = subscribe(); + let client_addr = "[::2]:7890".parse().unwrap(); + let mut server = Endpoint::new( + Default::default(), + Some(Arc::new(server_config())), + true, + None, + ); + let now = Instant::now(); + let mut buf = Vec::with_capacity(server.config().get_max_udp_payload_size() as usize); + // Initial header that has an empty DCID but is otherwise well-formed + let mut initial = BytesMut::from(hex!("c4 00000001 00 00 00 3f").as_ref()); + initial.resize(MIN_INITIAL_SIZE.into(), 0); + let event = server.handle(now, client_addr, None, None, initial, &mut buf); + let Some(DatagramEvent::Response(Transmit { .. })) = event else { + panic!("expected an initial close"); + }; +} + +/// Ensure that a connection can be made when a preferred address is advertised by the server, +/// regardless of whether the address is actually used. +#[test] +fn preferred_address() { + let _guard = subscribe(); + let mut server_config = server_config(); + server_config.preferred_address_v6(Some("[::1]:65535".parse().unwrap())); + + let mut pair = Pair::new(Arc::new(EndpointConfig::default()), server_config); + pair.connect(); +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/token.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/token.rs new file mode 100644 index 0000000..ac466c6 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/token.rs @@ -0,0 +1,333 @@ +//! Tests specifically for tokens + +use super::*; + +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +use wasm_bindgen_test::wasm_bindgen_test as test; + +#[test] +fn stateless_retry() { + let _guard = subscribe(); + let mut pair = Pair::default(); + pair.server.handle_incoming = Box::new(validate_incoming); + let (client_ch, _server_ch) = pair.connect(); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn retry_token_expired() { + let _guard = subscribe(); + + let fake_time = Arc::new(FakeTimeSource::new()); + let retry_token_lifetime = Duration::from_secs(1); + + let mut pair = Pair::default(); + pair.server.handle_incoming = Box::new(validate_incoming); + + let mut config = server_config(); + config + .time_source(Arc::clone(&fake_time) as _) + .retry_token_lifetime(retry_token_lifetime); + pair.server.set_server_config(Some(Arc::new(config))); + + let client_ch = pair.begin_connect(client_config()); + pair.drive_client(); + pair.drive_server(); + pair.drive_client(); + + // to expire retry token + fake_time.advance(retry_token_lifetime + Duration::from_millis(1)); + + pair.drive(); + assert_matches!( + pair.client_conn_mut(client_ch).poll(), + Some(Event::ConnectionLost { reason: ConnectionError::ConnectionClosed(err) }) + if err.error_code == TransportErrorCode::INVALID_TOKEN + ); + + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn use_token() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let client_config = client_config(); + let (client_ch, _server_ch) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + pair.server.handle_incoming = Box::new(|incoming| { + assert!(incoming.remote_address_validated()); + assert!(incoming.may_retry()); + IncomingConnectionBehavior::Accept + }); + let (client_ch_2, _server_ch_2) = pair.connect_with(client_config); + pair.client + .connections + .get_mut(&client_ch_2) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn retry_then_use_token() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let client_config = client_config(); + pair.server.handle_incoming = Box::new(validate_incoming); + let (client_ch, _server_ch) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + pair.server.handle_incoming = Box::new(|incoming| { + assert!(incoming.remote_address_validated()); + assert!(incoming.may_retry()); + IncomingConnectionBehavior::Accept + }); + let (client_ch_2, _server_ch_2) = pair.connect_with(client_config); + pair.client + .connections + .get_mut(&client_ch_2) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn use_token_then_retry() { + let _guard = subscribe(); + let mut pair = Pair::default(); + let client_config = client_config(); + let (client_ch, _server_ch) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + pair.server.handle_incoming = Box::new({ + let mut i = 0; + move |incoming| { + if i == 0 { + assert!(incoming.remote_address_validated()); + assert!(incoming.may_retry()); + i += 1; + IncomingConnectionBehavior::Retry + } else if i == 1 { + assert!(incoming.remote_address_validated()); + assert!(!incoming.may_retry()); + i += 1; + IncomingConnectionBehavior::Accept + } else { + panic!("too many handle_incoming iterations") + } + } + }); + let (client_ch_2, _server_ch_2) = pair.connect_with(client_config); + pair.client + .connections + .get_mut(&client_ch_2) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn use_same_token_twice() { + #[derive(Default)] + struct EvilTokenStore(Mutex); + + impl TokenStore for EvilTokenStore { + fn insert(&self, _server_name: &str, token: Bytes) { + let mut lock = self.0.lock().unwrap(); + if lock.is_empty() { + *lock = token; + } + } + + fn take(&self, _server_name: &str) -> Option { + let lock = self.0.lock().unwrap(); + if lock.is_empty() { + None + } else { + Some(lock.clone()) + } + } + } + + let _guard = subscribe(); + let mut pair = Pair::default(); + let mut client_config = client_config(); + client_config.token_store(Arc::new(EvilTokenStore::default())); + let (client_ch, _server_ch) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + pair.server.handle_incoming = Box::new(|incoming| { + assert!(incoming.remote_address_validated()); + assert!(incoming.may_retry()); + IncomingConnectionBehavior::Accept + }); + let (client_ch_2, _server_ch_2) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch_2) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + pair.server.handle_incoming = Box::new(|incoming| { + assert!(!incoming.remote_address_validated()); + assert!(incoming.may_retry()); + IncomingConnectionBehavior::Accept + }); + let (client_ch_3, _server_ch_3) = pair.connect_with(client_config); + pair.client + .connections + .get_mut(&client_ch_3) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +#[test] +fn use_token_expired() { + let _guard = subscribe(); + let fake_time = Arc::new(FakeTimeSource::new()); + let lifetime = Duration::from_secs(10000); + let mut server_config = server_config(); + server_config + .time_source(Arc::clone(&fake_time) as _) + .validation_token + .lifetime(lifetime); + let mut pair = Pair::new(Default::default(), server_config); + let client_config = client_config(); + let (client_ch, _server_ch) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + pair.server.handle_incoming = Box::new(|incoming| { + assert!(incoming.remote_address_validated()); + assert!(incoming.may_retry()); + IncomingConnectionBehavior::Accept + }); + let (client_ch_2, _server_ch_2) = pair.connect_with(client_config.clone()); + pair.client + .connections + .get_mut(&client_ch_2) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); + + fake_time.advance(lifetime + Duration::from_secs(1)); + + pair.server.handle_incoming = Box::new(|incoming| { + assert!(!incoming.remote_address_validated()); + assert!(incoming.may_retry()); + IncomingConnectionBehavior::Accept + }); + let (client_ch_3, _server_ch_3) = pair.connect_with(client_config); + pair.client + .connections + .get_mut(&client_ch_3) + .unwrap() + .close(pair.time, VarInt(42), Bytes::new()); + pair.drive(); + assert_eq!(pair.client.known_connections(), 0); + assert_eq!(pair.client.known_cids(), 0); + assert_eq!(pair.server.known_connections(), 0); + assert_eq!(pair.server.known_cids(), 0); +} + +pub(super) struct FakeTimeSource(Mutex); + +impl FakeTimeSource { + pub(super) fn new() -> Self { + Self(Mutex::new(SystemTime::now())) + } + + pub(super) fn advance(&self, dur: Duration) { + *self.0.lock().unwrap() += dur; + } +} + +impl TimeSource for FakeTimeSource { + fn now(&self) -> SystemTime { + *self.0.lock().unwrap() + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/util.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/util.rs new file mode 100644 index 0000000..fd61e61 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/tests/util.rs @@ -0,0 +1,745 @@ +use std::{ + cmp, + collections::{HashMap, HashSet, VecDeque}, + env, + io::{self, Write}, + mem, + net::{Ipv6Addr, SocketAddr, UdpSocket}, + ops::RangeFrom, + str, + sync::{Arc, Mutex}, +}; + +use assert_matches::assert_matches; +use bytes::BytesMut; +use lazy_static::lazy_static; +use rustls::{ + KeyLogFile, + client::WebPkiServerVerifier, + pki_types::{CertificateDer, PrivateKeyDer}, +}; +use tracing::{info_span, trace}; + +use super::crypto::rustls::{QuicClientConfig, QuicServerConfig, configured_provider}; +use super::*; +use crate::{Duration, Instant}; + +pub(super) const DEFAULT_MTU: usize = 1452; + +pub(super) struct Pair { + pub(super) server: TestEndpoint, + pub(super) client: TestEndpoint, + /// Start time + epoch: Instant, + /// Current time + pub(super) time: Instant, + /// Simulates the maximum size allowed for UDP payloads by the link (packets exceeding this size will be dropped) + pub(super) mtu: usize, + /// Simulates explicit congestion notification + pub(super) congestion_experienced: bool, + // One-way + pub(super) latency: Duration, + /// Number of spin bit flips + pub(super) spins: u64, + last_spin: bool, +} + +impl Pair { + pub(super) fn default_with_deterministic_pns() -> Self { + let mut cfg = server_config(); + let mut transport = TransportConfig::default(); + transport.deterministic_packet_numbers(true); + cfg.transport = Arc::new(transport); + Self::new(Default::default(), cfg) + } + + pub(super) fn new(endpoint_config: Arc, server_config: ServerConfig) -> Self { + let server = Endpoint::new( + endpoint_config.clone(), + Some(Arc::new(server_config)), + true, + None, + ); + let client = Endpoint::new(endpoint_config, None, true, None); + + Self::new_from_endpoint(client, server) + } + + pub(super) fn new_from_endpoint(client: Endpoint, server: Endpoint) -> Self { + let server_addr = SocketAddr::new( + Ipv6Addr::LOCALHOST.into(), + SERVER_PORTS.lock().unwrap().next().unwrap(), + ); + let client_addr = SocketAddr::new( + Ipv6Addr::LOCALHOST.into(), + CLIENT_PORTS.lock().unwrap().next().unwrap(), + ); + let now = Instant::now(); + Self { + server: TestEndpoint::new(server, server_addr), + client: TestEndpoint::new(client, client_addr), + epoch: now, + time: now, + mtu: DEFAULT_MTU, + latency: Duration::ZERO, + spins: 0, + last_spin: false, + congestion_experienced: false, + } + } + + /// Returns whether the connection is not idle + pub(super) fn step(&mut self) -> bool { + self.drive_client(); + self.drive_server(); + if self.client.is_idle() && self.server.is_idle() { + return false; + } + + let client_t = self.client.next_wakeup(); + let server_t = self.server.next_wakeup(); + match min_opt(client_t, server_t) { + Some(t) if Some(t) == client_t => { + if t != self.time { + self.time = self.time.max(t); + trace!("advancing to {:?} for client", self.time - self.epoch); + } + true + } + Some(t) if Some(t) == server_t => { + if t != self.time { + self.time = self.time.max(t); + trace!("advancing to {:?} for server", self.time - self.epoch); + } + true + } + Some(_) => unreachable!(), + None => false, + } + } + + /// Advance time until both connections are idle + pub(super) fn drive(&mut self) { + while self.step() {} + } + + /// Advance time until both connections are idle, or after 100 steps have been executed + /// + /// Returns true if the amount of steps exceeds the bounds, because the connections never became + /// idle + pub(super) fn drive_bounded(&mut self) -> bool { + for _ in 0..100 { + if !self.step() { + return false; + } + } + + true + } + + pub(super) fn drive_client(&mut self) { + let span = info_span!("client"); + let _guard = span.enter(); + self.client.drive(self.time, self.server.addr); + for (packet, buffer) in self.client.outbound.drain(..) { + let packet_size = packet_size(&packet, &buffer); + if packet_size > self.mtu { + info!(packet_size, "dropping packet (max size exceeded)"); + continue; + } + if buffer[0] & packet::LONG_HEADER_FORM == 0 { + let spin = buffer[0] & packet::SPIN_BIT != 0; + self.spins += (spin == self.last_spin) as u64; + self.last_spin = spin; + } + if let Some(ref socket) = self.client.socket { + socket.send_to(&buffer, packet.destination).unwrap(); + } + if self.server.addr == packet.destination { + let ecn = set_congestion_experienced(packet.ecn, self.congestion_experienced); + self.server.inbound.push_back(( + self.time + self.latency, + ecn, + buffer.as_ref().into(), + )); + } + } + } + + pub(super) fn drive_server(&mut self) { + let span = info_span!("server"); + let _guard = span.enter(); + self.server.drive(self.time, self.client.addr); + for (packet, buffer) in self.server.outbound.drain(..) { + let packet_size = packet_size(&packet, &buffer); + if packet_size > self.mtu { + info!(packet_size, "dropping packet (max size exceeded)"); + continue; + } + if let Some(ref socket) = self.server.socket { + socket.send_to(&buffer, packet.destination).unwrap(); + } + if self.client.addr == packet.destination { + let ecn = set_congestion_experienced(packet.ecn, self.congestion_experienced); + self.client.inbound.push_back(( + self.time + self.latency, + ecn, + buffer.as_ref().into(), + )); + } + } + } + + pub(super) fn connect(&mut self) -> (ConnectionHandle, ConnectionHandle) { + self.connect_with(client_config()) + } + + pub(super) fn connect_with( + &mut self, + config: ClientConfig, + ) -> (ConnectionHandle, ConnectionHandle) { + info!("connecting"); + let client_ch = self.begin_connect(config); + self.drive(); + let server_ch = self.server.assert_accept(); + self.finish_connect(client_ch, server_ch); + (client_ch, server_ch) + } + + /// Just start connecting the client + pub(super) fn begin_connect(&mut self, config: ClientConfig) -> ConnectionHandle { + let span = info_span!("client"); + let _guard = span.enter(); + let (client_ch, client_conn) = self + .client + .connect(self.time, config, self.server.addr, "localhost") + .unwrap(); + self.client.connections.insert(client_ch, client_conn); + client_ch + } + + fn finish_connect(&mut self, client_ch: ConnectionHandle, server_ch: ConnectionHandle) { + assert_matches!( + self.client_conn_mut(client_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + self.client_conn_mut(client_ch).poll(), + Some(Event::Connected) + ); + assert_matches!( + self.server_conn_mut(server_ch).poll(), + Some(Event::HandshakeDataReady) + ); + assert_matches!( + self.server_conn_mut(server_ch).poll(), + Some(Event::Connected) + ); + } + + pub(super) fn client_conn_mut(&mut self, ch: ConnectionHandle) -> &mut Connection { + self.client.connections.get_mut(&ch).unwrap() + } + + pub(super) fn client_streams(&mut self, ch: ConnectionHandle) -> Streams<'_> { + self.client_conn_mut(ch).streams() + } + + pub(super) fn client_send(&mut self, ch: ConnectionHandle, s: StreamId) -> SendStream<'_> { + self.client_conn_mut(ch).send_stream(s) + } + + pub(super) fn client_recv(&mut self, ch: ConnectionHandle, s: StreamId) -> RecvStream<'_> { + self.client_conn_mut(ch).recv_stream(s) + } + + pub(super) fn client_datagrams(&mut self, ch: ConnectionHandle) -> Datagrams<'_> { + self.client_conn_mut(ch).datagrams() + } + + pub(super) fn server_conn_mut(&mut self, ch: ConnectionHandle) -> &mut Connection { + self.server.connections.get_mut(&ch).unwrap() + } + + pub(super) fn server_streams(&mut self, ch: ConnectionHandle) -> Streams<'_> { + self.server_conn_mut(ch).streams() + } + + pub(super) fn server_send(&mut self, ch: ConnectionHandle, s: StreamId) -> SendStream<'_> { + self.server_conn_mut(ch).send_stream(s) + } + + pub(super) fn server_recv(&mut self, ch: ConnectionHandle, s: StreamId) -> RecvStream<'_> { + self.server_conn_mut(ch).recv_stream(s) + } + + pub(super) fn server_datagrams(&mut self, ch: ConnectionHandle) -> Datagrams<'_> { + self.server_conn_mut(ch).datagrams() + } +} + +impl Default for Pair { + fn default() -> Self { + Self::new(Default::default(), server_config()) + } +} + +pub(super) struct TestEndpoint { + pub(super) endpoint: Endpoint, + pub(super) addr: SocketAddr, + socket: Option, + timeout: Option, + pub(super) outbound: VecDeque<(Transmit, Bytes)>, + delayed: VecDeque<(Transmit, Bytes)>, + pub(super) inbound: VecDeque<(Instant, Option, BytesMut)>, + accepted: Option>, + pub(super) connections: HashMap, + conn_events: HashMap>, + pub(super) captured_packets: Vec>, + pub(super) capture_inbound_packets: bool, + pub(super) handle_incoming: Box IncomingConnectionBehavior>, + pub(super) waiting_incoming: Vec, +} + +#[derive(Debug, Copy, Clone)] +pub(super) enum IncomingConnectionBehavior { + Accept, + Reject, + Retry, + Wait, +} + +pub(super) fn validate_incoming(incoming: &Incoming) -> IncomingConnectionBehavior { + if incoming.remote_address_validated() { + IncomingConnectionBehavior::Accept + } else { + IncomingConnectionBehavior::Retry + } +} + +impl TestEndpoint { + fn new(endpoint: Endpoint, addr: SocketAddr) -> Self { + let socket = if env::var_os("SSLKEYLOGFILE").is_some() { + let socket = UdpSocket::bind(addr).expect("failed to bind UDP socket"); + socket + .set_read_timeout(Some(Duration::from_millis(10))) + .unwrap(); + Some(socket) + } else { + None + }; + Self { + endpoint, + addr, + socket, + timeout: None, + outbound: VecDeque::new(), + delayed: VecDeque::new(), + inbound: VecDeque::new(), + accepted: None, + connections: HashMap::default(), + conn_events: HashMap::default(), + captured_packets: Vec::new(), + capture_inbound_packets: false, + handle_incoming: Box::new(|_| IncomingConnectionBehavior::Accept), + waiting_incoming: Vec::new(), + } + } + + pub(super) fn drive(&mut self, now: Instant, remote: SocketAddr) { + self.drive_incoming(now, remote); + self.drive_outgoing(now); + } + + pub(super) fn drive_incoming(&mut self, now: Instant, remote: SocketAddr) { + if let Some(ref socket) = self.socket { + loop { + let mut buf = [0; 8192]; + if socket.recv_from(&mut buf).is_err() { + break; + } + } + } + let buffer_size = self.endpoint.config().get_max_udp_payload_size() as usize; + let mut buf = Vec::with_capacity(buffer_size); + + while self.inbound.front().is_some_and(|x| x.0 <= now) { + let (recv_time, ecn, packet) = self.inbound.pop_front().unwrap(); + if let Some(event) = self + .endpoint + .handle(recv_time, remote, None, ecn, packet, &mut buf) + { + match event { + DatagramEvent::NewConnection(incoming) => { + match (self.handle_incoming)(&incoming) { + IncomingConnectionBehavior::Accept => { + let _ = self.try_accept(incoming, now); + } + IncomingConnectionBehavior::Reject => { + self.reject(incoming); + } + IncomingConnectionBehavior::Retry => { + self.retry(incoming); + } + IncomingConnectionBehavior::Wait => { + self.waiting_incoming.push(incoming); + } + } + } + DatagramEvent::ConnectionEvent(ch, event) => { + if self.capture_inbound_packets { + let packet = self.connections[&ch].decode_packet(&event); + self.captured_packets.extend(packet); + } + + self.conn_events.entry(ch).or_default().push_back(event); + } + DatagramEvent::Response(transmit) => { + let size = transmit.size; + self.outbound.extend(split_transmit(transmit, &buf[..size])); + buf.clear(); + } + } + } + } + } + + pub(super) fn drive_outgoing(&mut self, now: Instant) { + let buffer_size = self.endpoint.config().get_max_udp_payload_size() as usize; + let mut buf = Vec::with_capacity(buffer_size); + + loop { + let mut endpoint_events: Vec<(ConnectionHandle, EndpointEvent)> = vec![]; + for (ch, conn) in self.connections.iter_mut() { + if self.timeout.is_some_and(|x| x <= now) { + self.timeout = None; + conn.handle_timeout(now); + } + + for (_, mut events) in self.conn_events.drain() { + for event in events.drain(..) { + conn.handle_event(event); + } + } + + while let Some(event) = conn.poll_endpoint_events() { + endpoint_events.push((*ch, event)); + } + while let Some(transmit) = conn.poll_transmit(now, MAX_DATAGRAMS, &mut buf) { + let size = transmit.size; + self.outbound.extend(split_transmit(transmit, &buf[..size])); + buf.clear(); + } + self.timeout = conn.poll_timeout(); + } + + if endpoint_events.is_empty() { + break; + } + + for (ch, event) in endpoint_events { + if let Some(event) = self.handle_event(ch, event) { + if let Some(conn) = self.connections.get_mut(&ch) { + conn.handle_event(event); + } + } + } + } + } + + pub(super) fn next_wakeup(&self) -> Option { + let next_inbound = self.inbound.front().map(|x| x.0); + min_opt(self.timeout, next_inbound) + } + + fn is_idle(&self) -> bool { + self.connections.values().all(|x| x.is_idle()) + } + + pub(super) fn delay_outbound(&mut self) { + assert!(self.delayed.is_empty()); + mem::swap(&mut self.delayed, &mut self.outbound); + } + + pub(super) fn finish_delay(&mut self) { + self.outbound.extend(self.delayed.drain(..)); + } + + pub(super) fn try_accept( + &mut self, + incoming: Incoming, + now: Instant, + ) -> Result { + let mut buf = Vec::new(); + match self.endpoint.accept(incoming, now, &mut buf, None) { + Ok((ch, conn)) => { + self.connections.insert(ch, conn); + self.accepted = Some(Ok(ch)); + Ok(ch) + } + Err(error) => { + if let Some(transmit) = error.response { + let size = transmit.size; + self.outbound.extend(split_transmit(transmit, &buf[..size])); + } + self.accepted = Some(Err(error.cause.clone())); + Err(error.cause) + } + } + } + + pub(super) fn retry(&mut self, incoming: Incoming) { + let mut buf = Vec::new(); + let transmit = self.endpoint.retry(incoming, &mut buf).unwrap(); + let size = transmit.size; + self.outbound.extend(split_transmit(transmit, &buf[..size])); + } + + pub(super) fn reject(&mut self, incoming: Incoming) { + let mut buf = Vec::new(); + let transmit = self.endpoint.refuse(incoming, &mut buf); + let size = transmit.size; + self.outbound.extend(split_transmit(transmit, &buf[..size])); + } + + pub(super) fn assert_accept(&mut self) -> ConnectionHandle { + self.accepted + .take() + .expect("server didn't try connecting") + .expect("server experienced error connecting") + } + + pub(super) fn assert_accept_error(&mut self) -> ConnectionError { + self.accepted + .take() + .expect("server didn't try connecting") + .expect_err("server did unexpectedly connect without error") + } + + pub(super) fn assert_no_accept(&self) { + assert!(self.accepted.is_none(), "server did unexpectedly connect") + } +} + +impl ::std::ops::Deref for TestEndpoint { + type Target = Endpoint; + fn deref(&self) -> &Endpoint { + &self.endpoint + } +} + +impl ::std::ops::DerefMut for TestEndpoint { + fn deref_mut(&mut self) -> &mut Endpoint { + &mut self.endpoint + } +} + +pub(super) fn subscribe() -> tracing::subscriber::DefaultGuard { + let builder = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::TRACE) + .with_writer(|| TestWriter); + // tracing uses std::time to trace time, which panics in wasm. + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + let builder = builder.without_time(); + tracing::subscriber::set_default(builder.finish()) +} + +struct TestWriter; + +impl Write for TestWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + print!( + "{}", + str::from_utf8(buf).expect("tried to log invalid UTF-8") + ); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + io::stdout().flush() + } +} + +pub(super) fn server_config() -> ServerConfig { + let mut config = ServerConfig::with_crypto(Arc::new(server_crypto())); + if !cfg!(feature = "bloom") { + config + .validation_token + .sent(2) + .log(Arc::new(SimpleTokenLog::default())); + } + config +} + +pub(super) fn server_config_with_cert( + cert: CertificateDer<'static>, + key: PrivateKeyDer<'static>, +) -> ServerConfig { + let mut config = ServerConfig::with_crypto(Arc::new(server_crypto_with_cert(cert, key))); + config + .validation_token + .sent(2) + .log(Arc::new(SimpleTokenLog::default())); + config +} + +pub(super) fn server_crypto() -> QuicServerConfig { + server_crypto_inner(None, None) +} + +pub(super) fn server_crypto_with_alpn(alpn: Vec>) -> QuicServerConfig { + server_crypto_inner(None, Some(alpn)) +} + +pub(super) fn server_crypto_with_cert( + cert: CertificateDer<'static>, + key: PrivateKeyDer<'static>, +) -> QuicServerConfig { + server_crypto_inner(Some((cert, key)), None) +} + +fn server_crypto_inner( + identity: Option<(CertificateDer<'static>, PrivateKeyDer<'static>)>, + alpn: Option>>, +) -> QuicServerConfig { + let (cert, key) = identity.unwrap_or_else(|| { + ( + CERTIFIED_KEY.cert.der().clone(), + PrivateKeyDer::Pkcs8(CERTIFIED_KEY.signing_key.serialize_der().into()), + ) + }); + + let mut config = QuicServerConfig::inner(vec![cert], key).unwrap(); + if let Some(alpn) = alpn { + config.alpn_protocols = alpn; + } + + config.try_into().unwrap() +} + +pub(super) fn client_config() -> ClientConfig { + ClientConfig::new(Arc::new(client_crypto())) +} + +pub(super) fn client_config_with_deterministic_pns() -> ClientConfig { + let mut cfg = ClientConfig::new(Arc::new(client_crypto())); + let mut transport = TransportConfig::default(); + transport.deterministic_packet_numbers(true); + cfg.transport = Arc::new(transport); + cfg +} + +pub(super) fn client_config_with_certs(certs: Vec>) -> ClientConfig { + ClientConfig::new(Arc::new(client_crypto_inner(Some(certs), None))) +} + +pub(super) fn client_crypto() -> QuicClientConfig { + client_crypto_inner(None, None) +} + +pub(super) fn client_crypto_with_alpn(protocols: Vec>) -> QuicClientConfig { + client_crypto_inner(None, Some(protocols)) +} + +fn client_crypto_inner( + certs: Option>>, + alpn: Option>>, +) -> QuicClientConfig { + let mut roots = rustls::RootCertStore::empty(); + for cert in certs.unwrap_or_else(|| vec![CERTIFIED_KEY.cert.der().clone()]) { + roots.add(cert).unwrap(); + } + + let mut inner = QuicClientConfig::inner( + WebPkiServerVerifier::builder_with_provider(Arc::new(roots), configured_provider()) + .build() + .unwrap(), + ); + inner.key_log = Arc::new(KeyLogFile::new()); + if let Some(alpn) = alpn { + inner.alpn_protocols = alpn; + } + + inner.try_into().unwrap() +} + +pub(super) fn min_opt(x: Option, y: Option) -> Option { + match (x, y) { + (Some(x), Some(y)) => Some(cmp::min(x, y)), + (Some(x), _) => Some(x), + (_, Some(y)) => Some(y), + _ => None, + } +} + +/// The maximum of datagrams TestEndpoint will produce via `poll_transmit` +const MAX_DATAGRAMS: usize = 10; + +fn split_transmit(transmit: Transmit, buffer: &[u8]) -> Vec<(Transmit, Bytes)> { + let mut buffer = Bytes::copy_from_slice(buffer); + let segment_size = match transmit.segment_size { + Some(segment_size) => segment_size, + _ => return vec![(transmit, buffer)], + }; + + let mut transmits = Vec::new(); + while !buffer.is_empty() { + let end = segment_size.min(buffer.len()); + + let contents = buffer.split_to(end); + transmits.push(( + Transmit { + destination: transmit.destination, + size: contents.len(), + ecn: transmit.ecn, + segment_size: None, + src_ip: transmit.src_ip, + }, + contents, + )); + } + + transmits +} + +fn packet_size(transmit: &Transmit, buffer: &Bytes) -> usize { + if transmit.segment_size.is_some() { + panic!("This transmit is meant to be split into multiple packets!"); + } + + buffer.len() +} + +fn set_congestion_experienced( + x: Option, + congestion_experienced: bool, +) -> Option { + x.map(|codepoint| match congestion_experienced { + true => EcnCodepoint::Ce, + false => codepoint, + }) +} + +lazy_static! { + pub static ref SERVER_PORTS: Mutex> = Mutex::new(4433..); + pub static ref CLIENT_PORTS: Mutex> = Mutex::new(44433..); + pub(crate) static ref CERTIFIED_KEY: rcgen::CertifiedKey = + rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); +} + +#[derive(Default)] +struct SimpleTokenLog(Mutex>); + +impl TokenLog for SimpleTokenLog { + fn check_and_insert( + &self, + nonce: u128, + _issued: SystemTime, + _lifetime: Duration, + ) -> Result<(), TokenReuseError> { + if self.0.lock().unwrap().insert(nonce) { + Ok(()) + } else { + Err(TokenReuseError) + } + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/token.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/token.rs new file mode 100644 index 0000000..e4ab5ea --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/token.rs @@ -0,0 +1,507 @@ +use std::{ + fmt, + mem::size_of, + net::{IpAddr, SocketAddr}, +}; + +use bytes::{Buf, BufMut, Bytes}; +use rand::Rng; + +use crate::{ + Duration, RESET_TOKEN_SIZE, ServerConfig, SystemTime, UNIX_EPOCH, + coding::{BufExt, BufMutExt}, + crypto::{HandshakeTokenKey, HmacKey}, + packet::InitialHeader, + shared::ConnectionId, +}; + +/// Responsible for limiting clients' ability to reuse validation tokens +/// +/// [_RFC 9000 § 8.1.4:_](https://www.rfc-editor.org/rfc/rfc9000.html#section-8.1.4) +/// +/// > Attackers could replay tokens to use servers as amplifiers in DDoS attacks. To protect +/// > against such attacks, servers MUST ensure that replay of tokens is prevented or limited. +/// > Servers SHOULD ensure that tokens sent in Retry packets are only accepted for a short time, +/// > as they are returned immediately by clients. Tokens that are provided in NEW_TOKEN frames +/// > (Section 19.7) need to be valid for longer but SHOULD NOT be accepted multiple times. +/// > Servers are encouraged to allow tokens to be used only once, if possible; tokens MAY include +/// > additional information about clients to further narrow applicability or reuse. +/// +/// `TokenLog` pertains only to tokens provided in NEW_TOKEN frames. +pub trait TokenLog: Send + Sync { + /// Record that the token was used and, ideally, return a token reuse error if the token may + /// have been already used previously + /// + /// False negatives and false positives are both permissible. Called when a client uses an + /// address validation token. + /// + /// Parameters: + /// - `nonce`: A server-generated random unique value for the token. + /// - `issued`: The time the server issued the token. + /// - `lifetime`: The expiration time of address validation tokens sent via NEW_TOKEN frames, + /// as configured by [`ServerValidationTokenConfig::lifetime`][1]. + /// + /// [1]: crate::ValidationTokenConfig::lifetime + /// + /// ## Security & Performance + /// + /// To the extent that it is possible to repeatedly trigger false negatives (returning `Ok` for + /// a token which has been reused), an attacker could use the server to perform [amplification + /// attacks][2]. The QUIC specification requires that this be limited, if not prevented fully. + /// + /// A false positive (returning `Err` for a token which has never been used) is not a security + /// vulnerability; it is permissible for a `TokenLog` to always return `Err`. A false positive + /// causes the token to be ignored, which may cause the transmission of some 0.5-RTT data to be + /// delayed until the handshake completes, if a sufficient amount of 0.5-RTT data it sent. + /// + /// [2]: https://en.wikipedia.org/wiki/Denial-of-service_attack#Amplification + fn check_and_insert( + &self, + nonce: u128, + issued: SystemTime, + lifetime: Duration, + ) -> Result<(), TokenReuseError>; +} + +/// Error for when a validation token may have been reused +pub struct TokenReuseError; + +/// Null implementation of [`TokenLog`], which never accepts tokens +pub struct NoneTokenLog; + +impl TokenLog for NoneTokenLog { + fn check_and_insert(&self, _: u128, _: SystemTime, _: Duration) -> Result<(), TokenReuseError> { + Err(TokenReuseError) + } +} + +/// Responsible for storing validation tokens received from servers and retrieving them for use in +/// subsequent connections +pub trait TokenStore: Send + Sync { + /// Potentially store a token for later one-time use + /// + /// Called when a NEW_TOKEN frame is received from the server. + fn insert(&self, server_name: &str, token: Bytes); + + /// Try to find and take a token that was stored with the given server name + /// + /// The same token must never be returned from `take` twice, as doing so can be used to + /// de-anonymize a client's traffic. + /// + /// Called when trying to connect to a server. It is always ok for this to return `None`. + fn take(&self, server_name: &str) -> Option; +} + +/// Null implementation of [`TokenStore`], which does not store any tokens +pub struct NoneTokenStore; + +impl TokenStore for NoneTokenStore { + fn insert(&self, _: &str, _: Bytes) {} + fn take(&self, _: &str) -> Option { + None + } +} + +/// State in an `Incoming` determined by a token or lack thereof +#[derive(Debug)] +pub(crate) struct IncomingToken { + pub(crate) retry_src_cid: Option, + pub(crate) orig_dst_cid: ConnectionId, + pub(crate) validated: bool, +} + +impl IncomingToken { + /// Construct for an `Incoming` given the first packet header, or error if the connection + /// cannot be established + pub(crate) fn from_header( + header: &InitialHeader, + server_config: &ServerConfig, + remote_address: SocketAddr, + ) -> Result { + let unvalidated = Self { + retry_src_cid: None, + orig_dst_cid: header.dst_cid, + validated: false, + }; + + // Decode token or short-circuit + if header.token.is_empty() { + return Ok(unvalidated); + } + + // In cases where a token cannot be decrypted/decoded, we must allow for the possibility + // that this is caused not by client malfeasance, but by the token having been generated by + // an incompatible endpoint, e.g. a different version or a neighbor behind the same load + // balancer. In such cases we proceed as if there was no token. + // + // [_RFC 9000 § 8.1.3:_](https://www.rfc-editor.org/rfc/rfc9000.html#section-8.1.3-10) + // + // > If the token is invalid, then the server SHOULD proceed as if the client did not have + // > a validated address, including potentially sending a Retry packet. + let Some(retry) = Token::decode(&*server_config.token_key, &header.token) else { + return Ok(unvalidated); + }; + + // Validate token, then convert into Self + match retry.payload { + TokenPayload::Retry { + address, + orig_dst_cid, + issued, + } => { + if address != remote_address { + return Err(InvalidRetryTokenError); + } + if issued + server_config.retry_token_lifetime < server_config.time_source.now() { + return Err(InvalidRetryTokenError); + } + + Ok(Self { + retry_src_cid: Some(header.dst_cid), + orig_dst_cid, + validated: true, + }) + } + TokenPayload::Validation { ip, issued } => { + if ip != remote_address.ip() { + return Ok(unvalidated); + } + if issued + server_config.validation_token.lifetime + < server_config.time_source.now() + { + return Ok(unvalidated); + } + if server_config + .validation_token + .log + .check_and_insert(retry.nonce, issued, server_config.validation_token.lifetime) + .is_err() + { + return Ok(unvalidated); + } + + Ok(Self { + retry_src_cid: None, + orig_dst_cid: header.dst_cid, + validated: true, + }) + } + } + } +} + +/// Error for a token being unambiguously from a Retry packet, and not valid +/// +/// The connection cannot be established. +pub(crate) struct InvalidRetryTokenError; + +/// Retry or validation token +pub(crate) struct Token { + /// Content that is encrypted from the client + pub(crate) payload: TokenPayload, + /// Randomly generated value, which must be unique, and is visible to the client + nonce: u128, +} + +impl Token { + /// Construct with newly sampled randomness + pub(crate) fn new(payload: TokenPayload, rng: &mut impl Rng) -> Self { + Self { + nonce: rng.random(), + payload, + } + } + + /// Encode and encrypt + pub(crate) fn encode(&self, key: &dyn HandshakeTokenKey) -> Vec { + let mut buf = Vec::new(); + + // Encode payload + match self.payload { + TokenPayload::Retry { + address, + orig_dst_cid, + issued, + } => { + buf.put_u8(TokenType::Retry as u8); + encode_addr(&mut buf, address); + orig_dst_cid.encode_long(&mut buf); + encode_unix_secs(&mut buf, issued); + } + TokenPayload::Validation { ip, issued } => { + buf.put_u8(TokenType::Validation as u8); + encode_ip(&mut buf, ip); + encode_unix_secs(&mut buf, issued); + } + } + + // Encrypt + let aead_key = key.aead_from_hkdf(&self.nonce.to_le_bytes()); + aead_key.seal(&mut buf, &[]).unwrap(); + buf.extend(&self.nonce.to_le_bytes()); + + buf + } + + /// Decode and decrypt + fn decode(key: &dyn HandshakeTokenKey, raw_token_bytes: &[u8]) -> Option { + // Decrypt + + // MSRV: split_at_checked requires 1.80.0 + let nonce_slice_start = raw_token_bytes.len().checked_sub(size_of::())?; + let (sealed_token, nonce_bytes) = raw_token_bytes.split_at(nonce_slice_start); + + let nonce = u128::from_le_bytes(nonce_bytes.try_into().unwrap()); + + let aead_key = key.aead_from_hkdf(nonce_bytes); + let mut sealed_token = sealed_token.to_vec(); + let data = aead_key.open(&mut sealed_token, &[]).ok()?; + + // Decode payload + let mut reader = &data[..]; + let payload = match TokenType::from_byte((&mut reader).get::().ok()?)? { + TokenType::Retry => TokenPayload::Retry { + address: decode_addr(&mut reader)?, + orig_dst_cid: ConnectionId::decode_long(&mut reader)?, + issued: decode_unix_secs(&mut reader)?, + }, + TokenType::Validation => TokenPayload::Validation { + ip: decode_ip(&mut reader)?, + issued: decode_unix_secs(&mut reader)?, + }, + }; + + if !reader.is_empty() { + // Consider extra bytes a decoding error (it may be from an incompatible endpoint) + return None; + } + + Some(Self { nonce, payload }) + } +} + +/// Content of a [`Token`] that is encrypted from the client +pub(crate) enum TokenPayload { + /// Token originating from a Retry packet + Retry { + /// The client's address + address: SocketAddr, + /// The destination connection ID set in the very first packet from the client + orig_dst_cid: ConnectionId, + /// The time at which this token was issued + issued: SystemTime, + }, + /// Token originating from a NEW_TOKEN frame + Validation { + /// The client's IP address (its port is likely to change between sessions) + ip: IpAddr, + /// The time at which this token was issued + issued: SystemTime, + }, +} + +/// Variant tag for a [`TokenPayload`] +#[derive(Copy, Clone)] +#[repr(u8)] +enum TokenType { + Retry = 0, + Validation = 1, +} + +impl TokenType { + fn from_byte(n: u8) -> Option { + use TokenType::*; + [Retry, Validation].into_iter().find(|ty| *ty as u8 == n) + } +} + +fn encode_addr(buf: &mut Vec, address: SocketAddr) { + encode_ip(buf, address.ip()); + buf.put_u16(address.port()); +} + +fn decode_addr(buf: &mut B) -> Option { + let ip = decode_ip(buf)?; + let port = buf.get().ok()?; + Some(SocketAddr::new(ip, port)) +} + +fn encode_ip(buf: &mut Vec, ip: IpAddr) { + match ip { + IpAddr::V4(x) => { + buf.put_u8(0); + buf.put_slice(&x.octets()); + } + IpAddr::V6(x) => { + buf.put_u8(1); + buf.put_slice(&x.octets()); + } + } +} + +fn decode_ip(buf: &mut B) -> Option { + match buf.get::().ok()? { + 0 => buf.get().ok().map(IpAddr::V4), + 1 => buf.get().ok().map(IpAddr::V6), + _ => None, + } +} + +fn encode_unix_secs(buf: &mut Vec, time: SystemTime) { + buf.write::( + time.duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + ); +} + +fn decode_unix_secs(buf: &mut B) -> Option { + Some(UNIX_EPOCH + Duration::from_secs(buf.get::().ok()?)) +} + +/// Stateless reset token +/// +/// Used for an endpoint to securely communicate that it has lost state for a connection. +#[allow(clippy::derived_hash_with_manual_eq)] // Custom PartialEq impl matches derived semantics +#[derive(Debug, Copy, Clone, Hash)] +pub(crate) struct ResetToken([u8; RESET_TOKEN_SIZE]); + +impl ResetToken { + pub(crate) fn new(key: &dyn HmacKey, id: ConnectionId) -> Self { + let mut signature = vec![0; key.signature_len()]; + key.sign(&id, &mut signature); + // TODO: Server ID?? + let mut result = [0; RESET_TOKEN_SIZE]; + result.copy_from_slice(&signature[..RESET_TOKEN_SIZE]); + result.into() + } +} + +impl PartialEq for ResetToken { + fn eq(&self, other: &Self) -> bool { + crate::constant_time::eq(&self.0, &other.0) + } +} + +impl Eq for ResetToken {} + +impl From<[u8; RESET_TOKEN_SIZE]> for ResetToken { + fn from(x: [u8; RESET_TOKEN_SIZE]) -> Self { + Self(x) + } +} + +impl std::ops::Deref for ResetToken { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for ResetToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.iter() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +#[cfg(all(test, any(feature = "aws-lc-rs", feature = "ring")))] +mod test { + use super::*; + #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] + use aws_lc_rs::hkdf; + use rand::prelude::*; + #[cfg(feature = "ring")] + use ring::hkdf; + + fn token_round_trip(payload: TokenPayload) -> TokenPayload { + let rng = &mut rand::rng(); + let token = Token::new(payload, rng); + let mut master_key = [0; 64]; + rng.fill_bytes(&mut master_key); + let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key); + let encoded = token.encode(&prk); + let decoded = Token::decode(&prk, &encoded).expect("token didn't decrypt / decode"); + assert_eq!(token.nonce, decoded.nonce); + decoded.payload + } + + #[test] + fn retry_token_sanity() { + use crate::MAX_CID_SIZE; + use crate::cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator}; + use crate::{Duration, UNIX_EPOCH}; + + use std::net::Ipv6Addr; + + let address_1 = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433); + let orig_dst_cid_1 = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid(); + let issued_1 = UNIX_EPOCH + Duration::from_secs(42); // Fractional seconds would be lost + let payload_1 = TokenPayload::Retry { + address: address_1, + orig_dst_cid: orig_dst_cid_1, + issued: issued_1, + }; + let TokenPayload::Retry { + address: address_2, + orig_dst_cid: orig_dst_cid_2, + issued: issued_2, + } = token_round_trip(payload_1) + else { + panic!("token decoded as wrong variant"); + }; + + assert_eq!(address_1, address_2); + assert_eq!(orig_dst_cid_1, orig_dst_cid_2); + assert_eq!(issued_1, issued_2); + } + + #[test] + fn validation_token_sanity() { + use crate::{Duration, UNIX_EPOCH}; + + use std::net::Ipv6Addr; + + let ip_1 = Ipv6Addr::LOCALHOST.into(); + let issued_1 = UNIX_EPOCH + Duration::from_secs(42); // Fractional seconds would be lost + + let payload_1 = TokenPayload::Validation { + ip: ip_1, + issued: issued_1, + }; + let TokenPayload::Validation { + ip: ip_2, + issued: issued_2, + } = token_round_trip(payload_1) + else { + panic!("token decoded as wrong variant"); + }; + + assert_eq!(ip_1, ip_2); + assert_eq!(issued_1, issued_2); + } + + #[test] + fn invalid_token_returns_err() { + use super::*; + use rand::RngCore; + + let rng = &mut rand::rng(); + + let mut master_key = [0; 64]; + rng.fill_bytes(&mut master_key); + + let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key); + + let mut invalid_token = Vec::new(); + + let mut random_data = [0; 32]; + rand::rng().fill_bytes(&mut random_data); + invalid_token.put_slice(&random_data); + + // Assert: garbage sealed data returns err + assert!(Token::decode(&prk, &invalid_token).is_none()); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/token_memory_cache.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/token_memory_cache.rs new file mode 100644 index 0000000..3fce05f --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/token_memory_cache.rs @@ -0,0 +1,246 @@ +//! Storing tokens sent from servers in NEW_TOKEN frames and using them in subsequent connections + +use std::{ + collections::{HashMap, VecDeque, hash_map}, + sync::{Arc, Mutex}, +}; + +use bytes::Bytes; +use lru_slab::LruSlab; +use tracing::trace; + +use crate::token::TokenStore; + +/// `TokenStore` implementation that stores up to `N` tokens per server name for up to a +/// limited number of server names, in-memory +#[derive(Debug)] +pub struct TokenMemoryCache(Mutex); + +impl TokenMemoryCache { + /// Construct empty + pub fn new(max_server_names: u32, max_tokens_per_server: usize) -> Self { + Self(Mutex::new(State::new( + max_server_names, + max_tokens_per_server, + ))) + } +} + +impl TokenStore for TokenMemoryCache { + fn insert(&self, server_name: &str, token: Bytes) { + trace!(%server_name, "storing token"); + self.0.lock().unwrap().store(server_name, token) + } + + fn take(&self, server_name: &str) -> Option { + let token = self.0.lock().unwrap().take(server_name); + trace!(%server_name, found=%token.is_some(), "taking token"); + token + } +} + +/// Defaults to a maximum of 256 servers and 2 tokens per server +impl Default for TokenMemoryCache { + fn default() -> Self { + Self::new(256, 2) + } +} + +/// Lockable inner state of `TokenMemoryCache` +#[derive(Debug)] +struct State { + max_server_names: u32, + max_tokens_per_server: usize, + // map from server name to index in lru + lookup: HashMap, u32>, + lru: LruSlab, +} + +impl State { + fn new(max_server_names: u32, max_tokens_per_server: usize) -> Self { + Self { + max_server_names, + max_tokens_per_server, + lookup: HashMap::new(), + lru: LruSlab::default(), + } + } + + fn store(&mut self, server_name: &str, token: Bytes) { + if self.max_server_names == 0 { + // the rest of this method assumes that we can always insert a new entry so long as + // we're willing to evict a pre-existing entry. thus, an entry limit of 0 is an edge + // case we must short-circuit on now. + return; + } + if self.max_tokens_per_server == 0 { + // similarly to above, the rest of this method assumes that we can always push a new + // token to a queue so long as we're willing to evict a pre-existing token, so we + // short-circuit on the edge case of a token limit of 0. + return; + } + + let server_name = Arc::::from(server_name); + match self.lookup.entry(server_name.clone()) { + hash_map::Entry::Occupied(hmap_entry) => { + // key already exists, push the new token to its token queue + let tokens = &mut self.lru.get_mut(*hmap_entry.get()).tokens; + if tokens.len() >= self.max_tokens_per_server { + debug_assert!(tokens.len() == self.max_tokens_per_server); + tokens.pop_front().unwrap(); + } + tokens.push_back(token); + } + hash_map::Entry::Vacant(hmap_entry) => { + // key does not yet exist, create a new one, evicting the oldest if necessary + let removed_key = if self.lru.len() >= self.max_server_names { + // unwrap safety: max_server_names is > 0, so there's at least one entry, so + // lru() is some + Some(self.lru.remove(self.lru.lru().unwrap()).server_name) + } else { + None + }; + + hmap_entry.insert(self.lru.insert(CacheEntry::new(server_name, token))); + + // for borrowing reasons, we must defer removing the evicted hmap entry to here + if let Some(removed_slot) = removed_key { + let removed = self.lookup.remove(&removed_slot); + debug_assert!(removed.is_some()); + } + } + }; + } + + fn take(&mut self, server_name: &str) -> Option { + let slab_key = *self.lookup.get(server_name)?; + + // pop from entry's token queue + let entry = self.lru.get_mut(slab_key); + // unwrap safety: we never leave tokens empty + let token = entry.tokens.pop_front().unwrap(); + + if entry.tokens.is_empty() { + // token stack emptied, remove entry + self.lru.remove(slab_key); + self.lookup.remove(server_name); + } + + Some(token) + } +} + +/// Cache entry within `TokenMemoryCache`'s LRU slab +#[derive(Debug)] +struct CacheEntry { + server_name: Arc, + // invariant: tokens is never empty + tokens: VecDeque, +} + +impl CacheEntry { + /// Construct with a single token + fn new(server_name: Arc, token: Bytes) -> Self { + let mut tokens = VecDeque::new(); + tokens.push_back(token); + Self { + server_name, + tokens, + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::VecDeque; + + use super::*; + use rand::prelude::*; + use rand_pcg::Pcg32; + + fn new_rng() -> impl Rng { + Pcg32::from_seed(0xdeadbeefdeadbeefdeadbeefdeadbeefu128.to_le_bytes()) + } + + #[test] + fn cache_test() { + let mut rng = new_rng(); + const N: usize = 2; + + for _ in 0..10 { + let mut cache_1: Vec<(u32, VecDeque)> = Vec::new(); // keep it sorted oldest to newest + let cache_2 = TokenMemoryCache::new(20, 2); + + for i in 0..200 { + let server_name = rng.random::() % 10; + if rng.random_bool(0.666) { + // store + let token = Bytes::from(vec![i]); + println!("STORE {server_name} {token:?}"); + if let Some((j, _)) = cache_1 + .iter() + .enumerate() + .find(|&(_, &(server_name_2, _))| server_name_2 == server_name) + { + let (_, mut queue) = cache_1.remove(j); + queue.push_back(token.clone()); + if queue.len() > N { + queue.pop_front(); + } + cache_1.push((server_name, queue)); + } else { + let mut queue = VecDeque::new(); + queue.push_back(token.clone()); + cache_1.push((server_name, queue)); + if cache_1.len() > 20 { + cache_1.remove(0); + } + } + cache_2.insert(&server_name.to_string(), token); + } else { + // take + println!("TAKE {server_name}"); + let expecting = cache_1 + .iter() + .enumerate() + .find(|&(_, &(server_name_2, _))| server_name_2 == server_name) + .map(|(j, _)| j) + .map(|j| { + let (_, mut queue) = cache_1.remove(j); + let token = queue.pop_front().unwrap(); + if !queue.is_empty() { + cache_1.push((server_name, queue)); + } + token + }); + println!("EXPECTING {expecting:?}"); + assert_eq!(cache_2.take(&server_name.to_string()), expecting); + } + } + } + } + + #[test] + fn zero_max_server_names() { + // test that this edge case doesn't panic + let cache = TokenMemoryCache::new(0, 2); + for i in 0..10 { + cache.insert(&i.to_string(), Bytes::from(vec![i])); + for j in 0..10 { + assert!(cache.take(&j.to_string()).is_none()); + } + } + } + + #[test] + fn zero_queue_length() { + // test that this edge case doesn't panic + let cache = TokenMemoryCache::new(256, 0); + for i in 0..10 { + cache.insert(&i.to_string(), Bytes::from(vec![i])); + for j in 0..10 { + assert!(cache.take(&j.to_string()).is_none()); + } + } + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/transport_error.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/transport_error.rs new file mode 100644 index 0000000..047cd0a --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/transport_error.rs @@ -0,0 +1,132 @@ +use std::fmt; + +use bytes::{Buf, BufMut}; + +use crate::{ + coding::{self, BufExt, BufMutExt}, + frame, +}; + +/// Transport-level errors occur when a peer violates the protocol specification +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Error { + /// Type of error + pub code: Code, + /// Frame type that triggered the error + pub frame: Option, + /// Human-readable explanation of the reason + pub reason: String, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.code.fmt(f)?; + if let Some(frame) = self.frame { + write!(f, " in {frame}")?; + } + if !self.reason.is_empty() { + write!(f, ": {}", self.reason)?; + } + Ok(()) + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(x: Code) -> Self { + Self { + code: x, + frame: None, + reason: "".to_string(), + } + } +} + +/// Transport-level error code +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct Code(u64); + +impl Code { + /// Create QUIC error code from TLS alert code + pub fn crypto(code: u8) -> Self { + Self(0x100 | u64::from(code)) + } +} + +impl coding::Codec for Code { + fn decode(buf: &mut B) -> coding::Result { + Ok(Self(buf.get_var()?)) + } + fn encode(&self, buf: &mut B) { + buf.write_var(self.0) + } +} + +impl From for u64 { + fn from(x: Code) -> Self { + x.0 + } +} + +macro_rules! errors { + {$($name:ident($val:expr) $desc:expr;)*} => { + #[allow(non_snake_case, unused)] + impl Error { + $( + pub(crate) fn $name(reason: T) -> Self where T: Into { + Self { + code: Code::$name, + frame: None, + reason: reason.into(), + } + } + )* + } + + impl Code { + $(#[doc = $desc] pub const $name: Self = Code($val);)* + } + + impl fmt::Debug for Code { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + $($val => f.write_str(stringify!($name)),)* + x if (0x100..0x200).contains(&x) => write!(f, "Code::crypto({:02x})", self.0 as u8), + _ => write!(f, "Code({:x})", self.0), + } + } + } + + impl fmt::Display for Code { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + $($val => f.write_str($desc),)* + // We're trying to be abstract over the crypto protocol, so human-readable descriptions here is tricky. + _ if self.0 >= 0x100 && self.0 < 0x200 => write!(f, "the cryptographic handshake failed: error {}", self.0 & 0xFF), + _ => f.write_str("unknown error"), + } + } + } + } +} + +errors! { + NO_ERROR(0x0) "the connection is being closed abruptly in the absence of any error"; + INTERNAL_ERROR(0x1) "the endpoint encountered an internal error and cannot continue with the connection"; + CONNECTION_REFUSED(0x2) "the server refused to accept a new connection"; + FLOW_CONTROL_ERROR(0x3) "received more data than permitted in advertised data limits"; + STREAM_LIMIT_ERROR(0x4) "received a frame for a stream identifier that exceeded advertised the stream limit for the corresponding stream type"; + STREAM_STATE_ERROR(0x5) "received a frame for a stream that was not in a state that permitted that frame"; + FINAL_SIZE_ERROR(0x6) "received a STREAM frame or a RESET_STREAM frame containing a different final size to the one already established"; + FRAME_ENCODING_ERROR(0x7) "received a frame that was badly formatted"; + TRANSPORT_PARAMETER_ERROR(0x8) "received transport parameters that were badly formatted, included an invalid value, was absent even though it is mandatory, was present though it is forbidden, or is otherwise in error"; + CONNECTION_ID_LIMIT_ERROR(0x9) "the number of connection IDs provided by the peer exceeds the advertised active_connection_id_limit"; + PROTOCOL_VIOLATION(0xA) "detected an error with protocol compliance that was not covered by more specific error codes"; + INVALID_TOKEN(0xB) "received an invalid Retry Token in a client Initial"; + APPLICATION_ERROR(0xC) "the application or application protocol caused the connection to be closed during the handshake"; + CRYPTO_BUFFER_EXCEEDED(0xD) "received more data in CRYPTO frames than can be buffered"; + KEY_UPDATE_ERROR(0xE) "key update error"; + AEAD_LIMIT_REACHED(0xF) "the endpoint has reached the confidentiality or integrity limit for the AEAD algorithm"; + NO_VIABLE_PATH(0x10) "no viable network path exists"; +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/transport_parameters.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/transport_parameters.rs new file mode 100644 index 0000000..67e48bd --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/transport_parameters.rs @@ -0,0 +1,874 @@ +//! QUIC connection transport parameters +//! +//! The `TransportParameters` type is used to represent the transport parameters +//! negotiated by peers while establishing a QUIC connection. This process +//! happens as part of the establishment of the TLS session. As such, the types +//! contained in this modules should generally only be referred to by custom +//! implementations of the `crypto::Session` trait. + +use std::{ + convert::TryFrom, + net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}, +}; + +use bytes::{Buf, BufMut}; +use rand::{Rng as _, RngCore, seq::SliceRandom as _}; +use thiserror::Error; + +use crate::{ + LOC_CID_COUNT, MAX_CID_SIZE, MAX_STREAM_COUNT, RESET_TOKEN_SIZE, ResetToken, Side, + TIMER_GRANULARITY, TransportError, VarInt, + cid_generator::ConnectionIdGenerator, + cid_queue::CidQueue, + coding::{BufExt, BufMutExt, UnexpectedEnd}, + config::{EndpointConfig, ServerConfig, TransportConfig}, + shared::ConnectionId, +}; + +// Apply a given macro to a list of all the transport parameters having integer types, along with +// their codes and default values. Using this helps us avoid error-prone duplication of the +// contained information across decoding, encoding, and the `Default` impl. Whenever we want to do +// something with transport parameters, we'll handle the bulk of cases by writing a macro that +// takes a list of arguments in this form, then passing it to this macro. +macro_rules! apply_params { + ($macro:ident) => { + $macro! { + // #[doc] name (id) = default, + /// Milliseconds, disabled if zero + max_idle_timeout(MaxIdleTimeout) = 0, + /// Limits the size of UDP payloads that the endpoint is willing to receive + max_udp_payload_size(MaxUdpPayloadSize) = 65527, + + /// Initial value for the maximum amount of data that can be sent on the connection + initial_max_data(InitialMaxData) = 0, + /// Initial flow control limit for locally-initiated bidirectional streams + initial_max_stream_data_bidi_local(InitialMaxStreamDataBidiLocal) = 0, + /// Initial flow control limit for peer-initiated bidirectional streams + initial_max_stream_data_bidi_remote(InitialMaxStreamDataBidiRemote) = 0, + /// Initial flow control limit for unidirectional streams + initial_max_stream_data_uni(InitialMaxStreamDataUni) = 0, + + /// Initial maximum number of bidirectional streams the peer may initiate + initial_max_streams_bidi(InitialMaxStreamsBidi) = 0, + /// Initial maximum number of unidirectional streams the peer may initiate + initial_max_streams_uni(InitialMaxStreamsUni) = 0, + + /// Exponent used to decode the ACK Delay field in the ACK frame + ack_delay_exponent(AckDelayExponent) = 3, + /// Maximum amount of time in milliseconds by which the endpoint will delay sending + /// acknowledgments + max_ack_delay(MaxAckDelay) = 25, + /// Maximum number of connection IDs from the peer that an endpoint is willing to store + active_connection_id_limit(ActiveConnectionIdLimit) = 2, + } + }; +} + +macro_rules! make_struct { + {$($(#[$doc:meta])* $name:ident ($id:ident) = $default:expr,)*} => { + /// Transport parameters used to negotiate connection-level preferences between peers + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub struct TransportParameters { + $($(#[$doc])* pub(crate) $name : VarInt,)* + + /// Does the endpoint support active connection migration + pub(crate) disable_active_migration: bool, + /// Maximum size for datagram frames + pub(crate) max_datagram_frame_size: Option, + /// The value that the endpoint included in the Source Connection ID field of the first + /// Initial packet it sends for the connection + pub(crate) initial_src_cid: Option, + /// The endpoint is willing to receive QUIC packets containing any value for the fixed + /// bit + pub(crate) grease_quic_bit: bool, + + /// Minimum amount of time in microseconds by which the endpoint is able to delay + /// sending acknowledgments + /// + /// If a value is provided, it implies that the endpoint supports QUIC Acknowledgement + /// Frequency + pub(crate) min_ack_delay: Option, + + // Server-only + /// The value of the Destination Connection ID field from the first Initial packet sent + /// by the client + pub(crate) original_dst_cid: Option, + /// The value that the server included in the Source Connection ID field of a Retry + /// packet + pub(crate) retry_src_cid: Option, + /// Token used by the client to verify a stateless reset from the server + pub(crate) stateless_reset_token: Option, + /// The server's preferred address for communication after handshake completion + pub(crate) preferred_address: Option, + /// The randomly generated reserved transport parameter to sustain future extensibility + /// of transport parameter extensions. + /// When present, it is included during serialization but ignored during deserialization. + pub(crate) grease_transport_parameter: Option, + + /// Defines the order in which transport parameters are serialized. + /// + /// This field is initialized only for outgoing `TransportParameters` instances and + /// is set to `None` for `TransportParameters` received from a peer. + pub(crate) write_order: Option<[u8; TransportParameterId::SUPPORTED.len()]>, + } + + // We deliberately don't implement the `Default` trait, since that would be public, and + // downstream crates should never construct `TransportParameters` except by decoding those + // supplied by a peer. + impl TransportParameters { + /// Standard defaults, used if the peer does not supply a given parameter. + pub(crate) fn default() -> Self { + Self { + $($name: VarInt::from_u32($default),)* + + disable_active_migration: false, + max_datagram_frame_size: None, + initial_src_cid: None, + grease_quic_bit: false, + min_ack_delay: None, + + original_dst_cid: None, + retry_src_cid: None, + stateless_reset_token: None, + preferred_address: None, + grease_transport_parameter: None, + write_order: None, + } + } + } + } +} + +apply_params!(make_struct); + +impl TransportParameters { + pub(crate) fn new( + config: &TransportConfig, + endpoint_config: &EndpointConfig, + cid_gen: &dyn ConnectionIdGenerator, + initial_src_cid: ConnectionId, + server_config: Option<&ServerConfig>, + rng: &mut impl RngCore, + ) -> Self { + Self { + initial_src_cid: Some(initial_src_cid), + initial_max_streams_bidi: config.max_concurrent_bidi_streams, + initial_max_streams_uni: config.max_concurrent_uni_streams, + initial_max_data: config.receive_window, + initial_max_stream_data_bidi_local: config.stream_receive_window, + initial_max_stream_data_bidi_remote: config.stream_receive_window, + initial_max_stream_data_uni: config.stream_receive_window, + max_udp_payload_size: endpoint_config.max_udp_payload_size, + max_idle_timeout: config.max_idle_timeout.unwrap_or(VarInt(0)), + disable_active_migration: server_config.is_some_and(|c| !c.migration), + active_connection_id_limit: if cid_gen.cid_len() == 0 { + 2 // i.e. default, i.e. unsent + } else { + CidQueue::LEN as u32 + } + .into(), + max_datagram_frame_size: config + .datagram_receive_buffer_size + .map(|x| (x.min(u16::MAX.into()) as u16).into()), + grease_quic_bit: endpoint_config.grease_quic_bit, + min_ack_delay: Some( + VarInt::from_u64(u64::try_from(TIMER_GRANULARITY.as_micros()).unwrap()).unwrap(), + ), + grease_transport_parameter: Some(ReservedTransportParameter::random(rng)), + write_order: Some({ + let mut order = std::array::from_fn(|i| i as u8); + order.shuffle(rng); + order + }), + ..Self::default() + } + } + + /// Check that these parameters are legal when resuming from + /// certain cached parameters + pub(crate) fn validate_resumption_from(&self, cached: &Self) -> Result<(), TransportError> { + if cached.active_connection_id_limit > self.active_connection_id_limit + || cached.initial_max_data > self.initial_max_data + || cached.initial_max_stream_data_bidi_local > self.initial_max_stream_data_bidi_local + || cached.initial_max_stream_data_bidi_remote > self.initial_max_stream_data_bidi_remote + || cached.initial_max_stream_data_uni > self.initial_max_stream_data_uni + || cached.initial_max_streams_bidi > self.initial_max_streams_bidi + || cached.initial_max_streams_uni > self.initial_max_streams_uni + || cached.max_datagram_frame_size > self.max_datagram_frame_size + || cached.grease_quic_bit && !self.grease_quic_bit + { + return Err(TransportError::PROTOCOL_VIOLATION( + "0-RTT accepted with incompatible transport parameters", + )); + } + Ok(()) + } + + /// Maximum number of CIDs to issue to this peer + /// + /// Consider both a) the active_connection_id_limit from the other end; and + /// b) LOC_CID_COUNT used locally + pub(crate) fn issue_cids_limit(&self) -> u64 { + self.active_connection_id_limit.0.min(LOC_CID_COUNT) + } +} + +/// A server's preferred address +/// +/// This is communicated as a transport parameter during TLS session establishment. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) struct PreferredAddress { + pub(crate) address_v4: Option, + pub(crate) address_v6: Option, + pub(crate) connection_id: ConnectionId, + pub(crate) stateless_reset_token: ResetToken, +} + +impl PreferredAddress { + fn wire_size(&self) -> u16 { + 4 + 2 + 16 + 2 + 1 + self.connection_id.len() as u16 + 16 + } + + fn write(&self, w: &mut W) { + w.write(self.address_v4.map_or(Ipv4Addr::UNSPECIFIED, |x| *x.ip())); + w.write::(self.address_v4.map_or(0, |x| x.port())); + w.write(self.address_v6.map_or(Ipv6Addr::UNSPECIFIED, |x| *x.ip())); + w.write::(self.address_v6.map_or(0, |x| x.port())); + w.write::(self.connection_id.len() as u8); + w.put_slice(&self.connection_id); + w.put_slice(&self.stateless_reset_token); + } + + fn read(r: &mut R) -> Result { + let ip_v4 = r.get::()?; + let port_v4 = r.get::()?; + let ip_v6 = r.get::()?; + let port_v6 = r.get::()?; + let cid_len = r.get::()?; + if r.remaining() < cid_len as usize || cid_len > MAX_CID_SIZE as u8 { + return Err(Error::Malformed); + } + let mut stage = [0; MAX_CID_SIZE]; + r.copy_to_slice(&mut stage[0..cid_len as usize]); + let cid = ConnectionId::new(&stage[0..cid_len as usize]); + if r.remaining() < 16 { + return Err(Error::Malformed); + } + let mut token = [0; RESET_TOKEN_SIZE]; + r.copy_to_slice(&mut token); + let address_v4 = if ip_v4.is_unspecified() && port_v4 == 0 { + None + } else { + Some(SocketAddrV4::new(ip_v4, port_v4)) + }; + let address_v6 = if ip_v6.is_unspecified() && port_v6 == 0 { + None + } else { + Some(SocketAddrV6::new(ip_v6, port_v6, 0, 0)) + }; + if address_v4.is_none() && address_v6.is_none() { + return Err(Error::IllegalValue); + } + Ok(Self { + address_v4, + address_v6, + connection_id: cid, + stateless_reset_token: token.into(), + }) + } +} + +/// Errors encountered while decoding `TransportParameters` +#[derive(Debug, Copy, Clone, Eq, PartialEq, Error)] +pub enum Error { + /// Parameters that are semantically invalid + #[error("parameter had illegal value")] + IllegalValue, + /// Catch-all error for problems while decoding transport parameters + #[error("parameters were malformed")] + Malformed, +} + +impl From for TransportError { + fn from(e: Error) -> Self { + match e { + Error::IllegalValue => Self::TRANSPORT_PARAMETER_ERROR("illegal value"), + Error::Malformed => Self::TRANSPORT_PARAMETER_ERROR("malformed"), + } + } +} + +impl From for Error { + fn from(_: UnexpectedEnd) -> Self { + Self::Malformed + } +} + +impl TransportParameters { + /// Encode `TransportParameters` into buffer + pub fn write(&self, w: &mut W) { + for idx in self + .write_order + .as_ref() + .unwrap_or(&std::array::from_fn(|i| i as u8)) + { + let id = TransportParameterId::SUPPORTED[*idx as usize]; + match id { + TransportParameterId::ReservedTransportParameter => { + if let Some(param) = self.grease_transport_parameter { + param.write(w); + } + } + TransportParameterId::StatelessResetToken => { + if let Some(ref x) = self.stateless_reset_token { + w.write_var(id as u64); + w.write_var(16); + w.put_slice(x); + } + } + TransportParameterId::DisableActiveMigration => { + if self.disable_active_migration { + w.write_var(id as u64); + w.write_var(0); + } + } + TransportParameterId::MaxDatagramFrameSize => { + if let Some(x) = self.max_datagram_frame_size { + w.write_var(id as u64); + w.write_var(x.size() as u64); + w.write(x); + } + } + TransportParameterId::PreferredAddress => { + if let Some(ref x) = self.preferred_address { + w.write_var(id as u64); + w.write_var(x.wire_size() as u64); + x.write(w); + } + } + TransportParameterId::OriginalDestinationConnectionId => { + if let Some(ref cid) = self.original_dst_cid { + w.write_var(id as u64); + w.write_var(cid.len() as u64); + w.put_slice(cid); + } + } + TransportParameterId::InitialSourceConnectionId => { + if let Some(ref cid) = self.initial_src_cid { + w.write_var(id as u64); + w.write_var(cid.len() as u64); + w.put_slice(cid); + } + } + TransportParameterId::RetrySourceConnectionId => { + if let Some(ref cid) = self.retry_src_cid { + w.write_var(id as u64); + w.write_var(cid.len() as u64); + w.put_slice(cid); + } + } + TransportParameterId::GreaseQuicBit => { + if self.grease_quic_bit { + w.write_var(id as u64); + w.write_var(0); + } + } + TransportParameterId::MinAckDelayDraft07 => { + if let Some(x) = self.min_ack_delay { + w.write_var(id as u64); + w.write_var(x.size() as u64); + w.write(x); + } + } + id => { + macro_rules! write_params { + {$($(#[$doc:meta])* $name:ident ($id:ident) = $default:expr,)*} => { + match id { + $(TransportParameterId::$id => { + if self.$name.0 != $default { + w.write_var(id as u64); + w.write(VarInt::try_from(self.$name.size()).unwrap()); + w.write(self.$name); + } + })*, + _ => { + unimplemented!("Missing implementation of write for transport parameter with code {id:?}"); + } + } + } + } + apply_params!(write_params); + } + } + } + } + + /// Decode `TransportParameters` from buffer + pub fn read(side: Side, r: &mut R) -> Result { + // Initialize to protocol-specified defaults + let mut params = Self::default(); + + // State to check for duplicate transport parameters. + macro_rules! param_state { + {$($(#[$doc:meta])* $name:ident ($id:ident) = $default:expr,)*} => {{ + struct ParamState { + $($name: bool,)* + } + + ParamState { + $($name: false,)* + } + }} + } + let mut got = apply_params!(param_state); + + while r.has_remaining() { + let id = r.get_var()?; + let len = r.get_var()?; + if (r.remaining() as u64) < len { + return Err(Error::Malformed); + } + let len = len as usize; + let Ok(id) = TransportParameterId::try_from(id) else { + // unknown transport parameters are ignored + r.advance(len); + continue; + }; + + match id { + TransportParameterId::OriginalDestinationConnectionId => { + decode_cid(len, &mut params.original_dst_cid, r)? + } + TransportParameterId::StatelessResetToken => { + if len != 16 || params.stateless_reset_token.is_some() { + return Err(Error::Malformed); + } + let mut tok = [0; RESET_TOKEN_SIZE]; + r.copy_to_slice(&mut tok); + params.stateless_reset_token = Some(tok.into()); + } + TransportParameterId::DisableActiveMigration => { + if len != 0 || params.disable_active_migration { + return Err(Error::Malformed); + } + params.disable_active_migration = true; + } + TransportParameterId::PreferredAddress => { + if params.preferred_address.is_some() { + return Err(Error::Malformed); + } + params.preferred_address = Some(PreferredAddress::read(&mut r.take(len))?); + } + TransportParameterId::InitialSourceConnectionId => { + decode_cid(len, &mut params.initial_src_cid, r)? + } + TransportParameterId::RetrySourceConnectionId => { + decode_cid(len, &mut params.retry_src_cid, r)? + } + TransportParameterId::MaxDatagramFrameSize => { + if len > 8 || params.max_datagram_frame_size.is_some() { + return Err(Error::Malformed); + } + params.max_datagram_frame_size = Some(r.get()?); + } + TransportParameterId::GreaseQuicBit => match len { + 0 => params.grease_quic_bit = true, + _ => return Err(Error::Malformed), + }, + TransportParameterId::MinAckDelayDraft07 => params.min_ack_delay = Some(r.get()?), + _ => { + macro_rules! parse { + {$($(#[$doc:meta])* $name:ident ($id:ident) = $default:expr,)*} => { + match id { + $(TransportParameterId::$id => { + let value = r.get::()?; + if len != value.size() || got.$name { return Err(Error::Malformed); } + params.$name = value.into(); + got.$name = true; + })* + _ => r.advance(len), + } + } + } + apply_params!(parse); + } + } + } + + // Semantic validation + + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.26.1 + if params.ack_delay_exponent.0 > 20 + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.28.1 + || params.max_ack_delay.0 >= 1 << 14 + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-6.2.1 + || params.active_connection_id_limit.0 < 2 + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.10.1 + || params.max_udp_payload_size.0 < 1200 + // https://www.rfc-editor.org/rfc/rfc9000.html#section-4.6-2 + || params.initial_max_streams_bidi.0 > MAX_STREAM_COUNT + || params.initial_max_streams_uni.0 > MAX_STREAM_COUNT + // https://www.ietf.org/archive/id/draft-ietf-quic-ack-frequency-08.html#section-3-4 + || params.min_ack_delay.is_some_and(|min_ack_delay| { + // min_ack_delay uses microseconds, whereas max_ack_delay uses milliseconds + min_ack_delay.0 > params.max_ack_delay.0 * 1_000 + }) + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-8 + || (side.is_server() + && (params.original_dst_cid.is_some() + || params.preferred_address.is_some() + || params.retry_src_cid.is_some() + || params.stateless_reset_token.is_some())) + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.38.1 + || params + .preferred_address.is_some_and(|x| x.connection_id.is_empty()) + { + return Err(Error::IllegalValue); + } + + Ok(params) + } +} + +/// A reserved transport parameter. +/// +/// It has an identifier of the form 31 * N + 27 for the integer value of N. +/// Such identifiers are reserved to exercise the requirement that unknown transport parameters be ignored. +/// The reserved transport parameter has no semantics and can carry arbitrary values. +/// It may be included in transport parameters sent to the peer, and should be ignored when received. +/// +/// See spec: +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) struct ReservedTransportParameter { + /// The reserved identifier of the transport parameter + id: VarInt, + + /// Buffer to store the parameter payload + payload: [u8; Self::MAX_PAYLOAD_LEN], + + /// The number of bytes to include in the wire format from the `payload` buffer + payload_len: usize, +} + +impl ReservedTransportParameter { + /// Generates a transport parameter with a random payload and a reserved ID. + /// + /// The implementation is inspired by quic-go and quiche: + /// 1. + /// 2. + fn random(rng: &mut impl RngCore) -> Self { + let id = Self::generate_reserved_id(rng); + + let payload_len = rng.random_range(0..Self::MAX_PAYLOAD_LEN); + + let payload = { + let mut slice = [0u8; Self::MAX_PAYLOAD_LEN]; + rng.fill_bytes(&mut slice[..payload_len]); + slice + }; + + Self { + id, + payload, + payload_len, + } + } + + fn write(&self, w: &mut impl BufMut) { + w.write_var(self.id.0); + w.write_var(self.payload_len as u64); + w.put_slice(&self.payload[..self.payload_len]); + } + + /// Generates a random reserved identifier of the form `31 * N + 27`, as required by RFC 9000. + /// Reserved transport parameter identifiers are used to test compliance with the requirement + /// that unknown transport parameters must be ignored by peers. + /// See: and + fn generate_reserved_id(rng: &mut impl RngCore) -> VarInt { + let id = { + let rand = rng.random_range(0u64..(1 << 62) - 27); + let n = rand / 31; + 31 * n + 27 + }; + debug_assert!( + id % 31 == 27, + "generated id does not have the form of 31 * N + 27" + ); + VarInt::from_u64(id).expect( + "generated id does fit into range of allowed transport parameter IDs: [0; 2^62)", + ) + } + + /// The maximum length of the payload to include as the parameter payload. + /// This value is not a specification-imposed limit but is chosen to match + /// the limit used by other implementations of QUIC, e.g., quic-go and quiche. + const MAX_PAYLOAD_LEN: usize = 16; +} + +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TransportParameterId { + // https://www.rfc-editor.org/rfc/rfc9000.html#iana-tp-table + OriginalDestinationConnectionId = 0x00, + MaxIdleTimeout = 0x01, + StatelessResetToken = 0x02, + MaxUdpPayloadSize = 0x03, + InitialMaxData = 0x04, + InitialMaxStreamDataBidiLocal = 0x05, + InitialMaxStreamDataBidiRemote = 0x06, + InitialMaxStreamDataUni = 0x07, + InitialMaxStreamsBidi = 0x08, + InitialMaxStreamsUni = 0x09, + AckDelayExponent = 0x0A, + MaxAckDelay = 0x0B, + DisableActiveMigration = 0x0C, + PreferredAddress = 0x0D, + ActiveConnectionIdLimit = 0x0E, + InitialSourceConnectionId = 0x0F, + RetrySourceConnectionId = 0x10, + + // Smallest possible ID of reserved transport parameter https://datatracker.ietf.org/doc/html/rfc9000#section-22.3 + ReservedTransportParameter = 0x1B, + + // https://www.rfc-editor.org/rfc/rfc9221.html#section-3 + MaxDatagramFrameSize = 0x20, + + // https://www.rfc-editor.org/rfc/rfc9287.html#section-3 + GreaseQuicBit = 0x2AB2, + + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-ack-frequency#section-10.1 + MinAckDelayDraft07 = 0xFF04DE1B, +} + +impl TransportParameterId { + /// Array with all supported transport parameter IDs + const SUPPORTED: [Self; 21] = [ + Self::MaxIdleTimeout, + Self::MaxUdpPayloadSize, + Self::InitialMaxData, + Self::InitialMaxStreamDataBidiLocal, + Self::InitialMaxStreamDataBidiRemote, + Self::InitialMaxStreamDataUni, + Self::InitialMaxStreamsBidi, + Self::InitialMaxStreamsUni, + Self::AckDelayExponent, + Self::MaxAckDelay, + Self::ActiveConnectionIdLimit, + Self::ReservedTransportParameter, + Self::StatelessResetToken, + Self::DisableActiveMigration, + Self::MaxDatagramFrameSize, + Self::PreferredAddress, + Self::OriginalDestinationConnectionId, + Self::InitialSourceConnectionId, + Self::RetrySourceConnectionId, + Self::GreaseQuicBit, + Self::MinAckDelayDraft07, + ]; +} + +impl std::cmp::PartialEq for TransportParameterId { + fn eq(&self, other: &u64) -> bool { + *other == (*self as u64) + } +} + +impl TryFrom for TransportParameterId { + type Error = (); + + fn try_from(value: u64) -> Result { + let param = match value { + id if Self::MaxIdleTimeout == id => Self::MaxIdleTimeout, + id if Self::MaxUdpPayloadSize == id => Self::MaxUdpPayloadSize, + id if Self::InitialMaxData == id => Self::InitialMaxData, + id if Self::InitialMaxStreamDataBidiLocal == id => Self::InitialMaxStreamDataBidiLocal, + id if Self::InitialMaxStreamDataBidiRemote == id => { + Self::InitialMaxStreamDataBidiRemote + } + id if Self::InitialMaxStreamDataUni == id => Self::InitialMaxStreamDataUni, + id if Self::InitialMaxStreamsBidi == id => Self::InitialMaxStreamsBidi, + id if Self::InitialMaxStreamsUni == id => Self::InitialMaxStreamsUni, + id if Self::AckDelayExponent == id => Self::AckDelayExponent, + id if Self::MaxAckDelay == id => Self::MaxAckDelay, + id if Self::ActiveConnectionIdLimit == id => Self::ActiveConnectionIdLimit, + id if Self::ReservedTransportParameter == id => Self::ReservedTransportParameter, + id if Self::StatelessResetToken == id => Self::StatelessResetToken, + id if Self::DisableActiveMigration == id => Self::DisableActiveMigration, + id if Self::MaxDatagramFrameSize == id => Self::MaxDatagramFrameSize, + id if Self::PreferredAddress == id => Self::PreferredAddress, + id if Self::OriginalDestinationConnectionId == id => { + Self::OriginalDestinationConnectionId + } + id if Self::InitialSourceConnectionId == id => Self::InitialSourceConnectionId, + id if Self::RetrySourceConnectionId == id => Self::RetrySourceConnectionId, + id if Self::GreaseQuicBit == id => Self::GreaseQuicBit, + id if Self::MinAckDelayDraft07 == id => Self::MinAckDelayDraft07, + _ => return Err(()), + }; + Ok(param) + } +} + +fn decode_cid(len: usize, value: &mut Option, r: &mut impl Buf) -> Result<(), Error> { + if len > MAX_CID_SIZE || value.is_some() || r.remaining() < len { + return Err(Error::Malformed); + } + + *value = Some(ConnectionId::from_buf(r, len)); + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn coding() { + let mut buf = Vec::new(); + let params = TransportParameters { + initial_src_cid: Some(ConnectionId::new(&[])), + original_dst_cid: Some(ConnectionId::new(&[])), + initial_max_streams_bidi: 16u32.into(), + initial_max_streams_uni: 16u32.into(), + ack_delay_exponent: 2u32.into(), + max_udp_payload_size: 1200u32.into(), + preferred_address: Some(PreferredAddress { + address_v4: Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 42)), + address_v6: Some(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 24, 0, 0)), + connection_id: ConnectionId::new(&[0x42]), + stateless_reset_token: [0xab; RESET_TOKEN_SIZE].into(), + }), + grease_quic_bit: true, + min_ack_delay: Some(2_000u32.into()), + ..TransportParameters::default() + }; + params.write(&mut buf); + assert_eq!( + TransportParameters::read(Side::Client, &mut buf.as_slice()).unwrap(), + params + ); + } + + #[test] + fn reserved_transport_parameter_generate_reserved_id() { + let mut rngs = [ + StepRng(0), + StepRng(1), + StepRng(27), + StepRng(31), + StepRng(u32::MAX as u64), + StepRng(u32::MAX as u64 - 1), + StepRng(u32::MAX as u64 + 1), + StepRng(u32::MAX as u64 - 27), + StepRng(u32::MAX as u64 + 27), + StepRng(u32::MAX as u64 - 31), + StepRng(u32::MAX as u64 + 31), + StepRng(u64::MAX), + StepRng(u64::MAX - 1), + StepRng(u64::MAX - 27), + StepRng(u64::MAX - 31), + StepRng(1 << 62), + StepRng((1 << 62) - 1), + StepRng((1 << 62) + 1), + StepRng((1 << 62) - 27), + StepRng((1 << 62) + 27), + StepRng((1 << 62) - 31), + StepRng((1 << 62) + 31), + ]; + for rng in &mut rngs { + let id = ReservedTransportParameter::generate_reserved_id(rng); + assert!(id.0 % 31 == 27) + } + } + + struct StepRng(u64); + + impl RngCore for StepRng { + #[inline] + fn next_u32(&mut self) -> u32 { + self.next_u64() as u32 + } + + #[inline] + fn next_u64(&mut self) -> u64 { + let res = self.0; + self.0 = self.0.wrapping_add(1); + res + } + + #[inline] + fn fill_bytes(&mut self, dst: &mut [u8]) { + let mut left = dst; + while left.len() >= 8 { + let (l, r) = left.split_at_mut(8); + left = r; + l.copy_from_slice(&self.next_u64().to_le_bytes()); + } + let n = left.len(); + if n > 0 { + left.copy_from_slice(&self.next_u32().to_le_bytes()[..n]); + } + } + } + + #[test] + fn reserved_transport_parameter_ignored_when_read() { + let mut buf = Vec::new(); + let reserved_parameter = ReservedTransportParameter::random(&mut rand::rng()); + assert!(reserved_parameter.payload_len < ReservedTransportParameter::MAX_PAYLOAD_LEN); + assert!(reserved_parameter.id.0 % 31 == 27); + + reserved_parameter.write(&mut buf); + assert!(!buf.is_empty()); + let read_params = TransportParameters::read(Side::Server, &mut buf.as_slice()).unwrap(); + assert_eq!(read_params, TransportParameters::default()); + } + + #[test] + fn read_semantic_validation() { + #[allow(clippy::type_complexity)] + let illegal_params_builders: Vec> = vec![ + Box::new(|t| { + // This min_ack_delay is bigger than max_ack_delay! + let min_ack_delay = t.max_ack_delay.0 * 1_000 + 1; + t.min_ack_delay = Some(VarInt::from_u64(min_ack_delay).unwrap()) + }), + Box::new(|t| { + // Preferred address can only be sent by senders (and we are reading the transport + // params as a client) + t.preferred_address = Some(PreferredAddress { + address_v4: Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 42)), + address_v6: None, + connection_id: ConnectionId::new(&[]), + stateless_reset_token: [0xab; RESET_TOKEN_SIZE].into(), + }) + }), + ]; + + for mut builder in illegal_params_builders { + let mut buf = Vec::new(); + let mut params = TransportParameters::default(); + builder(&mut params); + params.write(&mut buf); + + assert_eq!( + TransportParameters::read(Side::Server, &mut buf.as_slice()), + Err(Error::IllegalValue) + ); + } + } + + #[test] + fn resumption_params_validation() { + let high_limit = TransportParameters { + initial_max_streams_uni: 32u32.into(), + ..TransportParameters::default() + }; + let low_limit = TransportParameters { + initial_max_streams_uni: 16u32.into(), + ..TransportParameters::default() + }; + high_limit.validate_resumption_from(&low_limit).unwrap(); + low_limit.validate_resumption_from(&high_limit).unwrap_err(); + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn-proto/src/varint.rs b/vendor/flutter_quic/rust/vendor/quinn-proto/src/varint.rs new file mode 100644 index 0000000..220228f --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn-proto/src/varint.rs @@ -0,0 +1,193 @@ +use std::{convert::TryInto, fmt}; + +use bytes::{Buf, BufMut}; +use thiserror::Error; + +use crate::coding::{self, Codec, UnexpectedEnd}; + +#[cfg(feature = "arbitrary")] +use arbitrary::Arbitrary; + +/// An integer less than 2^62 +/// +/// Values of this type are suitable for encoding as QUIC variable-length integer. +// It would be neat if we could express to Rust that the top two bits are available for use as enum +// discriminants +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct VarInt(pub(crate) u64); + +impl VarInt { + /// The largest representable value + pub const MAX: Self = Self((1 << 62) - 1); + /// The largest encoded value length + pub const MAX_SIZE: usize = 8; + + /// Construct a `VarInt` infallibly + pub const fn from_u32(x: u32) -> Self { + Self(x as u64) + } + + /// Succeeds iff `x` < 2^62 + pub fn from_u64(x: u64) -> Result { + if x < 2u64.pow(62) { + Ok(Self(x)) + } else { + Err(VarIntBoundsExceeded) + } + } + + /// Create a VarInt without ensuring it's in range + /// + /// # Safety + /// + /// `x` must be less than 2^62. + pub const unsafe fn from_u64_unchecked(x: u64) -> Self { + Self(x) + } + + /// Extract the integer value + pub const fn into_inner(self) -> u64 { + self.0 + } + + /// Compute the number of bytes needed to encode this value + pub(crate) const fn size(self) -> usize { + let x = self.0; + if x < 2u64.pow(6) { + 1 + } else if x < 2u64.pow(14) { + 2 + } else if x < 2u64.pow(30) { + 4 + } else if x < 2u64.pow(62) { + 8 + } else { + panic!("malformed VarInt"); + } + } +} + +impl From for u64 { + fn from(x: VarInt) -> Self { + x.0 + } +} + +impl From for VarInt { + fn from(x: u8) -> Self { + Self(x.into()) + } +} + +impl From for VarInt { + fn from(x: u16) -> Self { + Self(x.into()) + } +} + +impl From for VarInt { + fn from(x: u32) -> Self { + Self(x.into()) + } +} + +impl std::convert::TryFrom for VarInt { + type Error = VarIntBoundsExceeded; + /// Succeeds iff `x` < 2^62 + fn try_from(x: u64) -> Result { + Self::from_u64(x) + } +} + +impl std::convert::TryFrom for VarInt { + type Error = VarIntBoundsExceeded; + /// Succeeds iff `x` < 2^62 + fn try_from(x: u128) -> Result { + Self::from_u64(x.try_into().map_err(|_| VarIntBoundsExceeded)?) + } +} + +impl std::convert::TryFrom for VarInt { + type Error = VarIntBoundsExceeded; + /// Succeeds iff `x` < 2^62 + fn try_from(x: usize) -> Result { + Self::try_from(x as u64) + } +} + +impl fmt::Debug for VarInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for VarInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "arbitrary")] +impl<'arbitrary> Arbitrary<'arbitrary> for VarInt { + fn arbitrary(u: &mut arbitrary::Unstructured<'arbitrary>) -> arbitrary::Result { + Ok(Self(u.int_in_range(0..=Self::MAX.0)?)) + } +} + +/// Error returned when constructing a `VarInt` from a value >= 2^62 +#[derive(Debug, Copy, Clone, Eq, PartialEq, Error)] +#[error("value too large for varint encoding")] +pub struct VarIntBoundsExceeded; + +impl Codec for VarInt { + fn decode(r: &mut B) -> coding::Result { + if !r.has_remaining() { + return Err(UnexpectedEnd); + } + let mut buf = [0; 8]; + buf[0] = r.get_u8(); + let tag = buf[0] >> 6; + buf[0] &= 0b0011_1111; + let x = match tag { + 0b00 => u64::from(buf[0]), + 0b01 => { + if r.remaining() < 1 { + return Err(UnexpectedEnd); + } + r.copy_to_slice(&mut buf[1..2]); + u64::from(u16::from_be_bytes(buf[..2].try_into().unwrap())) + } + 0b10 => { + if r.remaining() < 3 { + return Err(UnexpectedEnd); + } + r.copy_to_slice(&mut buf[1..4]); + u64::from(u32::from_be_bytes(buf[..4].try_into().unwrap())) + } + 0b11 => { + if r.remaining() < 7 { + return Err(UnexpectedEnd); + } + r.copy_to_slice(&mut buf[1..8]); + u64::from_be_bytes(buf) + } + _ => unreachable!(), + }; + Ok(Self(x)) + } + + fn encode(&self, w: &mut B) { + let x = self.0; + if x < 2u64.pow(6) { + w.put_u8(x as u8); + } else if x < 2u64.pow(14) { + w.put_u16((0b01 << 14) | x as u16); + } else if x < 2u64.pow(30) { + w.put_u32((0b10 << 30) | x as u32); + } else if x < 2u64.pow(62) { + w.put_u64((0b11 << 62) | x); + } else { + unreachable!("malformed VarInt") + } + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/.cargo-ok b/vendor/flutter_quic/rust/vendor/quinn/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/vendor/quinn/.cargo_vcs_info.json b/vendor/flutter_quic/rust/vendor/quinn/.cargo_vcs_info.json new file mode 100644 index 0000000..57460c3 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "b2b930a0662b18b2e351264a21e175478bb3c3f1" + }, + "path_in_vcs": "quinn" +} \ No newline at end of file diff --git a/vendor/flutter_quic/rust/vendor/quinn/Cargo.lock b/vendor/flutter_quic/rust/vendor/quinn/Cargo.lock new file mode 100644 index 0000000..3b02076 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/Cargo.lock @@ -0,0 +1,2644 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.8", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix 1.0.8", +] + +[[package]] +name = "async-signal" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e5a7965cc9d58c56234d346c9c89b824c4c8652b6f047b3bd0a777c0644f" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bencher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastbloom" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18c1ddb9231d8554c2d6bdf4cfaabf0c59251658c68b6c95cd52dd0c513a912a" +dependencies = [ + "getrandom 0.3.3", + "libm", + "rand", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qlog" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f15b83c59e6b945f2261c95a1dd9faf239187f32ff0a96af1d1d28c4557f919" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "smallvec", +] + +[[package]] +name = "quinn" +version = "0.11.9" +dependencies = [ + "anyhow", + "async-io", + "async-std", + "bencher", + "bytes", + "cfg_aliases", + "clap", + "crc", + "directories-next", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rand", + "rcgen", + "rustc-hash 2.1.1", + "rustls", + "rustls-pemfile", + "smol", + "socket2", + "thiserror 2.0.16", + "tokio", + "tracing", + "tracing-futures", + "tracing-subscriber", + "url", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "fastbloom", + "getrandom 0.3.3", + "lru-slab", + "qlog", + "rand", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rcgen" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.6", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "serde", + "serde_derive", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "time", + "tracing", + "tracing-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/vendor/flutter_quic/rust/vendor/quinn/Cargo.toml b/vendor/flutter_quic/rust/vendor/quinn/Cargo.toml new file mode 100644 index 0000000..04e638a --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/Cargo.toml @@ -0,0 +1,253 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.74.1" +name = "quinn" +version = "0.11.9" +build = "build.rs" +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Versatile QUIC transport protocol implementation" +readme = "README.md" +keywords = ["quic"] +categories = [ + "network-programming", + "asynchronous", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/quinn-rs/quinn" + +[package.metadata.docs.rs] +features = [ + "lock_tracking", + "rustls-aws-lc-rs", + "rustls-ring", + "runtime-tokio", + "runtime-async-std", + "runtime-smol", + "log", + "rustls-log", +] + +[features] +aws-lc-rs = ["proto/aws-lc-rs"] +aws-lc-rs-fips = ["proto/aws-lc-rs-fips"] +bloom = ["proto/bloom"] +default = [ + "log", + "platform-verifier", + "runtime-tokio", + "rustls-ring", + "bloom", +] +lock_tracking = [] +log = [ + "tracing/log", + "proto/log", + "udp/log", +] +platform-verifier = ["proto/platform-verifier"] +qlog = ["proto/qlog"] +ring = ["proto/ring"] +runtime-async-std = [ + "async-io", + "async-std", +] +runtime-smol = [ + "async-io", + "smol", +] +runtime-tokio = [ + "tokio/time", + "tokio/rt", + "tokio/net", +] +rustls = ["rustls-ring"] +rustls-aws-lc-rs = [ + "dep:rustls", + "aws-lc-rs", + "proto/rustls-aws-lc-rs", + "proto/aws-lc-rs", +] +rustls-aws-lc-rs-fips = [ + "dep:rustls", + "aws-lc-rs-fips", + "proto/rustls-aws-lc-rs-fips", + "proto/aws-lc-rs-fips", +] +rustls-log = ["rustls?/logging"] +rustls-ring = [ + "dep:rustls", + "ring", + "proto/rustls-ring", + "proto/ring", +] + +[lib] +name = "quinn" +path = "src/lib.rs" + +[[example]] +name = "client" +path = "examples/client.rs" +required-features = ["rustls-ring"] + +[[example]] +name = "connection" +path = "examples/connection.rs" +required-features = ["rustls-ring"] + +[[example]] +name = "insecure_connection" +path = "examples/insecure_connection.rs" +required-features = ["rustls-ring"] + +[[example]] +name = "server" +path = "examples/server.rs" +required-features = ["rustls-ring"] + +[[example]] +name = "single_socket" +path = "examples/single_socket.rs" +required-features = ["rustls-ring"] + +[[test]] +name = "many_connections" +path = "tests/many_connections.rs" + +[[bench]] +name = "bench" +path = "benches/bench.rs" +harness = false +required-features = ["rustls-ring"] + +[dependencies.async-io] +version = "2" +optional = true + +[dependencies.async-std] +version = "1.11" +optional = true + +[dependencies.bytes] +version = "1" + +[dependencies.futures-io] +version = "0.3.19" +optional = true + +[dependencies.pin-project-lite] +version = "0.2" + +[dependencies.proto] +version = "0.11.12" +default-features = false +package = "quinn-proto" + +[dependencies.rustc-hash] +version = "2" + +[dependencies.rustls] +version = "0.23.5" +features = ["std"] +optional = true +default-features = false + +[dependencies.smol] +version = "2" +optional = true + +[dependencies.thiserror] +version = "2.0.3" + +[dependencies.tokio] +version = "1.28.1" +features = ["sync"] + +[dependencies.tracing] +version = "0.1.10" +features = ["std"] +default-features = false + +[dependencies.udp] +version = "0.5" +features = ["tracing"] +default-features = false +package = "quinn-udp" + +[dev-dependencies.anyhow] +version = "1.0.22" + +[dev-dependencies.bencher] +version = "0.1.5" + +[dev-dependencies.clap] +version = "4" +features = ["derive"] + +[dev-dependencies.crc] +version = "3" + +[dev-dependencies.directories-next] +version = "2" + +[dev-dependencies.rand] +version = "0.9" + +[dev-dependencies.rcgen] +version = "0.14" + +[dev-dependencies.rustls-pemfile] +version = "2" + +[dev-dependencies.tokio] +version = "1.28.1" +features = [ + "sync", + "rt", + "rt-multi-thread", + "time", + "macros", +] + +[dev-dependencies.tracing-futures] +version = "0.2.0" +features = ["std-future"] +default-features = false + +[dev-dependencies.tracing-subscriber] +version = "0.3.0" +features = [ + "env-filter", + "fmt", + "ansi", + "time", + "local-time", +] +default-features = false + +[dev-dependencies.url] +version = "2" + +[build-dependencies.cfg_aliases] +version = "0.2" + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.web-time] +version = "1" + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies.socket2] +version = ">=0.5, <0.7" diff --git a/vendor/flutter_quic/rust/vendor/quinn/Cargo.toml.orig b/vendor/flutter_quic/rust/vendor/quinn/Cargo.toml.orig new file mode 100644 index 0000000..505e406 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/Cargo.toml.orig @@ -0,0 +1,115 @@ +[package] +name = "quinn" +version = "0.11.9" +license.workspace = true +repository.workspace = true +description = "Versatile QUIC transport protocol implementation" +readme = "../README.md" +keywords.workspace = true +categories.workspace = true +workspace = ".." +edition.workspace = true +rust-version.workspace = true + + +[features] +# NOTE: Please keep this in sync with the feature list in `.github/workflows/codecov.yml`, see +# comment in that file for more information. +default = ["log", "platform-verifier", "runtime-tokio", "rustls-ring", "bloom"] +# Enables `Endpoint::client` and `Endpoint::server` conveniences +aws-lc-rs = ["proto/aws-lc-rs"] +aws-lc-rs-fips = ["proto/aws-lc-rs-fips"] +# Enables BloomTokenLog, and uses it by default +bloom = ["proto/bloom"] +# Records how long locks are held, and warns if they are held >= 1ms +lock_tracking = [] +# Provides `ClientConfig::with_platform_verifier()` convenience method +platform-verifier = ["proto/platform-verifier"] +# For backwards compatibility, `rustls` forwards to `rustls-ring` +rustls = ["rustls-ring"] +# Enable rustls with the `aws-lc-rs` crypto provider +rustls-aws-lc-rs = ["dep:rustls", "aws-lc-rs", "proto/rustls-aws-lc-rs", "proto/aws-lc-rs"] +rustls-aws-lc-rs-fips = ["dep:rustls", "aws-lc-rs-fips", "proto/rustls-aws-lc-rs-fips", "proto/aws-lc-rs-fips"] +# Enable rustls with the `ring` crypto provider +rustls-ring = ["dep:rustls", "ring", "proto/rustls-ring", "proto/ring"] +# Enable the `ring` crypto provider. +# Outside wasm*-unknown-unknown targets, this enables `Endpoint::client` and `Endpoint::server` conveniences. +ring = ["proto/ring"] +runtime-tokio = ["tokio/time", "tokio/rt", "tokio/net"] +runtime-async-std = ["async-io", "async-std"] +runtime-smol = ["async-io", "smol"] + +# Configure `tracing` to log events via `log` if no `tracing` subscriber exists. +log = ["tracing/log", "proto/log", "udp/log"] +# Enable rustls logging +rustls-log = ["rustls?/logging"] +# Enable qlog support +qlog = ["proto/qlog"] + +[dependencies] +async-io = { workspace = true, optional = true } +async-std = { workspace = true, optional = true } +bytes = { workspace = true } +# Enables futures::io::{AsyncRead, AsyncWrite} support for streams +futures-io = { workspace = true, optional = true } +rustc-hash = { workspace = true } +pin-project-lite = { workspace = true } +proto = { package = "quinn-proto", path = "../quinn-proto", version = "0.11.12", default-features = false } +rustls = { workspace = true, optional = true } +smol = { workspace = true, optional = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tokio = { workspace = true } +udp = { package = "quinn-udp", path = "../quinn-udp", version = "0.5", default-features = false, features = ["tracing"] } + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +socket2 = { workspace = true } + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +web-time = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +crc = { workspace = true } +bencher = { workspace = true } +directories-next = { workspace = true } +rand = { workspace = true } +rcgen = { workspace = true } +rustls-pemfile = { workspace = true } +clap = { workspace = true } +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "time", "macros"] } +tracing-subscriber = { workspace = true } +tracing-futures = { workspace = true } +url = { workspace = true } + +[build-dependencies] +cfg_aliases = { workspace = true } + +[[example]] +name = "server" +required-features = ["rustls-ring"] + +[[example]] +name = "client" +required-features = ["rustls-ring"] + +[[example]] +name = "insecure_connection" +required-features = ["rustls-ring"] + +[[example]] +name = "single_socket" +required-features = ["rustls-ring"] + +[[example]] +name = "connection" +required-features = ["rustls-ring"] + +[[bench]] +name = "bench" +harness = false +required-features = ["rustls-ring"] + +[package.metadata.docs.rs] +# all non-default features except fips (cannot build on docs.rs environment) +features = ["lock_tracking", "rustls-aws-lc-rs", "rustls-ring", "runtime-tokio", "runtime-async-std", "runtime-smol", "log", "rustls-log"] diff --git a/vendor/flutter_quic/rust/vendor/quinn/LICENSE-APACHE b/vendor/flutter_quic/rust/vendor/quinn/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/flutter_quic/rust/vendor/quinn/LICENSE-MIT b/vendor/flutter_quic/rust/vendor/quinn/LICENSE-MIT new file mode 100644 index 0000000..f656104 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) 2018 The quinn Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/flutter_quic/rust/vendor/quinn/README.md b/vendor/flutter_quic/rust/vendor/quinn/README.md new file mode 100644 index 0000000..02546bd --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/README.md @@ -0,0 +1,127 @@ +

+ +[![Documentation](https://docs.rs/quinn/badge.svg)](https://docs.rs/quinn/) +[![Crates.io](https://img.shields.io/crates/v/quinn.svg)](https://crates.io/crates/quinn) +[![Build status](https://github.com/quinn-rs/quinn/workflows/CI/badge.svg)](https://github.com/djc/quinn/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/quinn-rs/quinn/branch/main/graph/badge.svg)](https://codecov.io/gh/quinn-rs/quinn) +[![Chat](https://img.shields.io/badge/chat-%23quinn:matrix.org-%2346BC99?logo=matrix)](https://matrix.to/#/#quinn:matrix.org) +[![Chat](https://img.shields.io/discord/976380008299917365?logo=discord)](https://discord.gg/SGPEcDfVzh) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE-APACHE) + +Quinn is a pure-Rust, async-compatible implementation of the IETF [QUIC][quic] transport protocol. +The project was founded by [Dirkjan Ochtman](https://github.com/djc) and +[Benjamin Saunders](https://github.com/Ralith) as a side project in 2018, and has seen more than +30 releases since then. If you're using Quinn in a commercial setting, please consider +[sponsoring](https://opencollective.com/quinn-rs) the project. + +## Features + +- Simultaneous client/server operation +- Ordered and unordered stream reads for improved performance +- Works on stable Rust, tested on Linux, macOS and Windows +- Pluggable cryptography, with a standard implementation backed by + [rustls][rustls] and [*ring*][ring] +- Application-layer datagrams for small, unreliable messages +- Future-based async API +- Minimum supported Rust version of 1.74.1 + +## Overview + +- **quinn:** High-level async API based on tokio, see [examples][examples] for usage. This will be used by most developers. (Basic benchmarks are included.) +- **quinn-proto:** Deterministic state machine of the protocol which performs [**no** I/O][sans-io] internally and is suitable for use with custom event loops (and potentially a C or C++ API). +- **quinn-udp:** UDP sockets with ECN information tuned for the protocol. +- **bench:** Benchmarks without any framework. +- **fuzz:** Fuzz tests. + +# Getting Started + +**Examples** + +```sh +$ cargo run --example server ./ +$ cargo run --example client https://localhost:4433/Cargo.toml +``` + +This launches an HTTP 0.9 server on the loopback address serving the current +working directory, with the client fetching `./Cargo.toml`. By default, the +server generates a self-signed certificate and stores it to disk, where the +client will automatically find and trust it. + +**Links** + +- Talk at [RustFest Paris (May 2018) presentation][talk]; [slides][slides]; [YouTube][youtube] +- Usage [examples][examples] +- Guide [book][documentation] + +## Usage Notes + +
+ +Click to show the notes + + +### Buffers + +A Quinn endpoint corresponds to a single UDP socket, no matter how many +connections are in use. Handling high aggregate data rates on a single endpoint +can require a larger UDP buffer than is configured by default in most +environments. If you observe erratic latency and/or throughput over a stable +network link, consider increasing the buffer sizes used. For example, you could +adjust the `SO_SNDBUF` and `SO_RCVBUF` options of the UDP socket to be used +before passing it in to Quinn. Note that some platforms (e.g. Linux) require +elevated privileges or modified system configuration for a process to increase +its UDP buffer sizes. + +### Certificates + +By default, Quinn clients validate the cryptographic identity of servers they +connect to. This prevents an active, on-path attacker from intercepting +messages, but requires trusting some certificate authority. For many purposes, +this can be accomplished by using certificates from [Let's Encrypt][letsencrypt] +for servers, and relying on the default configuration for clients. + +For some cases, including peer-to-peer, trust-on-first-use, deliberately +insecure applications, or any case where servers are not identified by domain +name, this isn't practical. Arbitrary certificate validation logic can be +implemented by enabling the `dangerous_configuration` feature of `rustls` and +constructing a Quinn `ClientConfig` with an overridden certificate verifier by +hand. + +When operating your own certificate authority doesn't make sense, [rcgen][rcgen] +can be used to generate self-signed certificates on demand. To support +trust-on-first-use, servers that automatically generate self-signed certificates +should write their generated certificate to persistent storage and reuse it on +future runs. + +
+

+ +## Contribution + +All feedback welcome. Feel free to file bugs, requests for documentation and +any other feedback to the [issue tracker][issues]. + +The quinn-proto test suite uses simulated IO for reproducibility and to avoid +long sleeps in certain timing-sensitive tests. If the `SSLKEYLOGFILE` +environment variable is set, the tests will emit UDP packets for inspection +using external protocol analyzers like Wireshark, and NSS-compatible key logs +for the client side of each connection will be written to the path specified in +the variable. + +The minimum supported Rust version for published releases of our +crates will always be at least 6 months old at the time of release. + +[quic]: https://quicwg.github.io/ +[issues]: https://github.com/djc/quinn/issues +[rustls]: https://github.com/ctz/rustls +[ring]: https://github.com/briansmith/ring +[talk]: https://paris.rustfest.eu/sessions/a-quic-future-in-rust +[slides]: https://github.com/djc/talks/blob/ff760845b51ba4836cce82e7f2c640ecb5fd59fa/2018-05-26%20A%20QUIC%20future%20in%20Rust/Quinn-Speaker.pdf +[animation]: https://dirkjan.ochtman.nl/files/head-of-line-blocking.html +[youtube]: https://www.youtube.com/watch?v=EHgyY5DNdvI +[letsencrypt]: https://letsencrypt.org/ +[rcgen]: https://crates.io/crates/rcgen +[examples]: https://github.com/djc/quinn/tree/main/quinn/examples +[documentation]: https://quinn-rs.github.io/quinn/networking-introduction.html +[sans-io]: https://sans-io.readthedocs.io/how-to-sans-io.html diff --git a/vendor/flutter_quic/rust/vendor/quinn/benches/bench.rs b/vendor/flutter_quic/rust/vendor/quinn/benches/bench.rs new file mode 100644 index 0000000..b438492 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/benches/bench.rs @@ -0,0 +1,170 @@ +use std::{ + net::{IpAddr, Ipv6Addr, SocketAddr, UdpSocket}, + sync::Arc, + thread, +}; + +use bencher::{Bencher, benchmark_group, benchmark_main}; +use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; +use tokio::runtime::{Builder, Runtime}; +use tracing::error_span; +use tracing_futures::Instrument as _; + +use quinn::{Endpoint, TokioRuntime}; + +benchmark_group!( + benches, + large_data_1_stream, + large_data_10_streams, + small_data_1_stream, + small_data_100_streams +); +benchmark_main!(benches); + +fn large_data_1_stream(bench: &mut Bencher) { + send_data(bench, LARGE_DATA, 1); +} + +fn large_data_10_streams(bench: &mut Bencher) { + send_data(bench, LARGE_DATA, 10); +} + +fn small_data_1_stream(bench: &mut Bencher) { + send_data(bench, SMALL_DATA, 1); +} + +fn small_data_100_streams(bench: &mut Bencher) { + send_data(bench, SMALL_DATA, 100); +} + +fn send_data(bench: &mut Bencher, data: &'static [u8], concurrent_streams: usize) { + let _ = tracing_subscriber::fmt::try_init(); + + let ctx = Context::new(); + let (addr, thread) = ctx.spawn_server(); + let (endpoint, client, runtime) = ctx.make_client(addr); + let client = Arc::new(client); + + bench.bytes = (data.len() as u64) * (concurrent_streams as u64); + bench.iter(|| { + let mut handles = Vec::new(); + + for _ in 0..concurrent_streams { + let client = client.clone(); + handles.push(runtime.spawn(async move { + let mut stream = client.open_uni().await.unwrap(); + stream.write_all(data).await.unwrap(); + stream.finish().unwrap(); + // Wait for stream to close + _ = stream.stopped().await; + })); + } + + runtime.block_on(async { + for handle in handles { + handle.await.unwrap(); + } + }); + }); + drop(client); + runtime.block_on(endpoint.wait_idle()); + thread.join().unwrap() +} + +struct Context { + server_config: quinn::ServerConfig, + client_config: quinn::ClientConfig, +} + +impl Context { + fn new() -> Self { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); + let cert = CertificateDer::from(cert.cert); + + let mut server_config = + quinn::ServerConfig::with_single_cert(vec![cert.clone()], key.into()).unwrap(); + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(1024_u16.into()); + + let mut roots = rustls::RootCertStore::empty(); + roots.add(cert).unwrap(); + + Self { + server_config, + client_config: quinn::ClientConfig::with_root_certificates(Arc::new(roots)).unwrap(), + } + } + + pub fn spawn_server(&self) -> (SocketAddr, thread::JoinHandle<()>) { + let sock = UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)).unwrap(); + let addr = sock.local_addr().unwrap(); + let config = self.server_config.clone(); + let handle = thread::spawn(move || { + let runtime = rt(); + let endpoint = { + let _guard = runtime.enter(); + Endpoint::new( + Default::default(), + Some(config), + sock, + Arc::new(TokioRuntime), + ) + .unwrap() + }; + let handle = runtime.spawn( + async move { + let connection = endpoint + .accept() + .await + .expect("accept") + .await + .expect("connect"); + + while let Ok(mut stream) = connection.accept_uni().await { + tokio::spawn(async move { + while stream + .read_chunk(usize::MAX, false) + .await + .unwrap() + .is_some() + {} + }); + } + } + .instrument(error_span!("server")), + ); + runtime.block_on(handle).unwrap(); + }); + (addr, handle) + } + + pub fn make_client( + &self, + server_addr: SocketAddr, + ) -> (quinn::Endpoint, quinn::Connection, Runtime) { + let runtime = rt(); + let endpoint = { + let _guard = runtime.enter(); + Endpoint::client(SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)).unwrap() + }; + let connection = runtime + .block_on(async { + endpoint + .connect_with(self.client_config.clone(), server_addr, "localhost") + .unwrap() + .instrument(error_span!("client")) + .await + }) + .unwrap(); + (endpoint, connection, runtime) + } +} + +fn rt() -> Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + +const LARGE_DATA: &[u8] = &[0xAB; 1024 * 1024]; + +const SMALL_DATA: &[u8] = &[0xAB; 1]; diff --git a/vendor/flutter_quic/rust/vendor/quinn/build.rs b/vendor/flutter_quic/rust/vendor/quinn/build.rs new file mode 100644 index 0000000..7aae568 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/build.rs @@ -0,0 +1,9 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + // Setup cfg aliases + cfg_aliases! { + // Convenience aliases + wasm_browser: { all(target_family = "wasm", target_os = "unknown") }, + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/README.md b/vendor/flutter_quic/rust/vendor/quinn/examples/README.md new file mode 100644 index 0000000..4baf933 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/README.md @@ -0,0 +1,92 @@ +## HTTP/0.9 File Serving Example + +The `server` and `client` examples demonstrate fetching files using a HTTP-like toy protocol. + +1. Server (`server.rs`) + +The server listens for any client requesting a file. +If the file path is valid and allowed, it returns the contents. + +Open up a terminal and execute: + +```text +$ cargo run --example server ./ +``` + +2. Client (`client.rs`) + +The client requests a file and prints it to the console. +If the file is on the server, it will receive the response. + +In a new terminal execute: + +```test +$ cargo run --example client https://localhost:4433/Cargo.toml +``` + +where `Cargo.toml` is any file in the directory passed to the server. + +**Result:** + +The output will be the contents of this README. + +**Troubleshooting:** + +If the client times out with no activity on the server, try forcing the server to run on IPv4 by +running it with `cargo run --example server -- ./ --listen 127.0.0.1:4433`. The server listens on +IPv6 by default, `localhost` tends to resolve to IPv4, and support for accepting IPv4 packets on +IPv6 sockets varies between platforms. + +If the client prints `failed to process request: failed reading file`, the request was processed +successfully but the path segment of the URL did not correspond to a file in the directory being +served. + +## Minimal Example +The `connection.rs` example intends to use the smallest amount of code to make a simple QUIC connection. +The server issues it's own certificate and passes it to the client to trust. + +```text +$ cargo run --example connection +``` + +This example will make a QUIC connection on localhost, and you should see output like: + +```text +[client] connected: addr=127.0.0.1:5000 +[server] connection accepted: addr=127.0.0.1:53712 +``` + +## Insecure Connection Example + +The `insecure_connection.rs` example demonstrates how to make a QUIC connection that ignores the server certificate. + +```text +$ cargo run --example insecure_connection --features="rustls/dangerous_configuration" +``` + +## Single Socket Example + +You can have multiple QUIC connections over a single UDP socket. This is especially +useful, if you are building a peer-to-peer system where you potentially need to communicate with +thousands of peers or if you have a +[hole punched](https://en.wikipedia.org/wiki/UDP_hole_punching) UDP socket. +Additionally, QUIC servers and clients can both operate on the same UDP socket. +This example demonstrates how to make multiple outgoing connections on a single UDP socket. + +```text +$ cargo run --example single_socket +``` + +The expected output should be something like: + +```text +[client] connected: addr=127.0.0.1:5000 +[server] incoming connection: addr=127.0.0.1:48930 +[client] connected: addr=127.0.0.1:5001 +[client] connected: addr=127.0.0.1:5002 +[server] incoming connection: addr=127.0.0.1:48930 +[server] incoming connection: addr=127.0.0.1:48930 +``` + +Notice how the server sees multiple incoming connections with different IDs coming from the same +endpoint. diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/client.rs b/vendor/flutter_quic/rust/vendor/quinn/examples/client.rs new file mode 100644 index 0000000..ea85e02 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/client.rs @@ -0,0 +1,169 @@ +//! This example demonstrates an HTTP client that requests files from a server. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + fs, + io::{self, Write}, + net::{SocketAddr, ToSocketAddrs}, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::{Result, anyhow}; +use clap::Parser; +use proto::crypto::rustls::QuicClientConfig; +use rustls::pki_types::CertificateDer; +use tracing::{error, info}; +use url::Url; + +mod common; + +/// HTTP/0.9 over QUIC client +#[derive(Parser, Debug)] +#[clap(name = "client")] +struct Opt { + /// Perform NSS-compatible TLS key logging to the file specified in `SSLKEYLOGFILE`. + #[clap(long = "keylog")] + keylog: bool, + + url: Url, + + /// Override hostname used for certificate verification + #[clap(long = "host")] + host: Option, + + /// Custom certificate authority to trust, in DER format + #[clap(long = "ca")] + ca: Option, + + /// Simulate NAT rebinding after connecting + #[clap(long = "rebind")] + rebind: bool, + + /// Address to bind on + #[clap(long = "bind", default_value = "[::]:0")] + bind: SocketAddr, +} + +fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {e}"); + 1 + } else { + 0 + } + }; + ::std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let url = options.url; + let url_host = strip_ipv6_brackets(url.host_str().unwrap()); + let remote = (url_host, url.port().unwrap_or(4433)) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow!("couldn't resolve to an address"))?; + + let mut roots = rustls::RootCertStore::empty(); + if let Some(ca_path) = options.ca { + roots.add(CertificateDer::from(fs::read(ca_path)?))?; + } else { + let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); + match fs::read(dirs.data_local_dir().join("cert.der")) { + Ok(cert) => { + roots.add(CertificateDer::from(cert))?; + } + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("local server certificate not found"); + } + Err(e) => { + error!("failed to open local server certificate: {}", e); + } + } + } + let mut client_crypto = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + + client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + if options.keylog { + client_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let client_config = + quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + let mut endpoint = quinn::Endpoint::client(options.bind)?; + endpoint.set_default_client_config(client_config); + + let request = format!("GET {}\r\n", url.path()); + let start = Instant::now(); + let rebind = options.rebind; + let host = options.host.as_deref().unwrap_or(url_host); + + eprintln!("connecting to {host} at {remote}"); + let conn = endpoint + .connect(remote, host)? + .await + .map_err(|e| anyhow!("failed to connect: {}", e))?; + eprintln!("connected at {:?}", start.elapsed()); + let (mut send, mut recv) = conn + .open_bi() + .await + .map_err(|e| anyhow!("failed to open stream: {}", e))?; + if rebind { + let socket = std::net::UdpSocket::bind("[::]:0").unwrap(); + let addr = socket.local_addr().unwrap(); + eprintln!("rebinding to {addr}"); + endpoint.rebind(socket).expect("rebind failed"); + } + + send.write_all(request.as_bytes()) + .await + .map_err(|e| anyhow!("failed to send request: {}", e))?; + send.finish().unwrap(); + let response_start = Instant::now(); + eprintln!("request sent at {:?}", response_start - start); + let resp = recv + .read_to_end(usize::MAX) + .await + .map_err(|e| anyhow!("failed to read response: {}", e))?; + let duration = response_start.elapsed(); + eprintln!( + "response received in {:?} - {} KiB/s", + duration, + resp.len() as f32 / (duration_secs(&duration) * 1024.0) + ); + io::stdout().write_all(&resp).unwrap(); + io::stdout().flush().unwrap(); + conn.close(0u32.into(), b"done"); + + // Give the server a fair chance to receive the close packet + endpoint.wait_idle().await; + + Ok(()) +} + +fn strip_ipv6_brackets(host: &str) -> &str { + // An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the + // ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those. + if host.starts_with('[') && host.ends_with(']') { + &host[1..host.len() - 1] + } else { + host + } +} + +fn duration_secs(x: &Duration) -> f32 { + x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9 +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/common/mod.rs b/vendor/flutter_quic/rust/vendor/quinn/examples/common/mod.rs new file mode 100644 index 0000000..4cae652 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/common/mod.rs @@ -0,0 +1,73 @@ +#![cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +//! Commonly used code in most examples. + +use quinn::{ClientConfig, Endpoint, ServerConfig}; +use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; + +use std::{error::Error, net::SocketAddr, sync::Arc}; + +/// Constructs a QUIC endpoint configured for use a client only. +/// +/// ## Args +/// +/// - server_certs: list of trusted certificates. +#[allow(unused)] +pub fn make_client_endpoint( + bind_addr: SocketAddr, + server_certs: &[&[u8]], +) -> Result> { + let client_cfg = configure_client(server_certs)?; + let mut endpoint = Endpoint::client(bind_addr)?; + endpoint.set_default_client_config(client_cfg); + Ok(endpoint) +} + +/// Constructs a QUIC endpoint configured to listen for incoming connections on a certain address +/// and port. +/// +/// ## Returns +/// +/// - a stream of incoming QUIC connections +/// - server certificate serialized into DER format +#[allow(unused)] +pub fn make_server_endpoint( + bind_addr: SocketAddr, +) -> Result<(Endpoint, CertificateDer<'static>), Box> { + let (server_config, server_cert) = configure_server()?; + let endpoint = Endpoint::server(server_config, bind_addr)?; + Ok((endpoint, server_cert)) +} + +/// Builds default quinn client config and trusts given certificates. +/// +/// ## Args +/// +/// - server_certs: a list of trusted certificates in DER format. +fn configure_client( + server_certs: &[&[u8]], +) -> Result> { + let mut certs = rustls::RootCertStore::empty(); + for cert in server_certs { + certs.add(CertificateDer::from(*cert))?; + } + + Ok(ClientConfig::with_root_certificates(Arc::new(certs))?) +} + +/// Returns default server configuration along with its certificate. +fn configure_server() +-> Result<(ServerConfig, CertificateDer<'static>), Box> { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_der = CertificateDer::from(cert.cert); + let priv_key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); + + let mut server_config = + ServerConfig::with_single_cert(vec![cert_der.clone()], priv_key.into())?; + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + + Ok((server_config, cert_der)) +} + +#[allow(unused)] +pub const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/connection.rs b/vendor/flutter_quic/rust/vendor/quinn/examples/connection.rs new file mode 100644 index 0000000..37cbe46 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/connection.rs @@ -0,0 +1,45 @@ +//! This example intends to use the smallest amount of code to make a simple QUIC connection. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + error::Error, + net::{IpAddr, Ipv4Addr, SocketAddr}, +}; + +mod common; +use common::{make_client_endpoint, make_server_endpoint}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5000); + let (endpoint, server_cert) = make_server_endpoint(server_addr)?; + // accept a single connection + let endpoint2 = endpoint.clone(); + tokio::spawn(async move { + let incoming_conn = endpoint2.accept().await.unwrap(); + let conn = incoming_conn.await.unwrap(); + println!( + "[server] connection accepted: addr={}", + conn.remote_address() + ); + // Dropping all handles associated with a connection implicitly closes it + }); + + let endpoint = make_client_endpoint("0.0.0.0:0".parse().unwrap(), &[&server_cert])?; + // connect to server + let connection = endpoint + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); + + // Waiting for a stream will complete with an error when the server closes the connection + let _ = connection.accept_uni().await; + + // Make sure the server has a chance to clean up + endpoint.wait_idle().await; + + Ok(()) +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/insecure_connection.rs b/vendor/flutter_quic/rust/vendor/quinn/examples/insecure_connection.rs new file mode 100644 index 0000000..91b987b --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/insecure_connection.rs @@ -0,0 +1,118 @@ +//! This example demonstrates how to make a QUIC connection that ignores the server certificate. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + error::Error, + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use proto::crypto::rustls::QuicClientConfig; +use quinn::{ClientConfig, Endpoint}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; + +mod common; +use common::make_server_endpoint; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // server and client are running on the same thread asynchronously + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); + tokio::spawn(run_server(addr)); + run_client(addr).await?; + Ok(()) +} + +/// Runs a QUIC server bound to given address. +async fn run_server(addr: SocketAddr) { + let (endpoint, _server_cert) = make_server_endpoint(addr).unwrap(); + // accept a single connection + let incoming_conn = endpoint.accept().await.unwrap(); + let conn = incoming_conn.await.unwrap(); + println!( + "[server] connection accepted: addr={}", + conn.remote_address() + ); +} + +async fn run_client(server_addr: SocketAddr) -> Result<(), Box> { + let mut endpoint = Endpoint::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))?; + + endpoint.set_default_client_config(ClientConfig::new(Arc::new(QuicClientConfig::try_from( + rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new()) + .with_no_client_auth(), + )?))); + + // connect to server + let connection = endpoint + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); + // Dropping handles allows the corresponding objects to automatically shut down + drop(connection); + // Make sure the server has a chance to clean up + endpoint.wait_idle().await; + + Ok(()) +} + +/// Dummy certificate verifier that treats any certificate as valid. +/// NOTE, such verification is vulnerable to MITM attacks, but convenient for testing. +#[derive(Debug)] +struct SkipServerVerification(Arc); + +impl SkipServerVerification { + fn new() -> Arc { + Arc::new(Self(Arc::new(rustls::crypto::ring::default_provider()))) + } +} + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/server.rs b/vendor/flutter_quic/rust/vendor/quinn/examples/server.rs new file mode 100644 index 0000000..298562f --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/server.rs @@ -0,0 +1,271 @@ +//! This example demonstrates an HTTP server that serves files from a directory. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + ascii, fs, io, + net::SocketAddr, + path::{self, Path, PathBuf}, + str, + sync::Arc, +}; + +use anyhow::{Context, Result, anyhow, bail}; +use clap::Parser; +use proto::crypto::rustls::QuicServerConfig; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; +use tracing::{error, info, info_span}; +use tracing_futures::Instrument as _; + +mod common; + +#[derive(Parser, Debug)] +#[clap(name = "server")] +struct Opt { + /// file to log TLS keys to for debugging + #[clap(long = "keylog")] + keylog: bool, + /// directory to serve files from + root: PathBuf, + /// TLS private key in PEM format + #[clap(short = 'k', long = "key", requires = "cert")] + key: Option, + /// TLS certificate in PEM format + #[clap(short = 'c', long = "cert", requires = "key")] + cert: Option, + /// Enable stateless retries + #[clap(long = "stateless-retry")] + stateless_retry: bool, + /// Address to listen on + #[clap(long = "listen", default_value = "[::1]:4433")] + listen: SocketAddr, + /// Client address to block + #[clap(long = "block")] + block: Option, + /// Maximum number of concurrent connections to allow + #[clap(long = "connection-limit")] + connection_limit: Option, +} + +fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {e}"); + 1 + } else { + 0 + } + }; + ::std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&options.key, &options.cert) { + let key = fs::read(key_path).context("failed to read private key")?; + let key = if key_path.extension().is_some_and(|x| x == "der") { + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) + } else { + rustls_pemfile::private_key(&mut &*key) + .context("malformed PKCS #1 private key")? + .ok_or_else(|| anyhow::Error::msg("no private keys found"))? + }; + let cert_chain = fs::read(cert_path).context("failed to read certificate chain")?; + let cert_chain = if cert_path.extension().is_some_and(|x| x == "der") { + vec![CertificateDer::from(cert_chain)] + } else { + rustls_pemfile::certs(&mut &*cert_chain) + .collect::>() + .context("invalid PEM-encoded certificate")? + }; + + (cert_chain, key) + } else { + let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); + let path = dirs.data_local_dir(); + let cert_path = path.join("cert.der"); + let key_path = path.join("key.der"); + let (cert, key) = match fs::read(&cert_path).and_then(|x| Ok((x, fs::read(&key_path)?))) { + Ok((cert, key)) => ( + CertificateDer::from(cert), + PrivateKeyDer::try_from(key).map_err(anyhow::Error::msg)?, + ), + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("generating self-signed certificate"); + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); + let cert = cert.cert.into(); + fs::create_dir_all(path).context("failed to create certificate directory")?; + fs::write(&cert_path, &cert).context("failed to write certificate")?; + fs::write(&key_path, key.secret_pkcs8_der()) + .context("failed to write private key")?; + (cert, key.into()) + } + Err(e) => { + bail!("failed to read certificate: {}", e); + } + }; + + (vec![cert], key) + }; + + let mut server_crypto = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key)?; + server_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + if options.keylog { + server_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let mut server_config = + quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + + let root = Arc::::from(options.root.clone()); + if !root.exists() { + bail!("root path does not exist"); + } + + let endpoint = quinn::Endpoint::server(server_config, options.listen)?; + eprintln!("listening on {}", endpoint.local_addr()?); + + while let Some(conn) = endpoint.accept().await { + if options + .connection_limit + .is_some_and(|n| endpoint.open_connections() >= n) + { + info!("refusing due to open connection limit"); + conn.refuse(); + } else if Some(conn.remote_address()) == options.block { + info!("refusing blocked client IP address"); + conn.refuse(); + } else if options.stateless_retry && !conn.remote_address_validated() { + info!("requiring connection to validate its address"); + conn.retry().unwrap(); + } else { + info!("accepting connection"); + let fut = handle_connection(root.clone(), conn); + tokio::spawn(async move { + if let Err(e) = fut.await { + error!("connection failed: {reason}", reason = e.to_string()) + } + }); + } + } + + Ok(()) +} + +async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> { + let connection = conn.await?; + let span = info_span!( + "connection", + remote = %connection.remote_address(), + protocol = %connection + .handshake_data() + .unwrap() + .downcast::().unwrap() + .protocol + .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) + ); + async { + info!("established"); + + // Each stream initiated by the client constitutes a new request. + loop { + let stream = connection.accept_bi().await; + let stream = match stream { + Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + info!("connection closed"); + return Ok(()); + } + Err(e) => { + return Err(e); + } + Ok(s) => s, + }; + let fut = handle_request(root.clone(), stream); + tokio::spawn( + async move { + if let Err(e) = fut.await { + error!("failed: {reason}", reason = e.to_string()); + } + } + .instrument(info_span!("request")), + ); + } + } + .instrument(span) + .await?; + Ok(()) +} + +async fn handle_request( + root: Arc, + (mut send, mut recv): (quinn::SendStream, quinn::RecvStream), +) -> Result<()> { + let req = recv + .read_to_end(64 * 1024) + .await + .map_err(|e| anyhow!("failed reading request: {}", e))?; + let mut escaped = String::new(); + for &x in &req[..] { + let part = ascii::escape_default(x).collect::>(); + escaped.push_str(str::from_utf8(&part).unwrap()); + } + info!(content = %escaped); + // Execute the request + let resp = process_get(&root, &req).unwrap_or_else(|e| { + error!("failed: {}", e); + format!("failed to process request: {e}\n").into_bytes() + }); + // Write the response + send.write_all(&resp) + .await + .map_err(|e| anyhow!("failed to send response: {}", e))?; + // Gracefully terminate the stream + send.finish().unwrap(); + info!("complete"); + Ok(()) +} + +fn process_get(root: &Path, x: &[u8]) -> Result> { + if x.len() < 4 || &x[0..4] != b"GET " { + bail!("missing GET"); + } + if x[4..].len() < 2 || &x[x.len() - 2..] != b"\r\n" { + bail!("missing \\r\\n"); + } + let x = &x[4..x.len() - 2]; + let end = x.iter().position(|&c| c == b' ').unwrap_or(x.len()); + let path = str::from_utf8(&x[..end]).context("path is malformed UTF-8")?; + let path = Path::new(&path); + let mut real_path = PathBuf::from(root); + let mut components = path.components(); + match components.next() { + Some(path::Component::RootDir) => {} + _ => { + bail!("path must be absolute"); + } + } + for c in components { + match c { + path::Component::Normal(x) => { + real_path.push(x); + } + x => { + bail!("illegal component in path: {:?}", x); + } + } + } + let data = fs::read(&real_path).context("failed reading file")?; + Ok(data) +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/examples/single_socket.rs b/vendor/flutter_quic/rust/vendor/quinn/examples/single_socket.rs new file mode 100644 index 0000000..2df2229 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/examples/single_socket.rs @@ -0,0 +1,65 @@ +//! This example demonstrates how to make multiple outgoing connections on a single UDP socket. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + error::Error, + net::{IpAddr, Ipv4Addr, SocketAddr}, +}; + +use quinn::Endpoint; + +mod common; +use common::{make_client_endpoint, make_server_endpoint}; +use rustls::pki_types::CertificateDer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5000); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5001); + let addr3 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5002); + let server1_cert = run_server(addr1)?; + let server2_cert = run_server(addr2)?; + let server3_cert = run_server(addr3)?; + + let client = make_client_endpoint( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + &[&server1_cert, &server2_cert, &server3_cert], + )?; + + // connect to multiple endpoints using the same socket/endpoint + tokio::join!( + run_client(&client, addr1), + run_client(&client, addr2), + run_client(&client, addr3), + ); + + // Make sure the server has a chance to clean up + client.wait_idle().await; + + Ok(()) +} + +/// Runs a QUIC server bound to given address and returns server certificate. +fn run_server( + addr: SocketAddr, +) -> Result, Box> { + let (endpoint, server_cert) = make_server_endpoint(addr)?; + // accept a single connection + tokio::spawn(async move { + let connection = endpoint.accept().await.unwrap().await.unwrap(); + println!( + "[server] incoming connection: addr={}", + connection.remote_address() + ); + }); + + Ok(server_cert) +} + +/// Attempt QUIC connection with the given server address. +async fn run_client(endpoint: &Endpoint, server_addr: SocketAddr) { + let connect = endpoint.connect(server_addr, "localhost").unwrap(); + let connection = connect.await.unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/connection.rs b/vendor/flutter_quic/rust/vendor/quinn/src/connection.rs new file mode 100644 index 0000000..62020a1 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/connection.rs @@ -0,0 +1,1356 @@ +use std::{ + any::Any, + fmt, + future::Future, + io, + net::{IpAddr, SocketAddr}, + pin::Pin, + sync::Arc, + task::{Context, Poll, Waker, ready}, +}; + +use bytes::Bytes; +use pin_project_lite::pin_project; +use rustc_hash::FxHashMap; +use thiserror::Error; +use tokio::sync::{Notify, futures::Notified, mpsc, oneshot}; +use tracing::{Instrument, Span, debug_span}; + +use crate::{ + ConnectionEvent, Duration, Instant, VarInt, + mutex::Mutex, + recv_stream::RecvStream, + runtime::{AsyncTimer, AsyncUdpSocket, Runtime, UdpPoller}, + send_stream::SendStream, + udp_transmit, +}; +use proto::{ + ConnectionError, ConnectionHandle, ConnectionStats, Dir, EndpointEvent, Side, StreamEvent, + StreamId, congestion::Controller, +}; + +/// In-progress connection attempt future +#[derive(Debug)] +pub struct Connecting { + conn: Option, + connected: oneshot::Receiver, + handshake_data_ready: Option>, +} + +impl Connecting { + pub(crate) fn new( + handle: ConnectionHandle, + conn: proto::Connection, + endpoint_events: mpsc::UnboundedSender<(ConnectionHandle, EndpointEvent)>, + conn_events: mpsc::UnboundedReceiver, + socket: Arc, + runtime: Arc, + ) -> Self { + let (on_handshake_data_send, on_handshake_data_recv) = oneshot::channel(); + let (on_connected_send, on_connected_recv) = oneshot::channel(); + let conn = ConnectionRef::new( + handle, + conn, + endpoint_events, + conn_events, + on_handshake_data_send, + on_connected_send, + socket, + runtime.clone(), + ); + + let driver = ConnectionDriver(conn.clone()); + runtime.spawn(Box::pin( + async { + if let Err(e) = driver.await { + tracing::error!("I/O error: {e}"); + } + } + .instrument(Span::current()), + )); + + Self { + conn: Some(conn), + connected: on_connected_recv, + handshake_data_ready: Some(on_handshake_data_recv), + } + } + + /// Convert into a 0-RTT or 0.5-RTT connection at the cost of weakened security + /// + /// Returns `Ok` immediately if the local endpoint is able to attempt sending 0/0.5-RTT data. + /// If so, the returned [`Connection`] can be used to send application data without waiting for + /// the rest of the handshake to complete, at the cost of weakened cryptographic security + /// guarantees. The returned [`ZeroRttAccepted`] future resolves when the handshake does + /// complete, at which point subsequently opened streams and written data will have full + /// cryptographic protection. + /// + /// ## Outgoing + /// + /// For outgoing connections, the initial attempt to convert to a [`Connection`] which sends + /// 0-RTT data will proceed if the [`crypto::ClientConfig`][crate::crypto::ClientConfig] + /// attempts to resume a previous TLS session. However, **the remote endpoint may not actually + /// _accept_ the 0-RTT data**--yet still accept the connection attempt in general. This + /// possibility is conveyed through the [`ZeroRttAccepted`] future--when the handshake + /// completes, it resolves to true if the 0-RTT data was accepted and false if it was rejected. + /// If it was rejected, the existence of streams opened and other application data sent prior + /// to the handshake completing will not be conveyed to the remote application, and local + /// operations on them will return `ZeroRttRejected` errors. + /// + /// A server may reject 0-RTT data at its discretion, but accepting 0-RTT data requires the + /// relevant resumption state to be stored in the server, which servers may limit or lose for + /// various reasons including not persisting resumption state across server restarts. + /// + /// If manually providing a [`crypto::ClientConfig`][crate::crypto::ClientConfig], check your + /// implementation's docs for 0-RTT pitfalls. + /// + /// ## Incoming + /// + /// For incoming connections, conversion to 0.5-RTT will always fully succeed. `into_0rtt` will + /// always return `Ok` and the [`ZeroRttAccepted`] will always resolve to true. + /// + /// If manually providing a [`crypto::ServerConfig`][crate::crypto::ServerConfig], check your + /// implementation's docs for 0-RTT pitfalls. + /// + /// ## Security + /// + /// On outgoing connections, this enables transmission of 0-RTT data, which is vulnerable to + /// replay attacks, and should therefore never invoke non-idempotent operations. + /// + /// On incoming connections, this enables transmission of 0.5-RTT data, which may be sent + /// before TLS client authentication has occurred, and should therefore not be used to send + /// data for which client authentication is being used. + pub fn into_0rtt(mut self) -> Result<(Connection, ZeroRttAccepted), Self> { + // This lock borrows `self` and would normally be dropped at the end of this scope, so we'll + // have to release it explicitly before returning `self` by value. + let conn = (self.conn.as_mut().unwrap()).state.lock("into_0rtt"); + + let is_ok = conn.inner.has_0rtt() || conn.inner.side().is_server(); + drop(conn); + + if is_ok { + let conn = self.conn.take().unwrap(); + Ok((Connection(conn), ZeroRttAccepted(self.connected))) + } else { + Err(self) + } + } + + /// Parameters negotiated during the handshake + /// + /// The dynamic type returned is determined by the configured + /// [`Session`](proto::crypto::Session). For the default `rustls` session, the return value can + /// be [`downcast`](Box::downcast) to a + /// [`crypto::rustls::HandshakeData`](crate::crypto::rustls::HandshakeData). + pub async fn handshake_data(&mut self) -> Result, ConnectionError> { + // Taking &mut self allows us to use a single oneshot channel rather than dealing with + // potentially many tasks waiting on the same event. It's a bit of a hack, but keeps things + // simple. + if let Some(x) = self.handshake_data_ready.take() { + let _ = x.await; + } + let conn = self.conn.as_ref().unwrap(); + let inner = conn.state.lock("handshake"); + inner + .inner + .crypto_session() + .handshake_data() + .ok_or_else(|| { + inner + .error + .clone() + .expect("spurious handshake data ready notification") + }) + } + + /// The local IP address which was used when the peer established + /// the connection + /// + /// This can be different from the address the endpoint is bound to, in case + /// the endpoint is bound to a wildcard address like `0.0.0.0` or `::`. + /// + /// This will return `None` for clients, or when the platform does not expose this + /// information. See [`quinn_udp::RecvMeta::dst_ip`](udp::RecvMeta::dst_ip) for a list of + /// supported platforms when using [`quinn_udp`](udp) for I/O, which is the default. + /// + /// Will panic if called after `poll` has returned `Ready`. + pub fn local_ip(&self) -> Option { + let conn = self.conn.as_ref().unwrap(); + let inner = conn.state.lock("local_ip"); + + inner.inner.local_ip() + } + + /// The peer's UDP address + /// + /// Will panic if called after `poll` has returned `Ready`. + pub fn remote_address(&self) -> SocketAddr { + let conn_ref: &ConnectionRef = self.conn.as_ref().expect("used after yielding Ready"); + conn_ref.state.lock("remote_address").inner.remote_address() + } +} + +impl Future for Connecting { + type Output = Result; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + Pin::new(&mut self.connected).poll(cx).map(|_| { + let conn = self.conn.take().unwrap(); + let inner = conn.state.lock("connecting"); + if inner.connected { + drop(inner); + Ok(Connection(conn)) + } else { + Err(inner + .error + .clone() + .expect("connected signaled without connection success or error")) + } + }) + } +} + +/// Future that completes when a connection is fully established +/// +/// For clients, the resulting value indicates if 0-RTT was accepted. For servers, the resulting +/// value is meaningless. +pub struct ZeroRttAccepted(oneshot::Receiver); + +impl Future for ZeroRttAccepted { + type Output = bool; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + Pin::new(&mut self.0).poll(cx).map(|x| x.unwrap_or(false)) + } +} + +/// A future that drives protocol logic for a connection +/// +/// This future handles the protocol logic for a single connection, routing events from the +/// `Connection` API object to the `Endpoint` task and the related stream-related interfaces. +/// It also keeps track of outstanding timeouts for the `Connection`. +/// +/// If the connection encounters an error condition, this future will yield an error. It will +/// terminate (yielding `Ok(())`) if the connection was closed without error. Unlike other +/// connection-related futures, this waits for the draining period to complete to ensure that +/// packets still in flight from the peer are handled gracefully. +#[must_use = "connection drivers must be spawned for their connections to function"] +#[derive(Debug)] +struct ConnectionDriver(ConnectionRef); + +impl Future for ConnectionDriver { + type Output = Result<(), io::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let conn = &mut *self.0.state.lock("poll"); + + let span = debug_span!("drive", id = conn.handle.0); + let _guard = span.enter(); + + if let Err(e) = conn.process_conn_events(&self.0.shared, cx) { + conn.terminate(e, &self.0.shared); + return Poll::Ready(Ok(())); + } + let mut keep_going = conn.drive_transmit(cx)?; + // If a timer expires, there might be more to transmit. When we transmit something, we + // might need to reset a timer. Hence, we must loop until neither happens. + keep_going |= conn.drive_timer(cx); + conn.forward_endpoint_events(); + conn.forward_app_events(&self.0.shared); + + if !conn.inner.is_drained() { + if keep_going { + // If the connection hasn't processed all tasks, schedule it again + cx.waker().wake_by_ref(); + } else { + conn.driver = Some(cx.waker().clone()); + } + return Poll::Pending; + } + if conn.error.is_none() { + unreachable!("drained connections always have an error"); + } + Poll::Ready(Ok(())) + } +} + +/// A QUIC connection. +/// +/// If all references to a connection (including every clone of the `Connection` handle, streams of +/// incoming streams, and the various stream types) have been dropped, then the connection will be +/// automatically closed with an `error_code` of 0 and an empty `reason`. You can also close the +/// connection explicitly by calling [`Connection::close()`]. +/// +/// Closing the connection immediately abandons efforts to deliver data to the peer. Upon +/// receiving CONNECTION_CLOSE the peer *may* drop any stream data not yet delivered to the +/// application. [`Connection::close()`] describes in more detail how to gracefully close a +/// connection without losing application data. +/// +/// May be cloned to obtain another handle to the same connection. +/// +/// [`Connection::close()`]: Connection::close +#[derive(Debug, Clone)] +pub struct Connection(ConnectionRef); + +impl Connection { + /// Initiate a new outgoing unidirectional stream. + /// + /// Streams are cheap and instantaneous to open unless blocked by flow control. As a + /// consequence, the peer won't be notified that a stream has been opened until the stream is + /// actually used. + pub fn open_uni(&self) -> OpenUni<'_> { + OpenUni { + conn: &self.0, + notify: self.0.shared.stream_budget_available[Dir::Uni as usize].notified(), + } + } + + /// Initiate a new outgoing bidirectional stream. + /// + /// Streams are cheap and instantaneous to open unless blocked by flow control. As a + /// consequence, the peer won't be notified that a stream has been opened until the stream is + /// actually used. Calling [`open_bi()`] then waiting on the [`RecvStream`] without writing + /// anything to [`SendStream`] will never succeed. + /// + /// [`open_bi()`]: crate::Connection::open_bi + /// [`SendStream`]: crate::SendStream + /// [`RecvStream`]: crate::RecvStream + pub fn open_bi(&self) -> OpenBi<'_> { + OpenBi { + conn: &self.0, + notify: self.0.shared.stream_budget_available[Dir::Bi as usize].notified(), + } + } + + /// Accept the next incoming uni-directional stream + pub fn accept_uni(&self) -> AcceptUni<'_> { + AcceptUni { + conn: &self.0, + notify: self.0.shared.stream_incoming[Dir::Uni as usize].notified(), + } + } + + /// Accept the next incoming bidirectional stream + /// + /// **Important Note**: The `Connection` that calls [`open_bi()`] must write to its [`SendStream`] + /// before the other `Connection` is able to `accept_bi()`. Calling [`open_bi()`] then + /// waiting on the [`RecvStream`] without writing anything to [`SendStream`] will never succeed. + /// + /// [`accept_bi()`]: crate::Connection::accept_bi + /// [`open_bi()`]: crate::Connection::open_bi + /// [`SendStream`]: crate::SendStream + /// [`RecvStream`]: crate::RecvStream + pub fn accept_bi(&self) -> AcceptBi<'_> { + AcceptBi { + conn: &self.0, + notify: self.0.shared.stream_incoming[Dir::Bi as usize].notified(), + } + } + + /// Receive an application datagram + pub fn read_datagram(&self) -> ReadDatagram<'_> { + ReadDatagram { + conn: &self.0, + notify: self.0.shared.datagram_received.notified(), + } + } + + /// Wait for the connection to be closed for any reason + /// + /// Despite the return type's name, closed connections are often not an error condition at the + /// application layer. Cases that might be routine include [`ConnectionError::LocallyClosed`] + /// and [`ConnectionError::ApplicationClosed`]. + pub async fn closed(&self) -> ConnectionError { + { + let conn = self.0.state.lock("closed"); + if let Some(error) = conn.error.as_ref() { + return error.clone(); + } + // Construct the future while the lock is held to ensure we can't miss a wakeup if + // the `Notify` is signaled immediately after we release the lock. `await` it after + // the lock guard is out of scope. + self.0.shared.closed.notified() + } + .await; + self.0 + .state + .lock("closed") + .error + .as_ref() + .expect("closed without an error") + .clone() + } + + /// If the connection is closed, the reason why. + /// + /// Returns `None` if the connection is still open. + pub fn close_reason(&self) -> Option { + self.0.state.lock("close_reason").error.clone() + } + + /// Close the connection immediately. + /// + /// Pending operations will fail immediately with [`ConnectionError::LocallyClosed`]. No + /// more data is sent to the peer and the peer may drop buffered data upon receiving + /// the CONNECTION_CLOSE frame. + /// + /// `error_code` and `reason` are not interpreted, and are provided directly to the peer. + /// + /// `reason` will be truncated to fit in a single packet with overhead; to improve odds that it + /// is preserved in full, it should be kept under 1KiB. + /// + /// # Gracefully closing a connection + /// + /// Only the peer last receiving application data can be certain that all data is + /// delivered. The only reliable action it can then take is to close the connection, + /// potentially with a custom error code. The delivery of the final CONNECTION_CLOSE + /// frame is very likely if both endpoints stay online long enough, and + /// [`Endpoint::wait_idle()`] can be used to provide sufficient time. Otherwise, the + /// remote peer will time out the connection, provided that the idle timeout is not + /// disabled. + /// + /// The sending side can not guarantee all stream data is delivered to the remote + /// application. It only knows the data is delivered to the QUIC stack of the remote + /// endpoint. Once the local side sends a CONNECTION_CLOSE frame in response to calling + /// [`close()`] the remote endpoint may drop any data it received but is as yet + /// undelivered to the application, including data that was acknowledged as received to + /// the local endpoint. + /// + /// [`ConnectionError::LocallyClosed`]: crate::ConnectionError::LocallyClosed + /// [`Endpoint::wait_idle()`]: crate::Endpoint::wait_idle + /// [`close()`]: Connection::close + pub fn close(&self, error_code: VarInt, reason: &[u8]) { + let conn = &mut *self.0.state.lock("close"); + conn.close(error_code, Bytes::copy_from_slice(reason), &self.0.shared); + } + + /// Transmit `data` as an unreliable, unordered application datagram + /// + /// Application datagrams are a low-level primitive. They may be lost or delivered out of order, + /// and `data` must both fit inside a single QUIC packet and be smaller than the maximum + /// dictated by the peer. + /// + /// Previously queued datagrams which are still unsent may be discarded to make space for this + /// datagram, in order of oldest to newest. + pub fn send_datagram(&self, data: Bytes) -> Result<(), SendDatagramError> { + let conn = &mut *self.0.state.lock("send_datagram"); + if let Some(ref x) = conn.error { + return Err(SendDatagramError::ConnectionLost(x.clone())); + } + use proto::SendDatagramError::*; + match conn.inner.datagrams().send(data, true) { + Ok(()) => { + conn.wake(); + Ok(()) + } + Err(e) => Err(match e { + Blocked(..) => unreachable!(), + UnsupportedByPeer => SendDatagramError::UnsupportedByPeer, + Disabled => SendDatagramError::Disabled, + TooLarge => SendDatagramError::TooLarge, + }), + } + } + + /// Transmit `data` as an unreliable, unordered application datagram + /// + /// Unlike [`send_datagram()`], this method will wait for buffer space during congestion + /// conditions, which effectively prioritizes old datagrams over new datagrams. + /// + /// See [`send_datagram()`] for details. + /// + /// [`send_datagram()`]: Connection::send_datagram + pub fn send_datagram_wait(&self, data: Bytes) -> SendDatagram<'_> { + SendDatagram { + conn: &self.0, + data: Some(data), + notify: self.0.shared.datagrams_unblocked.notified(), + } + } + + /// Compute the maximum size of datagrams that may be passed to [`send_datagram()`]. + /// + /// Returns `None` if datagrams are unsupported by the peer or disabled locally. + /// + /// This may change over the lifetime of a connection according to variation in the path MTU + /// estimate. The peer can also enforce an arbitrarily small fixed limit, but if the peer's + /// limit is large this is guaranteed to be a little over a kilobyte at minimum. + /// + /// Not necessarily the maximum size of received datagrams. + /// + /// [`send_datagram()`]: Connection::send_datagram + pub fn max_datagram_size(&self) -> Option { + self.0 + .state + .lock("max_datagram_size") + .inner + .datagrams() + .max_size() + } + + /// Bytes available in the outgoing datagram buffer + /// + /// When greater than zero, calling [`send_datagram()`](Self::send_datagram) with a datagram of + /// at most this size is guaranteed not to cause older datagrams to be dropped. + pub fn datagram_send_buffer_space(&self) -> usize { + self.0 + .state + .lock("datagram_send_buffer_space") + .inner + .datagrams() + .send_buffer_space() + } + + /// The side of the connection (client or server) + pub fn side(&self) -> Side { + self.0.state.lock("side").inner.side() + } + + /// The peer's UDP address + /// + /// If `ServerConfig::migration` is `true`, clients may change addresses at will, e.g. when + /// switching to a cellular internet connection. + pub fn remote_address(&self) -> SocketAddr { + self.0.state.lock("remote_address").inner.remote_address() + } + + /// The local IP address which was used when the peer established + /// the connection + /// + /// This can be different from the address the endpoint is bound to, in case + /// the endpoint is bound to a wildcard address like `0.0.0.0` or `::`. + /// + /// This will return `None` for clients, or when the platform does not expose this + /// information. See [`quinn_udp::RecvMeta::dst_ip`](udp::RecvMeta::dst_ip) for a list of + /// supported platforms when using [`quinn_udp`](udp) for I/O, which is the default. + pub fn local_ip(&self) -> Option { + self.0.state.lock("local_ip").inner.local_ip() + } + + /// Current best estimate of this connection's latency (round-trip-time) + pub fn rtt(&self) -> Duration { + self.0.state.lock("rtt").inner.rtt() + } + + /// Returns connection statistics + pub fn stats(&self) -> ConnectionStats { + self.0.state.lock("stats").inner.stats() + } + + /// The peer's advertised `initial_max_streams_bidi` transport parameter. + /// + /// See [`proto::Connection::peer_params_initial_max_streams_bidi`]. + pub fn peer_params_initial_max_streams_bidi(&self) -> u64 { + self.0 + .state + .lock("peer_params_initial_max_streams_bidi") + .inner + .peer_params_initial_max_streams_bidi() + } + + /// The peer's advertised `initial_max_streams_uni` transport parameter. + pub fn peer_params_initial_max_streams_uni(&self) -> u64 { + self.0 + .state + .lock("peer_params_initial_max_streams_uni") + .inner + .peer_params_initial_max_streams_uni() + } + + /// The peer's advertised `initial_max_data` transport parameter. + pub fn peer_params_initial_max_data(&self) -> u64 { + self.0 + .state + .lock("peer_params_initial_max_data") + .inner + .peer_params_initial_max_data() + } + + /// Current state of the congestion control algorithm, for debugging purposes + pub fn congestion_state(&self) -> Box { + self.0 + .state + .lock("congestion_state") + .inner + .congestion_state() + .clone_box() + } + + /// Parameters negotiated during the handshake + /// + /// Guaranteed to return `Some` on fully established connections or after + /// [`Connecting::handshake_data()`] succeeds. See that method's documentations for details on + /// the returned value. + /// + /// [`Connection::handshake_data()`]: crate::Connecting::handshake_data + pub fn handshake_data(&self) -> Option> { + self.0 + .state + .lock("handshake_data") + .inner + .crypto_session() + .handshake_data() + } + + /// Cryptographic identity of the peer + /// + /// The dynamic type returned is determined by the configured + /// [`Session`](proto::crypto::Session). For the default `rustls` session, the return value can + /// be [`downcast`](Box::downcast) to a Vec<[rustls::pki_types::CertificateDer]> + pub fn peer_identity(&self) -> Option> { + self.0 + .state + .lock("peer_identity") + .inner + .crypto_session() + .peer_identity() + } + + /// A stable identifier for this connection + /// + /// Peer addresses and connection IDs can change, but this value will remain + /// fixed for the lifetime of the connection. + pub fn stable_id(&self) -> usize { + self.0.stable_id() + } + + /// Update traffic keys spontaneously + /// + /// This primarily exists for testing purposes. + pub fn force_key_update(&self) { + self.0 + .state + .lock("force_key_update") + .inner + .force_key_update() + } + + /// Derive keying material from this connection's TLS session secrets. + /// + /// When both peers call this method with the same `label` and `context` + /// arguments and `output` buffers of equal length, they will get the + /// same sequence of bytes in `output`. These bytes are cryptographically + /// strong and pseudorandom, and are suitable for use as keying material. + /// + /// See [RFC5705](https://tools.ietf.org/html/rfc5705) for more information. + pub fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> Result<(), proto::crypto::ExportKeyingMaterialError> { + self.0 + .state + .lock("export_keying_material") + .inner + .crypto_session() + .export_keying_material(output, label, context) + } + + /// Modify the number of remotely initiated unidirectional streams that may be concurrently open + /// + /// No streams may be opened by the peer unless fewer than `count` are already open. Large + /// `count`s increase both minimum and worst-case memory consumption. + pub fn set_max_concurrent_uni_streams(&self, count: VarInt) { + let mut conn = self.0.state.lock("set_max_concurrent_uni_streams"); + conn.inner.set_max_concurrent_streams(Dir::Uni, count); + // May need to send MAX_STREAMS to make progress + conn.wake(); + } + + /// See [`proto::TransportConfig::send_window()`] + pub fn set_send_window(&self, send_window: u64) { + let mut conn = self.0.state.lock("set_send_window"); + conn.inner.set_send_window(send_window); + conn.wake(); + } + + /// See [`proto::TransportConfig::receive_window()`] + pub fn set_receive_window(&self, receive_window: VarInt) { + let mut conn = self.0.state.lock("set_receive_window"); + conn.inner.set_receive_window(receive_window); + conn.wake(); + } + + /// Modify the number of remotely initiated bidirectional streams that may be concurrently open + /// + /// No streams may be opened by the peer unless fewer than `count` are already open. Large + /// `count`s increase both minimum and worst-case memory consumption. + pub fn set_max_concurrent_bi_streams(&self, count: VarInt) { + let mut conn = self.0.state.lock("set_max_concurrent_bi_streams"); + conn.inner.set_max_concurrent_streams(Dir::Bi, count); + // May need to send MAX_STREAMS to make progress + conn.wake(); + } +} + +pin_project! { + /// Future produced by [`Connection::open_uni`] + pub struct OpenUni<'a> { + conn: &'a ConnectionRef, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for OpenUni<'_> { + type Output = Result; + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let this = self.project(); + let (conn, id, is_0rtt) = ready!(poll_open(ctx, this.conn, this.notify, Dir::Uni))?; + Poll::Ready(Ok(SendStream::new(conn, id, is_0rtt))) + } +} + +pin_project! { + /// Future produced by [`Connection::open_bi`] + pub struct OpenBi<'a> { + conn: &'a ConnectionRef, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for OpenBi<'_> { + type Output = Result<(SendStream, RecvStream), ConnectionError>; + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let this = self.project(); + let (conn, id, is_0rtt) = ready!(poll_open(ctx, this.conn, this.notify, Dir::Bi))?; + + Poll::Ready(Ok(( + SendStream::new(conn.clone(), id, is_0rtt), + RecvStream::new(conn, id, is_0rtt), + ))) + } +} + +fn poll_open<'a>( + ctx: &mut Context<'_>, + conn: &'a ConnectionRef, + mut notify: Pin<&mut Notified<'a>>, + dir: Dir, +) -> Poll> { + let mut state = conn.state.lock("poll_open"); + if let Some(ref e) = state.error { + return Poll::Ready(Err(e.clone())); + } else if let Some(id) = state.inner.streams().open(dir) { + let is_0rtt = state.inner.side().is_client() && state.inner.is_handshaking(); + drop(state); // Release the lock so clone can take it + return Poll::Ready(Ok((conn.clone(), id, is_0rtt))); + } + loop { + match notify.as_mut().poll(ctx) { + // `state` lock ensures we didn't race with readiness + Poll::Pending => return Poll::Pending, + // Spurious wakeup, get a new future + Poll::Ready(()) => { + notify.set(conn.shared.stream_budget_available[dir as usize].notified()) + } + } + } +} + +pin_project! { + /// Future produced by [`Connection::accept_uni`] + pub struct AcceptUni<'a> { + conn: &'a ConnectionRef, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for AcceptUni<'_> { + type Output = Result; + + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let this = self.project(); + let (conn, id, is_0rtt) = ready!(poll_accept(ctx, this.conn, this.notify, Dir::Uni))?; + Poll::Ready(Ok(RecvStream::new(conn, id, is_0rtt))) + } +} + +pin_project! { + /// Future produced by [`Connection::accept_bi`] + pub struct AcceptBi<'a> { + conn: &'a ConnectionRef, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for AcceptBi<'_> { + type Output = Result<(SendStream, RecvStream), ConnectionError>; + + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let this = self.project(); + let (conn, id, is_0rtt) = ready!(poll_accept(ctx, this.conn, this.notify, Dir::Bi))?; + Poll::Ready(Ok(( + SendStream::new(conn.clone(), id, is_0rtt), + RecvStream::new(conn, id, is_0rtt), + ))) + } +} + +fn poll_accept<'a>( + ctx: &mut Context<'_>, + conn: &'a ConnectionRef, + mut notify: Pin<&mut Notified<'a>>, + dir: Dir, +) -> Poll> { + let mut state = conn.state.lock("poll_accept"); + // Check for incoming streams before checking `state.error` so that already-received streams, + // which are necessarily finite, can be drained from a closed connection. + if let Some(id) = state.inner.streams().accept(dir) { + let is_0rtt = state.inner.is_handshaking(); + state.wake(); // To send additional stream ID credit + drop(state); // Release the lock so clone can take it + return Poll::Ready(Ok((conn.clone(), id, is_0rtt))); + } else if let Some(ref e) = state.error { + return Poll::Ready(Err(e.clone())); + } + loop { + match notify.as_mut().poll(ctx) { + // `state` lock ensures we didn't race with readiness + Poll::Pending => return Poll::Pending, + // Spurious wakeup, get a new future + Poll::Ready(()) => notify.set(conn.shared.stream_incoming[dir as usize].notified()), + } + } +} + +pin_project! { + /// Future produced by [`Connection::read_datagram`] + pub struct ReadDatagram<'a> { + conn: &'a ConnectionRef, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for ReadDatagram<'_> { + type Output = Result; + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + let mut state = this.conn.state.lock("ReadDatagram::poll"); + // Check for buffered datagrams before checking `state.error` so that already-received + // datagrams, which are necessarily finite, can be drained from a closed connection. + if let Some(x) = state.inner.datagrams().recv() { + return Poll::Ready(Ok(x)); + } else if let Some(ref e) = state.error { + return Poll::Ready(Err(e.clone())); + } + loop { + match this.notify.as_mut().poll(ctx) { + // `state` lock ensures we didn't race with readiness + Poll::Pending => return Poll::Pending, + // Spurious wakeup, get a new future + Poll::Ready(()) => this + .notify + .set(this.conn.shared.datagram_received.notified()), + } + } + } +} + +pin_project! { + /// Future produced by [`Connection::send_datagram_wait`] + pub struct SendDatagram<'a> { + conn: &'a ConnectionRef, + data: Option, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for SendDatagram<'_> { + type Output = Result<(), SendDatagramError>; + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + let mut state = this.conn.state.lock("SendDatagram::poll"); + if let Some(ref e) = state.error { + return Poll::Ready(Err(SendDatagramError::ConnectionLost(e.clone()))); + } + use proto::SendDatagramError::*; + match state + .inner + .datagrams() + .send(this.data.take().unwrap(), false) + { + Ok(()) => { + state.wake(); + Poll::Ready(Ok(())) + } + Err(e) => Poll::Ready(Err(match e { + Blocked(data) => { + this.data.replace(data); + loop { + match this.notify.as_mut().poll(ctx) { + Poll::Pending => return Poll::Pending, + // Spurious wakeup, get a new future + Poll::Ready(()) => this + .notify + .set(this.conn.shared.datagrams_unblocked.notified()), + } + } + } + UnsupportedByPeer => SendDatagramError::UnsupportedByPeer, + Disabled => SendDatagramError::Disabled, + TooLarge => SendDatagramError::TooLarge, + })), + } + } +} + +#[derive(Debug)] +pub(crate) struct ConnectionRef(Arc); + +impl ConnectionRef { + #[allow(clippy::too_many_arguments)] + fn new( + handle: ConnectionHandle, + conn: proto::Connection, + endpoint_events: mpsc::UnboundedSender<(ConnectionHandle, EndpointEvent)>, + conn_events: mpsc::UnboundedReceiver, + on_handshake_data: oneshot::Sender<()>, + on_connected: oneshot::Sender, + socket: Arc, + runtime: Arc, + ) -> Self { + Self(Arc::new(ConnectionInner { + state: Mutex::new(State { + inner: conn, + driver: None, + handle, + on_handshake_data: Some(on_handshake_data), + on_connected: Some(on_connected), + connected: false, + timer: None, + timer_deadline: None, + conn_events, + endpoint_events, + blocked_writers: FxHashMap::default(), + blocked_readers: FxHashMap::default(), + stopped: FxHashMap::default(), + error: None, + ref_count: 0, + io_poller: socket.clone().create_io_poller(), + socket, + runtime, + send_buffer: Vec::new(), + buffered_transmit: None, + }), + shared: Shared::default(), + })) + } + + fn stable_id(&self) -> usize { + &*self.0 as *const _ as usize + } +} + +impl Clone for ConnectionRef { + fn clone(&self) -> Self { + self.state.lock("clone").ref_count += 1; + Self(self.0.clone()) + } +} + +impl Drop for ConnectionRef { + fn drop(&mut self) { + let conn = &mut *self.state.lock("drop"); + if let Some(x) = conn.ref_count.checked_sub(1) { + conn.ref_count = x; + if x == 0 && !conn.inner.is_closed() { + // If the driver is alive, it's just it and us, so we'd better shut it down. If it's + // not, we can't do any harm. If there were any streams being opened, then either + // the connection will be closed for an unrelated reason or a fresh reference will + // be constructed for the newly opened stream. + conn.implicit_close(&self.shared); + } + } + } +} + +impl std::ops::Deref for ConnectionRef { + type Target = ConnectionInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub(crate) struct ConnectionInner { + pub(crate) state: Mutex, + pub(crate) shared: Shared, +} + +#[derive(Debug, Default)] +pub(crate) struct Shared { + /// Notified when new streams may be locally initiated due to an increase in stream ID flow + /// control budget + stream_budget_available: [Notify; 2], + /// Notified when the peer has initiated a new stream + stream_incoming: [Notify; 2], + datagram_received: Notify, + datagrams_unblocked: Notify, + closed: Notify, +} + +pub(crate) struct State { + pub(crate) inner: proto::Connection, + driver: Option, + handle: ConnectionHandle, + on_handshake_data: Option>, + on_connected: Option>, + connected: bool, + timer: Option>>, + timer_deadline: Option, + conn_events: mpsc::UnboundedReceiver, + endpoint_events: mpsc::UnboundedSender<(ConnectionHandle, EndpointEvent)>, + pub(crate) blocked_writers: FxHashMap, + pub(crate) blocked_readers: FxHashMap, + pub(crate) stopped: FxHashMap>, + /// Always set to Some before the connection becomes drained + pub(crate) error: Option, + /// Number of live handles that can be used to initiate or handle I/O; excludes the driver + ref_count: usize, + socket: Arc, + io_poller: Pin>, + runtime: Arc, + send_buffer: Vec, + /// We buffer a transmit when the underlying I/O would block + buffered_transmit: Option, +} + +impl State { + fn drive_transmit(&mut self, cx: &mut Context) -> io::Result { + let now = self.runtime.now(); + let mut transmits = 0; + + let max_datagrams = self + .socket + .max_transmit_segments() + .min(MAX_TRANSMIT_SEGMENTS); + + loop { + // Retry the last transmit, or get a new one. + let t = match self.buffered_transmit.take() { + Some(t) => t, + None => { + self.send_buffer.clear(); + self.send_buffer.reserve(self.inner.current_mtu() as usize); + match self + .inner + .poll_transmit(now, max_datagrams, &mut self.send_buffer) + { + Some(t) => { + transmits += match t.segment_size { + None => 1, + Some(s) => t.size.div_ceil(s), // round up + }; + t + } + None => break, + } + } + }; + + if self.io_poller.as_mut().poll_writable(cx)?.is_pending() { + // Retry after a future wakeup + self.buffered_transmit = Some(t); + return Ok(false); + } + + let len = t.size; + let retry = match self + .socket + .try_send(&udp_transmit(&t, &self.send_buffer[..len])) + { + Ok(()) => false, + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => true, + Err(e) => return Err(e), + }; + if retry { + // We thought the socket was writable, but it wasn't. Retry so that either another + // `poll_writable` call determines that the socket is indeed not writable and + // registers us for a wakeup, or the send succeeds if this really was just a + // transient failure. + self.buffered_transmit = Some(t); + continue; + } + + if transmits >= MAX_TRANSMIT_DATAGRAMS { + // TODO: What isn't ideal here yet is that if we don't poll all + // datagrams that could be sent we don't go into the `app_limited` + // state and CWND continues to grow until we get here the next time. + // See https://github.com/quinn-rs/quinn/issues/1126 + return Ok(true); + } + } + + Ok(false) + } + + fn forward_endpoint_events(&mut self) { + while let Some(event) = self.inner.poll_endpoint_events() { + // If the endpoint driver is gone, noop. + let _ = self.endpoint_events.send((self.handle, event)); + } + } + + /// If this returns `Err`, the endpoint is dead, so the driver should exit immediately. + fn process_conn_events( + &mut self, + shared: &Shared, + cx: &mut Context, + ) -> Result<(), ConnectionError> { + loop { + match self.conn_events.poll_recv(cx) { + Poll::Ready(Some(ConnectionEvent::Rebind(socket))) => { + self.socket = socket; + self.io_poller = self.socket.clone().create_io_poller(); + self.inner.local_address_changed(); + } + Poll::Ready(Some(ConnectionEvent::Proto(event))) => { + self.inner.handle_event(event); + } + Poll::Ready(Some(ConnectionEvent::Close { reason, error_code })) => { + self.close(error_code, reason, shared); + } + Poll::Ready(None) => { + return Err(ConnectionError::TransportError(proto::TransportError { + code: proto::TransportErrorCode::INTERNAL_ERROR, + frame: None, + reason: "endpoint driver future was dropped".to_string(), + })); + } + Poll::Pending => { + return Ok(()); + } + } + } + } + + fn forward_app_events(&mut self, shared: &Shared) { + while let Some(event) = self.inner.poll() { + use proto::Event::*; + match event { + HandshakeDataReady => { + if let Some(x) = self.on_handshake_data.take() { + let _ = x.send(()); + } + } + Connected => { + self.connected = true; + if let Some(x) = self.on_connected.take() { + // We don't care if the on-connected future was dropped + let _ = x.send(self.inner.accepted_0rtt()); + } + if self.inner.side().is_client() && !self.inner.accepted_0rtt() { + // Wake up rejected 0-RTT streams so they can fail immediately with + // `ZeroRttRejected` errors. + wake_all(&mut self.blocked_writers); + wake_all(&mut self.blocked_readers); + wake_all_notify(&mut self.stopped); + } + } + ConnectionLost { reason } => { + self.terminate(reason, shared); + } + Stream(StreamEvent::Writable { id }) => wake_stream(id, &mut self.blocked_writers), + Stream(StreamEvent::Opened { dir: Dir::Uni }) => { + shared.stream_incoming[Dir::Uni as usize].notify_waiters(); + } + Stream(StreamEvent::Opened { dir: Dir::Bi }) => { + shared.stream_incoming[Dir::Bi as usize].notify_waiters(); + } + DatagramReceived => { + shared.datagram_received.notify_waiters(); + } + DatagramsUnblocked => { + shared.datagrams_unblocked.notify_waiters(); + } + Stream(StreamEvent::Readable { id }) => wake_stream(id, &mut self.blocked_readers), + Stream(StreamEvent::Available { dir }) => { + // Might mean any number of streams are ready, so we wake up everyone + shared.stream_budget_available[dir as usize].notify_waiters(); + } + Stream(StreamEvent::Finished { id }) => wake_stream_notify(id, &mut self.stopped), + Stream(StreamEvent::Stopped { id, .. }) => { + wake_stream_notify(id, &mut self.stopped); + wake_stream(id, &mut self.blocked_writers); + } + } + } + } + + fn drive_timer(&mut self, cx: &mut Context) -> bool { + // Check whether we need to (re)set the timer. If so, we must poll again to ensure the + // timer is registered with the runtime (and check whether it's already + // expired). + match self.inner.poll_timeout() { + Some(deadline) => { + if let Some(delay) = &mut self.timer { + // There is no need to reset the tokio timer if the deadline + // did not change + if self + .timer_deadline + .map(|current_deadline| current_deadline != deadline) + .unwrap_or(true) + { + delay.as_mut().reset(deadline); + } + } else { + self.timer = Some(self.runtime.new_timer(deadline)); + } + // Store the actual expiration time of the timer + self.timer_deadline = Some(deadline); + } + None => { + self.timer_deadline = None; + return false; + } + } + + if self.timer_deadline.is_none() { + return false; + } + + let delay = self + .timer + .as_mut() + .expect("timer must exist in this state") + .as_mut(); + if delay.poll(cx).is_pending() { + // Since there wasn't a timeout event, there is nothing new + // for the connection to do + return false; + } + + // A timer expired, so the caller needs to check for + // new transmits, which might cause new timers to be set. + self.inner.handle_timeout(self.runtime.now()); + self.timer_deadline = None; + true + } + + /// Wake up a blocked `Driver` task to process I/O + pub(crate) fn wake(&mut self) { + if let Some(x) = self.driver.take() { + x.wake(); + } + } + + /// Used to wake up all blocked futures when the connection becomes closed for any reason + fn terminate(&mut self, reason: ConnectionError, shared: &Shared) { + self.error = Some(reason.clone()); + if let Some(x) = self.on_handshake_data.take() { + let _ = x.send(()); + } + wake_all(&mut self.blocked_writers); + wake_all(&mut self.blocked_readers); + shared.stream_budget_available[Dir::Uni as usize].notify_waiters(); + shared.stream_budget_available[Dir::Bi as usize].notify_waiters(); + shared.stream_incoming[Dir::Uni as usize].notify_waiters(); + shared.stream_incoming[Dir::Bi as usize].notify_waiters(); + shared.datagram_received.notify_waiters(); + shared.datagrams_unblocked.notify_waiters(); + if let Some(x) = self.on_connected.take() { + let _ = x.send(false); + } + wake_all_notify(&mut self.stopped); + shared.closed.notify_waiters(); + } + + fn close(&mut self, error_code: VarInt, reason: Bytes, shared: &Shared) { + self.inner.close(self.runtime.now(), error_code, reason); + self.terminate(ConnectionError::LocallyClosed, shared); + self.wake(); + } + + /// Close for a reason other than the application's explicit request + pub(crate) fn implicit_close(&mut self, shared: &Shared) { + self.close(0u32.into(), Bytes::new(), shared); + } + + pub(crate) fn check_0rtt(&self) -> Result<(), ()> { + if self.inner.is_handshaking() + || self.inner.accepted_0rtt() + || self.inner.side().is_server() + { + Ok(()) + } else { + Err(()) + } + } +} + +impl Drop for State { + fn drop(&mut self) { + if !self.inner.is_drained() { + // Ensure the endpoint can tidy up + let _ = self + .endpoint_events + .send((self.handle, proto::EndpointEvent::drained())); + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("State").field("inner", &self.inner).finish() + } +} + +fn wake_stream(stream_id: StreamId, wakers: &mut FxHashMap) { + if let Some(waker) = wakers.remove(&stream_id) { + waker.wake(); + } +} + +fn wake_all(wakers: &mut FxHashMap) { + wakers.drain().for_each(|(_, waker)| waker.wake()) +} + +fn wake_stream_notify(stream_id: StreamId, wakers: &mut FxHashMap>) { + if let Some(notify) = wakers.remove(&stream_id) { + notify.notify_waiters() + } +} + +fn wake_all_notify(wakers: &mut FxHashMap>) { + wakers + .drain() + .for_each(|(_, notify)| notify.notify_waiters()) +} + +/// Errors that can arise when sending a datagram +#[derive(Debug, Error, Clone, Eq, PartialEq)] +pub enum SendDatagramError { + /// The peer does not support receiving datagram frames + #[error("datagrams not supported by peer")] + UnsupportedByPeer, + /// Datagram support is disabled locally + #[error("datagram support disabled")] + Disabled, + /// The datagram is larger than the connection can currently accommodate + /// + /// Indicates that the path MTU minus overhead or the limit advertised by the peer has been + /// exceeded. + #[error("datagram too large")] + TooLarge, + /// The connection was lost + #[error("connection lost")] + ConnectionLost(#[from] ConnectionError), +} + +/// The maximum amount of datagrams which will be produced in a single `drive_transmit` call +/// +/// This limits the amount of CPU resources consumed by datagram generation, +/// and allows other tasks (like receiving ACKs) to run in between. +const MAX_TRANSMIT_DATAGRAMS: usize = 20; + +/// The maximum amount of datagrams that are sent in a single transmit +/// +/// This can be lower than the maximum platform capabilities, to avoid excessive +/// memory allocations when calling `poll_transmit()`. Benchmarks have shown +/// that numbers around 10 are a good compromise. +const MAX_TRANSMIT_SEGMENTS: usize = 10; diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/endpoint.rs b/vendor/flutter_quic/rust/vendor/quinn/src/endpoint.rs new file mode 100644 index 0000000..d6879d2 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/endpoint.rs @@ -0,0 +1,875 @@ +use std::{ + collections::VecDeque, + fmt, + future::Future, + io, + io::IoSliceMut, + mem, + net::{SocketAddr, SocketAddrV6}, + pin::Pin, + str, + sync::{Arc, Mutex}, + task::{Context, Poll, Waker}, +}; + +#[cfg(all(not(wasm_browser), any(feature = "aws-lc-rs", feature = "ring")))] +use crate::runtime::default_runtime; +use crate::{ + Instant, + runtime::{AsyncUdpSocket, Runtime}, + udp_transmit, +}; +use bytes::{Bytes, BytesMut}; +use pin_project_lite::pin_project; +use proto::{ + self as proto, ClientConfig, ConnectError, ConnectionError, ConnectionHandle, DatagramEvent, + EndpointEvent, ServerConfig, +}; +use rustc_hash::FxHashMap; +#[cfg(all(not(wasm_browser), any(feature = "aws-lc-rs", feature = "ring"),))] +use socket2::{Domain, Protocol, Socket, Type}; +use tokio::sync::{Notify, futures::Notified, mpsc}; +use tracing::{Instrument, Span}; +use udp::{BATCH_SIZE, RecvMeta}; + +use crate::{ + ConnectionEvent, EndpointConfig, IO_LOOP_BOUND, RECV_TIME_BOUND, VarInt, + connection::Connecting, incoming::Incoming, work_limiter::WorkLimiter, +}; + +/// A QUIC endpoint. +/// +/// An endpoint corresponds to a single UDP socket, may host many connections, and may act as both +/// client and server for different connections. +/// +/// May be cloned to obtain another handle to the same endpoint. +#[derive(Debug, Clone)] +pub struct Endpoint { + pub(crate) inner: EndpointRef, + pub(crate) default_client_config: Option, + runtime: Arc, +} + +impl Endpoint { + /// Helper to construct an endpoint for use with outgoing connections only + /// + /// Note that `addr` is the *local* address to bind to, which should usually be a wildcard + /// address like `0.0.0.0:0` or `[::]:0`, which allow communication with any reachable IPv4 or + /// IPv6 address respectively from an OS-assigned port. + /// + /// If an IPv6 address is provided, attempts to make the socket dual-stack so as to allow + /// communication with both IPv4 and IPv6 addresses. As such, calling `Endpoint::client` with + /// the address `[::]:0` is a reasonable default to maximize the ability to connect to other + /// address. For example: + /// + /// ``` + /// quinn::Endpoint::client((std::net::Ipv6Addr::UNSPECIFIED, 0).into()); + /// ``` + /// + /// Some environments may not allow creation of dual-stack sockets, in which case an IPv6 + /// client will only be able to connect to IPv6 servers. An IPv4 client is never dual-stack. + #[cfg(all(not(wasm_browser), any(feature = "aws-lc-rs", feature = "ring")))] // `EndpointConfig::default()` is only available with these + pub fn client(addr: SocketAddr) -> io::Result { + let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))?; + if addr.is_ipv6() { + if let Err(e) = socket.set_only_v6(false) { + tracing::debug!(%e, "unable to make socket dual-stack"); + } + } + socket.bind(&addr.into())?; + let runtime = + default_runtime().ok_or_else(|| io::Error::other("no async runtime found"))?; + Self::new_with_abstract_socket( + EndpointConfig::default(), + None, + runtime.wrap_udp_socket(socket.into())?, + runtime, + ) + } + + /// Returns relevant stats from this Endpoint + pub fn stats(&self) -> EndpointStats { + self.inner.state.lock().unwrap().stats + } + + /// Helper to construct an endpoint for use with both incoming and outgoing connections + /// + /// Platform defaults for dual-stack sockets vary. For example, any socket bound to a wildcard + /// IPv6 address on Windows will not by default be able to communicate with IPv4 + /// addresses. Portable applications should bind an address that matches the family they wish to + /// communicate within. + #[cfg(all(not(wasm_browser), any(feature = "aws-lc-rs", feature = "ring")))] // `EndpointConfig::default()` is only available with these + pub fn server(config: ServerConfig, addr: SocketAddr) -> io::Result { + let socket = std::net::UdpSocket::bind(addr)?; + let runtime = + default_runtime().ok_or_else(|| io::Error::other("no async runtime found"))?; + Self::new_with_abstract_socket( + EndpointConfig::default(), + Some(config), + runtime.wrap_udp_socket(socket)?, + runtime, + ) + } + + /// Construct an endpoint with arbitrary configuration and socket + #[cfg(not(wasm_browser))] + pub fn new( + config: EndpointConfig, + server_config: Option, + socket: std::net::UdpSocket, + runtime: Arc, + ) -> io::Result { + let socket = runtime.wrap_udp_socket(socket)?; + Self::new_with_abstract_socket(config, server_config, socket, runtime) + } + + /// Construct an endpoint with arbitrary configuration and pre-constructed abstract socket + /// + /// Useful when `socket` has additional state (e.g. sidechannels) attached for which shared + /// ownership is needed. + pub fn new_with_abstract_socket( + config: EndpointConfig, + server_config: Option, + socket: Arc, + runtime: Arc, + ) -> io::Result { + let addr = socket.local_addr()?; + let allow_mtud = !socket.may_fragment(); + let rc = EndpointRef::new( + socket, + proto::Endpoint::new( + Arc::new(config), + server_config.map(Arc::new), + allow_mtud, + None, + ), + addr.is_ipv6(), + runtime.clone(), + ); + let driver = EndpointDriver(rc.clone()); + runtime.spawn(Box::pin( + async { + if let Err(e) = driver.await { + tracing::error!("I/O error: {}", e); + } + } + .instrument(Span::current()), + )); + Ok(Self { + inner: rc, + default_client_config: None, + runtime, + }) + } + + /// Get the next incoming connection attempt from a client + /// + /// Yields [`Incoming`]s, or `None` if the endpoint is [`close`](Self::close)d. [`Incoming`] + /// can be `await`ed to obtain the final [`Connection`](crate::Connection), or used to e.g. + /// filter connection attempts or force address validation, or converted into an intermediate + /// `Connecting` future which can be used to e.g. send 0.5-RTT data. + pub fn accept(&self) -> Accept<'_> { + Accept { + endpoint: self, + notify: self.inner.shared.incoming.notified(), + } + } + + /// Set the client configuration used by `connect` + pub fn set_default_client_config(&mut self, config: ClientConfig) { + self.default_client_config = Some(config); + } + + /// Connect to a remote endpoint + /// + /// `server_name` must be covered by the certificate presented by the server. This prevents a + /// connection from being intercepted by an attacker with a valid certificate for some other + /// server. + /// + /// May fail immediately due to configuration errors, or in the future if the connection could + /// not be established. + pub fn connect(&self, addr: SocketAddr, server_name: &str) -> Result { + let config = match &self.default_client_config { + Some(config) => config.clone(), + None => return Err(ConnectError::NoDefaultClientConfig), + }; + + self.connect_with(config, addr, server_name) + } + + /// Connect to a remote endpoint using a custom configuration. + /// + /// See [`connect()`] for details. + /// + /// [`connect()`]: Endpoint::connect + pub fn connect_with( + &self, + config: ClientConfig, + addr: SocketAddr, + server_name: &str, + ) -> Result { + let mut endpoint = self.inner.state.lock().unwrap(); + if endpoint.driver_lost || endpoint.recv_state.connections.close.is_some() { + return Err(ConnectError::EndpointStopping); + } + if addr.is_ipv6() && !endpoint.ipv6 { + return Err(ConnectError::InvalidRemoteAddress(addr)); + } + let addr = if endpoint.ipv6 { + SocketAddr::V6(ensure_ipv6(addr)) + } else { + addr + }; + + let (ch, conn) = endpoint + .inner + .connect(self.runtime.now(), config, addr, server_name)?; + + let socket = endpoint.socket.clone(); + endpoint.stats.outgoing_handshakes += 1; + Ok(endpoint + .recv_state + .connections + .insert(ch, conn, socket, self.runtime.clone())) + } + + /// Switch to a new UDP socket + /// + /// See [`Endpoint::rebind_abstract()`] for details. + #[cfg(not(wasm_browser))] + pub fn rebind(&self, socket: std::net::UdpSocket) -> io::Result<()> { + self.rebind_abstract(self.runtime.wrap_udp_socket(socket)?) + } + + /// Switch to a new UDP socket + /// + /// Allows the endpoint's address to be updated live, affecting all active connections. Incoming + /// connections and connections to servers unreachable from the new address will be lost. + /// + /// On error, the old UDP socket is retained. + pub fn rebind_abstract(&self, socket: Arc) -> io::Result<()> { + let addr = socket.local_addr()?; + let mut inner = self.inner.state.lock().unwrap(); + inner.prev_socket = Some(mem::replace(&mut inner.socket, socket)); + inner.ipv6 = addr.is_ipv6(); + + // Update connection socket references + for sender in inner.recv_state.connections.senders.values() { + // Ignoring errors from dropped connections + let _ = sender.send(ConnectionEvent::Rebind(inner.socket.clone())); + } + if let Some(driver) = inner.driver.take() { + // Ensure the driver can register for wake-ups from the new socket + driver.wake(); + } + + Ok(()) + } + + /// Replace the server configuration, affecting new incoming connections only + /// + /// Useful for e.g. refreshing TLS certificates without disrupting existing connections. + pub fn set_server_config(&self, server_config: Option) { + self.inner + .state + .lock() + .unwrap() + .inner + .set_server_config(server_config.map(Arc::new)) + } + + /// Get the local `SocketAddr` the underlying socket is bound to + pub fn local_addr(&self) -> io::Result { + self.inner.state.lock().unwrap().socket.local_addr() + } + + /// Get the number of connections that are currently open + pub fn open_connections(&self) -> usize { + self.inner.state.lock().unwrap().inner.open_connections() + } + + /// Close all of this endpoint's connections immediately and cease accepting new connections. + /// + /// See [`Connection::close()`] for details. + /// + /// [`Connection::close()`]: crate::Connection::close + pub fn close(&self, error_code: VarInt, reason: &[u8]) { + let reason = Bytes::copy_from_slice(reason); + let mut endpoint = self.inner.state.lock().unwrap(); + endpoint.recv_state.connections.close = Some((error_code, reason.clone())); + for sender in endpoint.recv_state.connections.senders.values() { + // Ignoring errors from dropped connections + let _ = sender.send(ConnectionEvent::Close { + error_code, + reason: reason.clone(), + }); + } + self.inner.shared.incoming.notify_waiters(); + } + + /// Wait for all connections on the endpoint to be cleanly shut down + /// + /// Waiting for this condition before exiting ensures that a good-faith effort is made to notify + /// peers of recent connection closes, whereas exiting immediately could force them to wait out + /// the idle timeout period. + /// + /// Does not proactively close existing connections or cause incoming connections to be + /// rejected. Consider calling [`close()`] if that is desired. + /// + /// [`close()`]: Endpoint::close + pub async fn wait_idle(&self) { + loop { + { + let endpoint = &mut *self.inner.state.lock().unwrap(); + if endpoint.recv_state.connections.is_empty() { + break; + } + // Construct future while lock is held to avoid race + self.inner.shared.idle.notified() + } + .await; + } + } +} + +/// Statistics on [Endpoint] activity +#[non_exhaustive] +#[derive(Debug, Default, Copy, Clone)] +pub struct EndpointStats { + /// Cummulative number of Quic handshakes accepted by this [Endpoint] + pub accepted_handshakes: u64, + /// Cummulative number of Quic handshakees sent from this [Endpoint] + pub outgoing_handshakes: u64, + /// Cummulative number of Quic handshakes refused on this [Endpoint] + pub refused_handshakes: u64, + /// Cummulative number of Quic handshakes ignored on this [Endpoint] + pub ignored_handshakes: u64, +} + +/// A future that drives IO on an endpoint +/// +/// This task functions as the switch point between the UDP socket object and the +/// `Endpoint` responsible for routing datagrams to their owning `Connection`. +/// In order to do so, it also facilitates the exchange of different types of events +/// flowing between the `Endpoint` and the tasks managing `Connection`s. As such, +/// running this task is necessary to keep the endpoint's connections running. +/// +/// `EndpointDriver` futures terminate when all clones of the `Endpoint` have been dropped, or when +/// an I/O error occurs. +#[must_use = "endpoint drivers must be spawned for I/O to occur"] +#[derive(Debug)] +pub(crate) struct EndpointDriver(pub(crate) EndpointRef); + +impl Future for EndpointDriver { + type Output = Result<(), io::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let mut endpoint = self.0.state.lock().unwrap(); + if endpoint.driver.is_none() { + endpoint.driver = Some(cx.waker().clone()); + } + + let now = endpoint.runtime.now(); + let mut keep_going = false; + keep_going |= endpoint.drive_recv(cx, now)?; + keep_going |= endpoint.handle_events(cx, &self.0.shared); + + if !endpoint.recv_state.incoming.is_empty() { + self.0.shared.incoming.notify_waiters(); + } + + if endpoint.ref_count == 0 && endpoint.recv_state.connections.is_empty() { + Poll::Ready(Ok(())) + } else { + drop(endpoint); + // If there is more work to do schedule the endpoint task again. + // `wake_by_ref()` is called outside the lock to minimize + // lock contention on a multithreaded runtime. + if keep_going { + cx.waker().wake_by_ref(); + } + Poll::Pending + } + } +} + +impl Drop for EndpointDriver { + fn drop(&mut self) { + let mut endpoint = self.0.state.lock().unwrap(); + endpoint.driver_lost = true; + self.0.shared.incoming.notify_waiters(); + // Drop all outgoing channels, signaling the termination of the endpoint to the associated + // connections. + endpoint.recv_state.connections.senders.clear(); + } +} + +#[derive(Debug)] +pub(crate) struct EndpointInner { + pub(crate) state: Mutex, + pub(crate) shared: Shared, +} + +impl EndpointInner { + pub(crate) fn accept( + &self, + incoming: proto::Incoming, + server_config: Option>, + ) -> Result { + let mut state = self.state.lock().unwrap(); + let mut response_buffer = Vec::new(); + let now = state.runtime.now(); + match state + .inner + .accept(incoming, now, &mut response_buffer, server_config) + { + Ok((handle, conn)) => { + state.stats.accepted_handshakes += 1; + let socket = state.socket.clone(); + let runtime = state.runtime.clone(); + Ok(state + .recv_state + .connections + .insert(handle, conn, socket, runtime)) + } + Err(error) => { + if let Some(transmit) = error.response { + respond(transmit, &response_buffer, &*state.socket); + } + Err(error.cause) + } + } + } + + pub(crate) fn refuse(&self, incoming: proto::Incoming) { + let mut state = self.state.lock().unwrap(); + state.stats.refused_handshakes += 1; + let mut response_buffer = Vec::new(); + let transmit = state.inner.refuse(incoming, &mut response_buffer); + respond(transmit, &response_buffer, &*state.socket); + } + + pub(crate) fn retry(&self, incoming: proto::Incoming) -> Result<(), proto::RetryError> { + let mut state = self.state.lock().unwrap(); + let mut response_buffer = Vec::new(); + let transmit = state.inner.retry(incoming, &mut response_buffer)?; + respond(transmit, &response_buffer, &*state.socket); + Ok(()) + } + + pub(crate) fn ignore(&self, incoming: proto::Incoming) { + let mut state = self.state.lock().unwrap(); + state.stats.ignored_handshakes += 1; + state.inner.ignore(incoming); + } +} + +#[derive(Debug)] +pub(crate) struct State { + socket: Arc, + /// During an active migration, abandoned_socket receives traffic + /// until the first packet arrives on the new socket. + prev_socket: Option>, + inner: proto::Endpoint, + recv_state: RecvState, + driver: Option, + ipv6: bool, + events: mpsc::UnboundedReceiver<(ConnectionHandle, EndpointEvent)>, + /// Number of live handles that can be used to initiate or handle I/O; excludes the driver + ref_count: usize, + driver_lost: bool, + runtime: Arc, + stats: EndpointStats, +} + +#[derive(Debug)] +pub(crate) struct Shared { + incoming: Notify, + idle: Notify, +} + +impl State { + fn drive_recv(&mut self, cx: &mut Context, now: Instant) -> Result { + let get_time = || self.runtime.now(); + self.recv_state.recv_limiter.start_cycle(get_time); + if let Some(socket) = &self.prev_socket { + // We don't care about the `PollProgress` from old sockets. + let poll_res = + self.recv_state + .poll_socket(cx, &mut self.inner, &**socket, &*self.runtime, now); + if poll_res.is_err() { + self.prev_socket = None; + } + }; + let poll_res = + self.recv_state + .poll_socket(cx, &mut self.inner, &*self.socket, &*self.runtime, now); + self.recv_state.recv_limiter.finish_cycle(get_time); + let poll_res = poll_res?; + if poll_res.received_connection_packet { + // Traffic has arrived on self.socket, therefore there is no need for the abandoned + // one anymore. TODO: Account for multiple outgoing connections. + self.prev_socket = None; + } + Ok(poll_res.keep_going) + } + + fn handle_events(&mut self, cx: &mut Context, shared: &Shared) -> bool { + for _ in 0..IO_LOOP_BOUND { + let (ch, event) = match self.events.poll_recv(cx) { + Poll::Ready(Some(x)) => x, + Poll::Ready(None) => unreachable!("EndpointInner owns one sender"), + Poll::Pending => { + return false; + } + }; + + if event.is_drained() { + self.recv_state.connections.senders.remove(&ch); + if self.recv_state.connections.is_empty() { + shared.idle.notify_waiters(); + } + } + let Some(event) = self.inner.handle_event(ch, event) else { + continue; + }; + // Ignoring errors from dropped connections that haven't yet been cleaned up + let _ = self + .recv_state + .connections + .senders + .get_mut(&ch) + .unwrap() + .send(ConnectionEvent::Proto(event)); + } + + true + } +} + +impl Drop for State { + fn drop(&mut self) { + for incoming in self.recv_state.incoming.drain(..) { + self.inner.ignore(incoming); + } + } +} + +fn respond(transmit: proto::Transmit, response_buffer: &[u8], socket: &dyn AsyncUdpSocket) { + // Send if there's kernel buffer space; otherwise, drop it + // + // As an endpoint-generated packet, we know this is an + // immediate, stateless response to an unconnected peer, + // one of: + // + // - A version negotiation response due to an unknown version + // - A `CLOSE` due to a malformed or unwanted connection attempt + // - A stateless reset due to an unrecognized connection + // - A `Retry` packet due to a connection attempt when + // `use_retry` is set + // + // In each case, a well-behaved peer can be trusted to retry a + // few times, which is guaranteed to produce the same response + // from us. Repeated failures might at worst cause a peer's new + // connection attempt to time out, which is acceptable if we're + // under such heavy load that there's never room for this code + // to transmit. This is morally equivalent to the packet getting + // lost due to congestion further along the link, which + // similarly relies on peer retries for recovery. + _ = socket.try_send(&udp_transmit(&transmit, &response_buffer[..transmit.size])); +} + +#[inline] +fn proto_ecn(ecn: udp::EcnCodepoint) -> proto::EcnCodepoint { + match ecn { + udp::EcnCodepoint::Ect0 => proto::EcnCodepoint::Ect0, + udp::EcnCodepoint::Ect1 => proto::EcnCodepoint::Ect1, + udp::EcnCodepoint::Ce => proto::EcnCodepoint::Ce, + } +} + +#[derive(Debug)] +struct ConnectionSet { + /// Senders for communicating with the endpoint's connections + senders: FxHashMap>, + /// Stored to give out clones to new ConnectionInners + sender: mpsc::UnboundedSender<(ConnectionHandle, EndpointEvent)>, + /// Set if the endpoint has been manually closed + close: Option<(VarInt, Bytes)>, +} + +impl ConnectionSet { + fn insert( + &mut self, + handle: ConnectionHandle, + conn: proto::Connection, + socket: Arc, + runtime: Arc, + ) -> Connecting { + let (send, recv) = mpsc::unbounded_channel(); + if let Some((error_code, ref reason)) = self.close { + send.send(ConnectionEvent::Close { + error_code, + reason: reason.clone(), + }) + .unwrap(); + } + self.senders.insert(handle, send); + Connecting::new(handle, conn, self.sender.clone(), recv, socket, runtime) + } + + fn is_empty(&self) -> bool { + self.senders.is_empty() + } +} + +fn ensure_ipv6(x: SocketAddr) -> SocketAddrV6 { + match x { + SocketAddr::V6(x) => x, + SocketAddr::V4(x) => SocketAddrV6::new(x.ip().to_ipv6_mapped(), x.port(), 0, 0), + } +} + +pin_project! { + /// Future produced by [`Endpoint::accept`] + pub struct Accept<'a> { + endpoint: &'a Endpoint, + #[pin] + notify: Notified<'a>, + } +} + +impl Future for Accept<'_> { + type Output = Option; + fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + let mut endpoint = this.endpoint.inner.state.lock().unwrap(); + if endpoint.driver_lost { + return Poll::Ready(None); + } + if let Some(incoming) = endpoint.recv_state.incoming.pop_front() { + // Release the mutex lock on endpoint so cloning it doesn't deadlock + drop(endpoint); + let incoming = Incoming::new(incoming, this.endpoint.inner.clone()); + return Poll::Ready(Some(incoming)); + } + if endpoint.recv_state.connections.close.is_some() { + return Poll::Ready(None); + } + loop { + match this.notify.as_mut().poll(ctx) { + // `state` lock ensures we didn't race with readiness + Poll::Pending => return Poll::Pending, + // Spurious wakeup, get a new future + Poll::Ready(()) => this + .notify + .set(this.endpoint.inner.shared.incoming.notified()), + } + } + } +} + +#[derive(Debug)] +pub(crate) struct EndpointRef(Arc); + +impl EndpointRef { + pub(crate) fn new( + socket: Arc, + inner: proto::Endpoint, + ipv6: bool, + runtime: Arc, + ) -> Self { + let (sender, events) = mpsc::unbounded_channel(); + let recv_state = RecvState::new(sender, socket.max_receive_segments(), &inner); + Self(Arc::new(EndpointInner { + shared: Shared { + incoming: Notify::new(), + idle: Notify::new(), + }, + state: Mutex::new(State { + socket, + prev_socket: None, + inner, + ipv6, + events, + driver: None, + ref_count: 0, + driver_lost: false, + recv_state, + runtime, + stats: EndpointStats::default(), + }), + })) + } +} + +impl Clone for EndpointRef { + fn clone(&self) -> Self { + self.0.state.lock().unwrap().ref_count += 1; + Self(self.0.clone()) + } +} + +impl Drop for EndpointRef { + fn drop(&mut self) { + let endpoint = &mut *self.0.state.lock().unwrap(); + if let Some(x) = endpoint.ref_count.checked_sub(1) { + endpoint.ref_count = x; + if x == 0 { + // If the driver is about to be on its own, ensure it can shut down if the last + // connection is gone. + if let Some(task) = endpoint.driver.take() { + task.wake(); + } + } + } + } +} + +impl std::ops::Deref for EndpointRef { + type Target = EndpointInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// State directly involved in handling incoming packets +struct RecvState { + incoming: VecDeque, + connections: ConnectionSet, + recv_buf: Box<[u8]>, + recv_limiter: WorkLimiter, +} + +impl RecvState { + fn new( + sender: mpsc::UnboundedSender<(ConnectionHandle, EndpointEvent)>, + max_receive_segments: usize, + endpoint: &proto::Endpoint, + ) -> Self { + let recv_buf = vec![ + 0; + endpoint.config().get_max_udp_payload_size().min(64 * 1024) as usize + * max_receive_segments + * BATCH_SIZE + ]; + Self { + connections: ConnectionSet { + senders: FxHashMap::default(), + sender, + close: None, + }, + incoming: VecDeque::new(), + recv_buf: recv_buf.into(), + recv_limiter: WorkLimiter::new(RECV_TIME_BOUND), + } + } + + fn poll_socket( + &mut self, + cx: &mut Context, + endpoint: &mut proto::Endpoint, + socket: &dyn AsyncUdpSocket, + runtime: &dyn Runtime, + now: Instant, + ) -> Result { + let mut received_connection_packet = false; + let mut metas = [RecvMeta::default(); BATCH_SIZE]; + let mut iovs: [IoSliceMut; BATCH_SIZE] = { + let mut bufs = self + .recv_buf + .chunks_mut(self.recv_buf.len() / BATCH_SIZE) + .map(IoSliceMut::new); + + // expect() safe as self.recv_buf is chunked into BATCH_SIZE items + // and iovs will be of size BATCH_SIZE, thus from_fn is called + // exactly BATCH_SIZE times. + std::array::from_fn(|_| bufs.next().expect("BATCH_SIZE elements")) + }; + loop { + match socket.poll_recv(cx, &mut iovs, &mut metas) { + Poll::Ready(Ok(msgs)) => { + self.recv_limiter.record_work(msgs); + for (meta, buf) in metas.iter().zip(iovs.iter()).take(msgs) { + let mut data: BytesMut = buf[0..meta.len].into(); + while !data.is_empty() { + let buf = data.split_to(meta.stride.min(data.len())); + let mut response_buffer = Vec::new(); + match endpoint.handle( + now, + meta.addr, + meta.dst_ip, + meta.ecn.map(proto_ecn), + buf, + &mut response_buffer, + ) { + Some(DatagramEvent::NewConnection(incoming)) => { + if self.connections.close.is_none() { + self.incoming.push_back(incoming); + } else { + let transmit = + endpoint.refuse(incoming, &mut response_buffer); + respond(transmit, &response_buffer, socket); + } + } + Some(DatagramEvent::ConnectionEvent(handle, event)) => { + // Ignoring errors from dropped connections that haven't yet been cleaned up + received_connection_packet = true; + let _ = self + .connections + .senders + .get_mut(&handle) + .unwrap() + .send(ConnectionEvent::Proto(event)); + } + Some(DatagramEvent::Response(transmit)) => { + respond(transmit, &response_buffer, socket); + } + None => {} + } + } + } + } + Poll::Pending => { + return Ok(PollProgress { + received_connection_packet, + keep_going: false, + }); + } + // Ignore ECONNRESET as it's undefined in QUIC and may be injected by an + // attacker + Poll::Ready(Err(ref e)) if e.kind() == io::ErrorKind::ConnectionReset => { + continue; + } + Poll::Ready(Err(e)) => { + return Err(e); + } + } + if !self.recv_limiter.allow_work(|| runtime.now()) { + return Ok(PollProgress { + received_connection_packet, + keep_going: true, + }); + } + } + } +} + +impl fmt::Debug for RecvState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("RecvState") + .field("incoming", &self.incoming) + .field("connections", &self.connections) + // recv_buf too large + .field("recv_limiter", &self.recv_limiter) + .finish_non_exhaustive() + } +} + +#[derive(Default)] +struct PollProgress { + /// Whether a datagram was routed to an existing connection + received_connection_packet: bool, + /// Whether datagram handling was interrupted early by the work limiter for fairness + keep_going: bool, +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/incoming.rs b/vendor/flutter_quic/rust/vendor/quinn/src/incoming.rs new file mode 100644 index 0000000..8eced8c --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/incoming.rs @@ -0,0 +1,152 @@ +use std::{ + future::{Future, IntoFuture}, + net::{IpAddr, SocketAddr}, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use proto::{ConnectionError, ConnectionId, ServerConfig}; +use thiserror::Error; + +use crate::{ + connection::{Connecting, Connection}, + endpoint::EndpointRef, +}; + +/// An incoming connection for which the server has not yet begun its part of the handshake +#[derive(Debug)] +pub struct Incoming(Option); + +impl Incoming { + pub(crate) fn new(inner: proto::Incoming, endpoint: EndpointRef) -> Self { + Self(Some(State { inner, endpoint })) + } + + /// Attempt to accept this incoming connection (an error may still occur) + pub fn accept(mut self) -> Result { + let state = self.0.take().unwrap(); + state.endpoint.accept(state.inner, None) + } + + /// Accept this incoming connection using a custom configuration + /// + /// See [`accept()`][Incoming::accept] for more details. + pub fn accept_with( + mut self, + server_config: Arc, + ) -> Result { + let state = self.0.take().unwrap(); + state.endpoint.accept(state.inner, Some(server_config)) + } + + /// Reject this incoming connection attempt + pub fn refuse(mut self) { + let state = self.0.take().unwrap(); + state.endpoint.refuse(state.inner); + } + + /// Respond with a retry packet, requiring the client to retry with address validation + /// + /// Errors if `may_retry()` is false. + pub fn retry(mut self) -> Result<(), RetryError> { + let state = self.0.take().unwrap(); + state.endpoint.retry(state.inner).map_err(|e| { + RetryError(Box::new(Self(Some(State { + inner: e.into_incoming(), + endpoint: state.endpoint, + })))) + }) + } + + /// Ignore this incoming connection attempt, not sending any packet in response + pub fn ignore(mut self) { + let state = self.0.take().unwrap(); + state.endpoint.ignore(state.inner); + } + + /// The local IP address which was used when the peer established the connection + pub fn local_ip(&self) -> Option { + self.0.as_ref().unwrap().inner.local_ip() + } + + /// The peer's UDP address + pub fn remote_address(&self) -> SocketAddr { + self.0.as_ref().unwrap().inner.remote_address() + } + + /// Whether the socket address that is initiating this connection has been validated + /// + /// This means that the sender of the initial packet has proved that they can receive traffic + /// sent to `self.remote_address()`. + /// + /// If `self.remote_address_validated()` is false, `self.may_retry()` is guaranteed to be true. + /// The inverse is not guaranteed. + pub fn remote_address_validated(&self) -> bool { + self.0.as_ref().unwrap().inner.remote_address_validated() + } + + /// Whether it is legal to respond with a retry packet + /// + /// If `self.remote_address_validated()` is false, `self.may_retry()` is guaranteed to be true. + /// The inverse is not guaranteed. + pub fn may_retry(&self) -> bool { + self.0.as_ref().unwrap().inner.may_retry() + } + + /// The original destination CID when initiating the connection + pub fn orig_dst_cid(&self) -> ConnectionId { + *self.0.as_ref().unwrap().inner.orig_dst_cid() + } +} + +impl Drop for Incoming { + fn drop(&mut self) { + // Implicit reject, similar to Connection's implicit close + if let Some(state) = self.0.take() { + state.endpoint.refuse(state.inner); + } + } +} + +#[derive(Debug)] +struct State { + inner: proto::Incoming, + endpoint: EndpointRef, +} + +/// Error for attempting to retry an [`Incoming`] which already bears a token from a previous retry +#[derive(Debug, Error)] +#[error("retry() with validated Incoming")] +pub struct RetryError(Box); + +impl RetryError { + /// Get the [`Incoming`] + pub fn into_incoming(self) -> Incoming { + *self.0 + } +} + +/// Basic adapter to let [`Incoming`] be `await`-ed like a [`Connecting`] +#[derive(Debug)] +pub struct IncomingFuture(Result); + +impl Future for IncomingFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match &mut self.0 { + Ok(ref mut connecting) => Pin::new(connecting).poll(cx), + Err(e) => Poll::Ready(Err(e.clone())), + } + } +} + +impl IntoFuture for Incoming { + type Output = Result; + type IntoFuture = IncomingFuture; + + fn into_future(self) -> Self::IntoFuture { + IncomingFuture(self.accept()) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/lib.rs b/vendor/flutter_quic/rust/vendor/quinn/src/lib.rs new file mode 100644 index 0000000..82becdb --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/lib.rs @@ -0,0 +1,136 @@ +//! QUIC transport protocol implementation +//! +//! [QUIC](https://en.wikipedia.org/wiki/QUIC) is a modern transport protocol addressing +//! shortcomings of TCP, such as head-of-line blocking, poor security, slow handshakes, and +//! inefficient congestion control. This crate provides a portable userspace implementation. It +//! builds on top of quinn-proto, which implements protocol logic independent of any particular +//! runtime. +//! +//! The entry point of this crate is the [`Endpoint`]. +//! +//! # About QUIC +//! +//! A QUIC connection is an association between two endpoints. The endpoint which initiates the +//! connection is termed the client, and the endpoint which accepts it is termed the server. A +//! single endpoint may function as both client and server for different connections, for example +//! in a peer-to-peer application. To communicate application data, each endpoint may open streams +//! up to a limit dictated by its peer. Typically, that limit is increased as old streams are +//! finished. +//! +//! Streams may be unidirectional or bidirectional, and are cheap to create and disposable. For +//! example, a traditionally datagram-oriented application could use a new stream for every +//! message it wants to send, no longer needing to worry about MTUs. Bidirectional streams behave +//! much like a traditional TCP connection, and are useful for sending messages that have an +//! immediate response, such as an HTTP request. Stream data is delivered reliably, and there is no +//! ordering enforced between data on different streams. +//! +//! By avoiding head-of-line blocking and providing unified congestion control across all streams +//! of a connection, QUIC is able to provide higher throughput and lower latency than one or +//! multiple TCP connections between the same two hosts, while providing more useful behavior than +//! raw UDP sockets. +//! +//! Quinn also exposes unreliable datagrams, which are a low-level primitive preferred when +//! automatic fragmentation and retransmission of certain data is not desired. +//! +//! QUIC uses encryption and identity verification built directly on TLS 1.3. Just as with a TLS +//! server, it is useful for a QUIC server to be identified by a certificate signed by a trusted +//! authority. If this is infeasible--for example, if servers are short-lived or not associated +//! with a domain name--then as with TLS, self-signed certificates can be used to provide +//! encryption alone. +#![warn(missing_docs)] +#![warn(unreachable_pub)] +#![warn(clippy::use_self)] + +use std::sync::Arc; + +mod connection; +mod endpoint; +mod incoming; +mod mutex; +mod recv_stream; +mod runtime; +mod send_stream; +mod work_limiter; + +#[cfg(not(wasm_browser))] +pub(crate) use std::time::{Duration, Instant}; +#[cfg(wasm_browser)] +pub(crate) use web_time::{Duration, Instant}; + +#[cfg(feature = "bloom")] +pub use proto::BloomTokenLog; +pub use proto::{ + AckFrequencyConfig, ApplicationClose, Chunk, ClientConfig, ClosedStream, ConfigError, + ConnectError, ConnectionClose, ConnectionError, ConnectionId, ConnectionIdGenerator, + ConnectionStats, Dir, EcnCodepoint, EndpointConfig, FrameStats, FrameType, IdleTimeout, + MtuDiscoveryConfig, NoneTokenLog, NoneTokenStore, PathStats, ServerConfig, Side, StdSystemTime, + StreamId, TimeSource, TokenLog, TokenMemoryCache, TokenReuseError, TokenStore, Transmit, + TransportConfig, TransportErrorCode, UdpStats, ValidationTokenConfig, VarInt, + VarIntBoundsExceeded, Written, congestion, crypto, +}; +#[cfg(feature = "qlog")] +pub use proto::{QlogConfig, QlogStream}; +#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +pub use rustls; +pub use udp; + +pub use crate::connection::{ + AcceptBi, AcceptUni, Connecting, Connection, OpenBi, OpenUni, ReadDatagram, SendDatagram, + SendDatagramError, ZeroRttAccepted, +}; +pub use crate::endpoint::{Accept, Endpoint, EndpointStats}; +pub use crate::incoming::{Incoming, IncomingFuture, RetryError}; +pub use crate::recv_stream::{ReadError, ReadExactError, ReadToEndError, RecvStream, ResetError}; +#[cfg(feature = "runtime-async-std")] +pub use crate::runtime::AsyncStdRuntime; +#[cfg(feature = "runtime-smol")] +pub use crate::runtime::SmolRuntime; +#[cfg(feature = "runtime-tokio")] +pub use crate::runtime::TokioRuntime; +pub use crate::runtime::{AsyncTimer, AsyncUdpSocket, Runtime, UdpPoller, default_runtime}; +pub use crate::send_stream::{SendStream, StoppedError, WriteError}; + +#[cfg(test)] +mod tests; + +#[derive(Debug)] +enum ConnectionEvent { + Close { + error_code: VarInt, + reason: bytes::Bytes, + }, + Proto(proto::ConnectionEvent), + Rebind(Arc), +} + +fn udp_transmit<'a>(t: &proto::Transmit, buffer: &'a [u8]) -> udp::Transmit<'a> { + udp::Transmit { + destination: t.destination, + ecn: t.ecn.map(udp_ecn), + contents: buffer, + segment_size: t.segment_size, + src_ip: t.src_ip, + } +} + +fn udp_ecn(ecn: proto::EcnCodepoint) -> udp::EcnCodepoint { + match ecn { + proto::EcnCodepoint::Ect0 => udp::EcnCodepoint::Ect0, + proto::EcnCodepoint::Ect1 => udp::EcnCodepoint::Ect1, + proto::EcnCodepoint::Ce => udp::EcnCodepoint::Ce, + } +} + +/// Maximum number of datagrams processed in send/recv calls to make before moving on to other processing +/// +/// This helps ensure we don't starve anything when the CPU is slower than the link. +/// Value is selected by picking a low number which didn't degrade throughput in benchmarks. +const IO_LOOP_BOUND: usize = 160; + +/// The maximum amount of time that should be spent in `recvmsg()` calls per endpoint iteration +/// +/// 50us are chosen so that an endpoint iteration with a 50us sendmsg limit blocks +/// the runtime for a maximum of about 100us. +/// Going much lower does not yield any noticeable difference, since a single `recvmmsg` +/// batch of size 32 was observed to take 30us on some systems. +const RECV_TIME_BOUND: Duration = Duration::from_micros(50); diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/mutex.rs b/vendor/flutter_quic/rust/vendor/quinn/src/mutex.rs new file mode 100644 index 0000000..bdefd72 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/mutex.rs @@ -0,0 +1,163 @@ +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; + +#[cfg(feature = "lock_tracking")] +mod tracking { + use super::*; + use crate::{Duration, Instant}; + use std::collections::VecDeque; + use tracing::warn; + + #[derive(Debug)] + struct Inner { + last_lock_owner: VecDeque<(&'static str, Duration)>, + value: T, + } + + /// A Mutex which optionally allows to track the time a lock was held and + /// emit warnings in case of excessive lock times + pub(crate) struct Mutex { + inner: std::sync::Mutex>, + } + + impl std::fmt::Debug for Mutex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.inner, f) + } + } + + impl Mutex { + pub(crate) fn new(value: T) -> Self { + Self { + inner: std::sync::Mutex::new(Inner { + last_lock_owner: VecDeque::new(), + value, + }), + } + } + + /// Acquires the lock for a certain purpose + /// + /// The purpose will be recorded in the list of last lock owners + pub(crate) fn lock(&self, purpose: &'static str) -> MutexGuard<'_, T> { + // We don't bother dispatching through Runtime::now because they're pure performance + // diagnostics. + let now = Instant::now(); + let guard = self.inner.lock().unwrap(); + + let lock_time = Instant::now(); + let elapsed = lock_time.duration_since(now); + + if elapsed > Duration::from_millis(1) { + warn!( + "Locking the connection for {} took {:?}. Last owners: {:?}", + purpose, elapsed, guard.last_lock_owner + ); + } + + MutexGuard { + guard, + start_time: lock_time, + purpose, + } + } + } + + pub(crate) struct MutexGuard<'a, T> { + guard: std::sync::MutexGuard<'a, Inner>, + start_time: Instant, + purpose: &'static str, + } + + impl Drop for MutexGuard<'_, T> { + fn drop(&mut self) { + if self.guard.last_lock_owner.len() == MAX_LOCK_OWNERS { + self.guard.last_lock_owner.pop_back(); + } + + let duration = self.start_time.elapsed(); + + if duration > Duration::from_millis(1) { + warn!( + "Utilizing the connection for {} took {:?}", + self.purpose, duration + ); + } + + self.guard + .last_lock_owner + .push_front((self.purpose, duration)); + } + } + + impl Deref for MutexGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.guard.value + } + } + + impl DerefMut for MutexGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.guard.value + } + } + + const MAX_LOCK_OWNERS: usize = 20; +} + +#[cfg(feature = "lock_tracking")] +pub(crate) use tracking::Mutex; + +#[cfg(not(feature = "lock_tracking"))] +mod non_tracking { + use super::*; + + /// A Mutex which optionally allows to track the time a lock was held and + /// emit warnings in case of excessive lock times + #[derive(Debug)] + pub(crate) struct Mutex { + inner: std::sync::Mutex, + } + + impl Mutex { + pub(crate) fn new(value: T) -> Self { + Self { + inner: std::sync::Mutex::new(value), + } + } + + /// Acquires the lock for a certain purpose + /// + /// The purpose will be recorded in the list of last lock owners + pub(crate) fn lock(&self, _purpose: &'static str) -> MutexGuard<'_, T> { + MutexGuard { + guard: self.inner.lock().unwrap(), + } + } + } + + pub(crate) struct MutexGuard<'a, T> { + guard: std::sync::MutexGuard<'a, T>, + } + + impl Deref for MutexGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.guard.deref() + } + } + + impl DerefMut for MutexGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.guard.deref_mut() + } + } +} + +#[cfg(not(feature = "lock_tracking"))] +pub(crate) use non_tracking::Mutex; diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/recv_stream.rs b/vendor/flutter_quic/rust/vendor/quinn/src/recv_stream.rs new file mode 100644 index 0000000..8b77e62 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/recv_stream.rs @@ -0,0 +1,699 @@ +use std::{ + future::{Future, poll_fn}, + io, + pin::Pin, + task::{Context, Poll, ready}, +}; + +use bytes::Bytes; +use proto::{Chunk, Chunks, ClosedStream, ConnectionError, ReadableError, StreamId}; +use thiserror::Error; +use tokio::io::ReadBuf; + +use crate::{VarInt, connection::ConnectionRef}; + +/// A stream that can only be used to receive data +/// +/// `stop(0)` is implicitly called on drop unless: +/// - A variant of [`ReadError`] has been yielded by a read call +/// - [`stop()`] was called explicitly +/// +/// # Cancellation +/// +/// A `read` method is said to be *cancel-safe* when dropping its future before the future becomes +/// ready cannot lead to loss of stream data. This is true of methods which succeed immediately when +/// any progress is made, and is not true of methods which might need to perform multiple reads +/// internally before succeeding. Each `read` method documents whether it is cancel-safe. +/// +/// # Common issues +/// +/// ## Data never received on a locally-opened stream +/// +/// Peers are not notified of streams until they or a later-numbered stream are used to send +/// data. If a bidirectional stream is locally opened but never used to send, then the peer may +/// never see it. Application protocols should always arrange for the endpoint which will first +/// transmit on a stream to be the endpoint responsible for opening it. +/// +/// ## Data never received on a remotely-opened stream +/// +/// Verify that the stream you are receiving is the same one that the server is sending on, e.g. by +/// logging the [`id`] of each. Streams are always accepted in the same order as they are created, +/// i.e. ascending order by [`StreamId`]. For example, even if a sender first transmits on +/// bidirectional stream 1, the first stream yielded by [`Connection::accept_bi`] on the receiver +/// will be bidirectional stream 0. +/// +/// [`ReadError`]: crate::ReadError +/// [`stop()`]: RecvStream::stop +/// [`SendStream::finish`]: crate::SendStream::finish +/// [`WriteError::Stopped`]: crate::WriteError::Stopped +/// [`id`]: RecvStream::id +/// [`Connection::accept_bi`]: crate::Connection::accept_bi +#[derive(Debug)] +pub struct RecvStream { + conn: ConnectionRef, + stream: StreamId, + is_0rtt: bool, + all_data_read: bool, + reset: Option, +} + +impl RecvStream { + pub(crate) fn new(conn: ConnectionRef, stream: StreamId, is_0rtt: bool) -> Self { + Self { + conn, + stream, + is_0rtt, + all_data_read: false, + reset: None, + } + } + + /// Read data contiguously from the stream. + /// + /// Yields the number of bytes read into `buf` on success, or `None` if the stream was finished. + /// + /// This operation is cancel-safe. + pub async fn read(&mut self, buf: &mut [u8]) -> Result, ReadError> { + Read { + stream: self, + buf: ReadBuf::new(buf), + } + .await + } + + /// Read an exact number of bytes contiguously from the stream. + /// + /// See [`read()`] for details. This operation is *not* cancel-safe. + /// + /// [`read()`]: RecvStream::read + pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), ReadExactError> { + ReadExact { + stream: self, + buf: ReadBuf::new(buf), + } + .await + } + + /// Attempts to read from the stream into the provided buffer + /// + /// On success, returns `Poll::Ready(Ok(num_bytes_read))` and places data into `buf`. If this + /// returns zero bytes read (and `buf` has a non-zero length), that indicates that the remote + /// side has [`finish`]ed the stream and the local side has already read all bytes. + /// + /// If no data is available for reading, this returns `Poll::Pending` and arranges for the + /// current task (via `cx.waker()`) to be notified when the stream becomes readable or is + /// closed. + /// + /// [`finish`]: crate::SendStream::finish + pub fn poll_read( + &mut self, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + let mut buf = ReadBuf::new(buf); + ready!(self.poll_read_buf(cx, &mut buf))?; + Poll::Ready(Ok(buf.filled().len())) + } + + /// Attempts to read from the stream into the provided buffer, which may be uninitialized + /// + /// On success, returns `Poll::Ready(Ok(()))` and places data into the unfilled portion of + /// `buf`. If this does not write any bytes to `buf` (and `buf.remaining()` is non-zero), that + /// indicates that the remote side has [`finish`]ed the stream and the local side has already + /// read all bytes. + /// + /// If no data is available for reading, this returns `Poll::Pending` and arranges for the + /// current task (via `cx.waker()`) to be notified when the stream becomes readable or is + /// closed. + /// + /// [`finish`]: crate::SendStream::finish + pub fn poll_read_buf( + &mut self, + cx: &mut Context, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + if buf.remaining() == 0 { + return Poll::Ready(Ok(())); + } + + self.poll_read_generic(cx, true, |chunks| { + let mut read = false; + loop { + if buf.remaining() == 0 { + // We know `read` is `true` because `buf.remaining()` was not 0 before + return ReadStatus::Readable(()); + } + + match chunks.next(buf.remaining()) { + Ok(Some(chunk)) => { + buf.put_slice(&chunk.bytes); + read = true; + } + res => return (if read { Some(()) } else { None }, res.err()).into(), + } + } + }) + .map(|res| res.map(|_| ())) + } + + /// Read the next segment of data + /// + /// Yields `None` if the stream was finished. Otherwise, yields a segment of data and its + /// offset in the stream. If `ordered` is `true`, the chunk's offset will be immediately after + /// the last data yielded by `read()` or `read_chunk()`. If `ordered` is `false`, segments may + /// be received in any order, and the `Chunk`'s `offset` field can be used to determine + /// ordering in the caller. Unordered reads are less prone to head-of-line blocking within a + /// stream, but require the application to manage reassembling the original data. + /// + /// Slightly more efficient than `read` due to not copying. Chunk boundaries do not correspond + /// to peer writes, and hence cannot be used as framing. + /// + /// This operation is cancel-safe. + pub async fn read_chunk( + &mut self, + max_length: usize, + ordered: bool, + ) -> Result, ReadError> { + ReadChunk { + stream: self, + max_length, + ordered, + } + .await + } + + /// Attempts to read a chunk from the stream. + /// + /// On success, returns `Poll::Ready(Ok(Some(chunk)))`. If `Poll::Ready(Ok(None))` + /// is returned, it implies that EOF has been reached. + /// + /// If no data is available for reading, the method returns `Poll::Pending` + /// and arranges for the current task (via cx.waker()) to receive a notification + /// when the stream becomes readable or is closed. + fn poll_read_chunk( + &mut self, + cx: &mut Context, + max_length: usize, + ordered: bool, + ) -> Poll, ReadError>> { + self.poll_read_generic(cx, ordered, |chunks| match chunks.next(max_length) { + Ok(Some(chunk)) => ReadStatus::Readable(chunk), + res => (None, res.err()).into(), + }) + } + + /// Read the next segments of data + /// + /// Fills `bufs` with the segments of data beginning immediately after the + /// last data yielded by `read` or `read_chunk`, or `None` if the stream was + /// finished. + /// + /// Slightly more efficient than `read` due to not copying. Chunk boundaries + /// do not correspond to peer writes, and hence cannot be used as framing. + /// + /// This operation is cancel-safe. + pub async fn read_chunks(&mut self, bufs: &mut [Bytes]) -> Result, ReadError> { + ReadChunks { stream: self, bufs }.await + } + + /// Foundation of [`Self::read_chunks`] + fn poll_read_chunks( + &mut self, + cx: &mut Context, + bufs: &mut [Bytes], + ) -> Poll, ReadError>> { + if bufs.is_empty() { + return Poll::Ready(Ok(Some(0))); + } + + self.poll_read_generic(cx, true, |chunks| { + let mut read = 0; + loop { + if read >= bufs.len() { + // We know `read > 0` because `bufs` cannot be empty here + return ReadStatus::Readable(read); + } + + match chunks.next(usize::MAX) { + Ok(Some(chunk)) => { + bufs[read] = chunk.bytes; + read += 1; + } + res => return (if read == 0 { None } else { Some(read) }, res.err()).into(), + } + } + }) + } + + /// Convenience method to read all remaining data into a buffer + /// + /// Fails with [`ReadToEndError::TooLong`] on reading more than `size_limit` bytes, discarding + /// all data read. Uses unordered reads to be more efficient than using `AsyncRead` would + /// allow. `size_limit` should be set to limit worst-case memory use. + /// + /// If unordered reads have already been made, the resulting buffer may have gaps containing + /// arbitrary data. + /// + /// This operation is *not* cancel-safe. + /// + /// [`ReadToEndError::TooLong`]: crate::ReadToEndError::TooLong + pub async fn read_to_end(&mut self, size_limit: usize) -> Result, ReadToEndError> { + ReadToEnd { + stream: self, + size_limit, + read: Vec::new(), + start: u64::MAX, + end: 0, + } + .await + } + + /// Stop accepting data + /// + /// Discards unread data and notifies the peer to stop transmitting. Once stopped, further + /// attempts to operate on a stream will yield `ClosedStream` errors. + pub fn stop(&mut self, error_code: VarInt) -> Result<(), ClosedStream> { + let mut conn = self.conn.state.lock("RecvStream::stop"); + if self.is_0rtt && conn.check_0rtt().is_err() { + return Ok(()); + } + conn.inner.recv_stream(self.stream).stop(error_code)?; + conn.wake(); + self.all_data_read = true; + Ok(()) + } + + /// Check if this stream has been opened during 0-RTT. + /// + /// In which case any non-idempotent request should be considered dangerous at the application + /// level. Because read data is subject to replay attacks. + pub fn is_0rtt(&self) -> bool { + self.is_0rtt + } + + /// Get the identity of this stream + pub fn id(&self) -> StreamId { + self.stream + } + + /// Completes when the stream has been reset by the peer or otherwise closed + /// + /// Yields `Some` with the reset error code when the stream is reset by the peer. Yields `None` + /// when the stream was previously [`stop()`](Self::stop)ed, or when the stream was + /// [`finish()`](crate::SendStream::finish)ed by the peer and all data has been received, after + /// which it is no longer meaningful for the stream to be reset. + /// + /// This operation is cancel-safe. + pub async fn received_reset(&mut self) -> Result, ResetError> { + poll_fn(|cx| { + let mut conn = self.conn.state.lock("RecvStream::reset"); + if self.is_0rtt && conn.check_0rtt().is_err() { + return Poll::Ready(Err(ResetError::ZeroRttRejected)); + } + + if let Some(code) = self.reset { + return Poll::Ready(Ok(Some(code))); + } + + match conn.inner.recv_stream(self.stream).received_reset() { + Err(_) => Poll::Ready(Ok(None)), + Ok(Some(error_code)) => { + // Stream state has just now been freed, so the connection may need to issue new + // stream ID flow control credit + conn.wake(); + Poll::Ready(Ok(Some(error_code))) + } + Ok(None) => { + if let Some(e) = &conn.error { + return Poll::Ready(Err(e.clone().into())); + } + // Resets always notify readers, since a reset is an immediate read error. We + // could introduce a dedicated channel to reduce the risk of spurious wakeups, + // but that increased complexity is probably not justified, as an application + // that is expecting a reset is not likely to receive large amounts of data. + conn.blocked_readers.insert(self.stream, cx.waker().clone()); + Poll::Pending + } + } + }) + .await + } + + /// Handle common logic related to reading out of a receive stream + /// + /// This takes an `FnMut` closure that takes care of the actual reading process, matching + /// the detailed read semantics for the calling function with a particular return type. + /// The closure can read from the passed `&mut Chunks` and has to return the status after + /// reading: the amount of data read, and the status after the final read call. + fn poll_read_generic( + &mut self, + cx: &mut Context, + ordered: bool, + mut read_fn: T, + ) -> Poll, ReadError>> + where + T: FnMut(&mut Chunks) -> ReadStatus, + { + use proto::ReadError::*; + if self.all_data_read { + return Poll::Ready(Ok(None)); + } + + let mut conn = self.conn.state.lock("RecvStream::poll_read"); + if self.is_0rtt { + conn.check_0rtt().map_err(|()| ReadError::ZeroRttRejected)?; + } + + // If we stored an error during a previous call, return it now. This can happen if a + // `read_fn` both wants to return data and also returns an error in its final stream status. + let status = match self.reset { + Some(code) => ReadStatus::Failed(None, Reset(code)), + None => { + let mut recv = conn.inner.recv_stream(self.stream); + let mut chunks = recv.read(ordered)?; + let status = read_fn(&mut chunks); + if chunks.finalize().should_transmit() { + conn.wake(); + } + status + } + }; + + match status { + ReadStatus::Readable(read) => Poll::Ready(Ok(Some(read))), + ReadStatus::Finished(read) => { + self.all_data_read = true; + Poll::Ready(Ok(read)) + } + ReadStatus::Failed(read, Blocked) => match read { + Some(val) => Poll::Ready(Ok(Some(val))), + None => { + if let Some(ref x) = conn.error { + return Poll::Ready(Err(ReadError::ConnectionLost(x.clone()))); + } + conn.blocked_readers.insert(self.stream, cx.waker().clone()); + Poll::Pending + } + }, + ReadStatus::Failed(read, Reset(error_code)) => match read { + None => { + self.all_data_read = true; + self.reset = Some(error_code); + Poll::Ready(Err(ReadError::Reset(error_code))) + } + done => { + self.reset = Some(error_code); + Poll::Ready(Ok(done)) + } + }, + } + } +} + +enum ReadStatus { + Readable(T), + Finished(Option), + Failed(Option, proto::ReadError), +} + +impl From<(Option, Option)> for ReadStatus { + fn from(status: (Option, Option)) -> Self { + match status { + (read, None) => Self::Finished(read), + (read, Some(e)) => Self::Failed(read, e), + } + } +} + +/// Future produced by [`RecvStream::read_to_end()`]. +/// +/// [`RecvStream::read_to_end()`]: crate::RecvStream::read_to_end +struct ReadToEnd<'a> { + stream: &'a mut RecvStream, + read: Vec<(Bytes, u64)>, + start: u64, + end: u64, + size_limit: usize, +} + +impl Future for ReadToEnd<'_> { + type Output = Result, ReadToEndError>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + loop { + match ready!(self.stream.poll_read_chunk(cx, usize::MAX, false))? { + Some(chunk) => { + self.start = self.start.min(chunk.offset); + let end = chunk.bytes.len() as u64 + chunk.offset; + if (end - self.start) > self.size_limit as u64 { + return Poll::Ready(Err(ReadToEndError::TooLong)); + } + self.end = self.end.max(end); + self.read.push((chunk.bytes, chunk.offset)); + } + None => { + if self.end == 0 { + // Never received anything + return Poll::Ready(Ok(Vec::new())); + } + let start = self.start; + let mut buffer = vec![0; (self.end - start) as usize]; + for (data, offset) in self.read.drain(..) { + let offset = (offset - start) as usize; + buffer[offset..offset + data.len()].copy_from_slice(&data); + } + return Poll::Ready(Ok(buffer)); + } + } + } + } +} + +/// Errors from [`RecvStream::read_to_end`] +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ReadToEndError { + /// An error occurred during reading + #[error("read error: {0}")] + Read(#[from] ReadError), + /// The stream is larger than the user-supplied limit + #[error("stream too long")] + TooLong, +} + +#[cfg(feature = "futures-io")] +impl futures_io::AsyncRead for RecvStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + let mut buf = ReadBuf::new(buf); + ready!(Self::poll_read_buf(self.get_mut(), cx, &mut buf))?; + Poll::Ready(Ok(buf.filled().len())) + } +} + +impl tokio::io::AsyncRead for RecvStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + ready!(Self::poll_read_buf(self.get_mut(), cx, buf))?; + Poll::Ready(Ok(())) + } +} + +impl Drop for RecvStream { + fn drop(&mut self) { + let mut conn = self.conn.state.lock("RecvStream::drop"); + + // clean up any previously registered wakers + conn.blocked_readers.remove(&self.stream); + + if conn.error.is_some() || (self.is_0rtt && conn.check_0rtt().is_err()) { + return; + } + if !self.all_data_read { + // Ignore ClosedStream errors + let _ = conn.inner.recv_stream(self.stream).stop(0u32.into()); + conn.wake(); + } + } +} + +/// Errors that arise from reading from a stream. +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ReadError { + /// The peer abandoned transmitting data on this stream + /// + /// Carries an application-defined error code. + #[error("stream reset by peer: error {0}")] + Reset(VarInt), + /// The connection was lost + #[error("connection lost")] + ConnectionLost(#[from] ConnectionError), + /// The stream has already been stopped, finished, or reset + #[error("closed stream")] + ClosedStream, + /// Attempted an ordered read following an unordered read + /// + /// Performing an unordered read allows discontinuities to arise in the receive buffer of a + /// stream which cannot be recovered, making further ordered reads impossible. + #[error("ordered read after unordered read")] + IllegalOrderedRead, + /// This was a 0-RTT stream and the server rejected it + /// + /// Can only occur on clients for 0-RTT streams, which can be opened using + /// [`Connecting::into_0rtt()`]. + /// + /// [`Connecting::into_0rtt()`]: crate::Connecting::into_0rtt() + #[error("0-RTT rejected")] + ZeroRttRejected, +} + +impl From for ReadError { + fn from(e: ReadableError) -> Self { + match e { + ReadableError::ClosedStream => Self::ClosedStream, + ReadableError::IllegalOrderedRead => Self::IllegalOrderedRead, + } + } +} + +impl From for ReadError { + fn from(e: ResetError) -> Self { + match e { + ResetError::ConnectionLost(e) => Self::ConnectionLost(e), + ResetError::ZeroRttRejected => Self::ZeroRttRejected, + } + } +} + +impl From for io::Error { + fn from(x: ReadError) -> Self { + use ReadError::*; + let kind = match x { + Reset { .. } | ZeroRttRejected => io::ErrorKind::ConnectionReset, + ConnectionLost(_) | ClosedStream => io::ErrorKind::NotConnected, + IllegalOrderedRead => io::ErrorKind::InvalidInput, + }; + Self::new(kind, x) + } +} + +/// Errors that arise while waiting for a stream to be reset +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ResetError { + /// The connection was lost + #[error("connection lost")] + ConnectionLost(#[from] ConnectionError), + /// This was a 0-RTT stream and the server rejected it + /// + /// Can only occur on clients for 0-RTT streams, which can be opened using + /// [`Connecting::into_0rtt()`]. + /// + /// [`Connecting::into_0rtt()`]: crate::Connecting::into_0rtt() + #[error("0-RTT rejected")] + ZeroRttRejected, +} + +impl From for io::Error { + fn from(x: ResetError) -> Self { + use ResetError::*; + let kind = match x { + ZeroRttRejected => io::ErrorKind::ConnectionReset, + ConnectionLost(_) => io::ErrorKind::NotConnected, + }; + Self::new(kind, x) + } +} + +/// Future produced by [`RecvStream::read()`]. +/// +/// [`RecvStream::read()`]: crate::RecvStream::read +struct Read<'a> { + stream: &'a mut RecvStream, + buf: ReadBuf<'a>, +} + +impl Future for Read<'_> { + type Output = Result, ReadError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let this = self.get_mut(); + ready!(this.stream.poll_read_buf(cx, &mut this.buf))?; + match this.buf.filled().len() { + 0 if this.buf.capacity() != 0 => Poll::Ready(Ok(None)), + n => Poll::Ready(Ok(Some(n))), + } + } +} + +/// Future produced by [`RecvStream::read_exact()`]. +/// +/// [`RecvStream::read_exact()`]: crate::RecvStream::read_exact +struct ReadExact<'a> { + stream: &'a mut RecvStream, + buf: ReadBuf<'a>, +} + +impl Future for ReadExact<'_> { + type Output = Result<(), ReadExactError>; + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let this = self.get_mut(); + let mut remaining = this.buf.remaining(); + while remaining > 0 { + ready!(this.stream.poll_read_buf(cx, &mut this.buf))?; + let new = this.buf.remaining(); + if new == remaining { + return Poll::Ready(Err(ReadExactError::FinishedEarly(this.buf.filled().len()))); + } + remaining = new; + } + Poll::Ready(Ok(())) + } +} + +/// Errors that arise from reading from a stream. +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ReadExactError { + /// The stream finished before all bytes were read + #[error("stream finished early ({0} bytes read)")] + FinishedEarly(usize), + /// A read error occurred + #[error(transparent)] + ReadError(#[from] ReadError), +} + +/// Future produced by [`RecvStream::read_chunk()`]. +/// +/// [`RecvStream::read_chunk()`]: crate::RecvStream::read_chunk +struct ReadChunk<'a> { + stream: &'a mut RecvStream, + max_length: usize, + ordered: bool, +} + +impl Future for ReadChunk<'_> { + type Output = Result, ReadError>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let (max_length, ordered) = (self.max_length, self.ordered); + self.stream.poll_read_chunk(cx, max_length, ordered) + } +} + +/// Future produced by [`RecvStream::read_chunks()`]. +/// +/// [`RecvStream::read_chunks()`]: crate::RecvStream::read_chunks +struct ReadChunks<'a> { + stream: &'a mut RecvStream, + bufs: &'a mut [Bytes], +} + +impl Future for ReadChunks<'_> { + type Output = Result, ReadError>; + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let this = self.get_mut(); + this.stream.poll_read_chunks(cx, this.bufs) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/runtime.rs b/vendor/flutter_quic/rust/vendor/quinn/src/runtime.rs new file mode 100644 index 0000000..9a7bb69 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/runtime.rs @@ -0,0 +1,200 @@ +use std::{ + fmt::Debug, + future::Future, + io::{self, IoSliceMut}, + net::SocketAddr, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use udp::{RecvMeta, Transmit}; + +use crate::Instant; + +/// Abstracts I/O and timer operations for runtime independence +pub trait Runtime: Send + Sync + Debug + 'static { + /// Construct a timer that will expire at `i` + fn new_timer(&self, i: Instant) -> Pin>; + /// Drive `future` to completion in the background + fn spawn(&self, future: Pin + Send>>); + /// Convert `t` into the socket type used by this runtime + #[cfg(not(wasm_browser))] + fn wrap_udp_socket(&self, t: std::net::UdpSocket) -> io::Result>; + /// Look up the current time + /// + /// Allows simulating the flow of time for testing. + fn now(&self) -> Instant { + Instant::now() + } +} + +/// Abstract implementation of an async timer for runtime independence +pub trait AsyncTimer: Send + Debug + 'static { + /// Update the timer to expire at `i` + fn reset(self: Pin<&mut Self>, i: Instant); + /// Check whether the timer has expired, and register to be woken if not + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()>; +} + +/// Abstract implementation of a UDP socket for runtime independence +pub trait AsyncUdpSocket: Send + Sync + Debug + 'static { + /// Create a [`UdpPoller`] that can register a single task for write-readiness notifications + /// + /// A `poll_send` method on a single object can usually store only one [`Waker`] at a time, + /// i.e. allow at most one caller to wait for an event. This method allows any number of + /// interested tasks to construct their own [`UdpPoller`] object. They can all then wait for the + /// same event and be notified concurrently, because each [`UdpPoller`] can store a separate + /// [`Waker`]. + /// + /// [`Waker`]: std::task::Waker + fn create_io_poller(self: Arc) -> Pin>; + + /// Send UDP datagrams from `transmits`, or return `WouldBlock` and clear the underlying + /// socket's readiness, or return an I/O error + /// + /// If this returns [`io::ErrorKind::WouldBlock`], [`UdpPoller::poll_writable`] must be called + /// to register the calling task to be woken when a send should be attempted again. + fn try_send(&self, transmit: &Transmit) -> io::Result<()>; + + /// Receive UDP datagrams, or register to be woken if receiving may succeed in the future + fn poll_recv( + &self, + cx: &mut Context, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [RecvMeta], + ) -> Poll>; + + /// Look up the local IP address and port used by this socket + fn local_addr(&self) -> io::Result; + + /// Maximum number of datagrams that a [`Transmit`] may encode + fn max_transmit_segments(&self) -> usize { + 1 + } + + /// Maximum number of datagrams that might be described by a single [`RecvMeta`] + fn max_receive_segments(&self) -> usize { + 1 + } + + /// Whether datagrams might get fragmented into multiple parts + /// + /// Sockets should prevent this for best performance. See e.g. the `IPV6_DONTFRAG` socket + /// option. + fn may_fragment(&self) -> bool { + true + } +} + +/// An object polled to detect when an associated [`AsyncUdpSocket`] is writable +/// +/// Any number of `UdpPoller`s may exist for a single [`AsyncUdpSocket`]. Each `UdpPoller` is +/// responsible for notifying at most one task when that socket becomes writable. +pub trait UdpPoller: Send + Sync + Debug + 'static { + /// Check whether the associated socket is likely to be writable + /// + /// Must be called after [`AsyncUdpSocket::try_send`] returns [`io::ErrorKind::WouldBlock`] to + /// register the task associated with `cx` to be woken when a send should be attempted + /// again. Unlike in [`Future::poll`], a [`UdpPoller`] may be reused indefinitely no matter how + /// many times `poll_writable` returns [`Poll::Ready`]. + fn poll_writable(self: Pin<&mut Self>, cx: &mut Context) -> Poll>; +} + +pin_project_lite::pin_project! { + /// Helper adapting a function `MakeFut` that constructs a single-use future `Fut` into a + /// [`UdpPoller`] that may be reused indefinitely + struct UdpPollHelper { + make_fut: MakeFut, + #[pin] + fut: Option, + } +} + +impl UdpPollHelper { + /// Construct a [`UdpPoller`] that calls `make_fut` to get the future to poll, storing it until + /// it yields [`Poll::Ready`], then creating a new one on the next + /// [`poll_writable`](UdpPoller::poll_writable) + #[cfg(any( + feature = "runtime-async-std", + feature = "runtime-smol", + feature = "runtime-tokio", + ))] + fn new(make_fut: MakeFut) -> Self { + Self { + make_fut, + fut: None, + } + } +} + +impl UdpPoller for UdpPollHelper +where + MakeFut: Fn() -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, +{ + fn poll_writable(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let mut this = self.project(); + if this.fut.is_none() { + this.fut.set(Some((this.make_fut)())); + } + // We're forced to `unwrap` here because `Fut` may be `!Unpin`, which means we can't safely + // obtain an `&mut Fut` after storing it in `self.fut` when `self` is already behind `Pin`, + // and if we didn't store it then we wouldn't be able to keep it alive between + // `poll_writable` calls. + let result = this.fut.as_mut().as_pin_mut().unwrap().poll(cx); + if result.is_ready() { + // Polling an arbitrary `Future` after it becomes ready is a logic error, so arrange for + // a new `Future` to be created on the next call. + this.fut.set(None); + } + result + } +} + +impl Debug for UdpPollHelper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UdpPollHelper").finish_non_exhaustive() + } +} + +/// Automatically select an appropriate runtime from those enabled at compile time +/// +/// If `runtime-tokio` is enabled and this function is called from within a Tokio runtime context, +/// then `TokioRuntime` is returned. Otherwise, if `runtime-async-std` is enabled, `AsyncStdRuntime` +/// is returned. Otherwise, if `runtime-smol` is enabled, `SmolRuntime` is returned. +/// Otherwise, `None` is returned. +#[allow(clippy::needless_return)] // Be sure we return the right thing +pub fn default_runtime() -> Option> { + #[cfg(feature = "runtime-tokio")] + { + if ::tokio::runtime::Handle::try_current().is_ok() { + return Some(Arc::new(TokioRuntime)); + } + } + + #[cfg(feature = "runtime-async-std")] + { + return Some(Arc::new(AsyncStdRuntime)); + } + + #[cfg(all(feature = "runtime-smol", not(feature = "runtime-async-std")))] + { + return Some(Arc::new(SmolRuntime)); + } + + #[cfg(not(any(feature = "runtime-async-std", feature = "runtime-smol")))] + None +} + +#[cfg(feature = "runtime-tokio")] +mod tokio; +// Due to MSRV, we must specify `self::` where there's crate/module ambiguity +#[cfg(feature = "runtime-tokio")] +pub use self::tokio::TokioRuntime; + +#[cfg(feature = "async-io")] +mod async_io; +// Due to MSRV, we must specify `self::` where there's crate/module ambiguity +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +pub use self::async_io::*; diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/runtime/async_io.rs b/vendor/flutter_quic/rust/vendor/quinn/src/runtime/async_io.rs new file mode 100644 index 0000000..ab26bdb --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/runtime/async_io.rs @@ -0,0 +1,147 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Instant, +}; +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +use std::{io, sync::Arc, task::ready}; + +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +use async_io::Async; +use async_io::Timer; + +use super::AsyncTimer; +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +use super::{AsyncUdpSocket, Runtime, UdpPollHelper}; + +#[cfg(feature = "runtime-smol")] +// Due to MSRV, we must specify `self::` where there's crate/module ambiguity +pub use self::smol::SmolRuntime; + +#[cfg(feature = "runtime-smol")] +mod smol { + use super::*; + + /// A Quinn runtime for smol + #[derive(Debug)] + pub struct SmolRuntime; + + impl Runtime for SmolRuntime { + fn new_timer(&self, t: Instant) -> Pin> { + Box::pin(Timer::at(t)) + } + + fn spawn(&self, future: Pin + Send>>) { + ::smol::spawn(future).detach(); + } + + fn wrap_udp_socket( + &self, + sock: std::net::UdpSocket, + ) -> io::Result> { + Ok(Arc::new(UdpSocket::new(sock)?)) + } + } +} + +#[cfg(feature = "runtime-async-std")] +// Due to MSRV, we must specify `self::` where there's crate/module ambiguity +pub use self::async_std::AsyncStdRuntime; + +#[cfg(feature = "runtime-async-std")] +mod async_std { + use super::*; + + /// A Quinn runtime for async-std + #[derive(Debug)] + pub struct AsyncStdRuntime; + + impl Runtime for AsyncStdRuntime { + fn new_timer(&self, t: Instant) -> Pin> { + Box::pin(Timer::at(t)) + } + + fn spawn(&self, future: Pin + Send>>) { + ::async_std::task::spawn(future); + } + + fn wrap_udp_socket( + &self, + sock: std::net::UdpSocket, + ) -> io::Result> { + Ok(Arc::new(UdpSocket::new(sock)?)) + } + } +} + +impl AsyncTimer for Timer { + fn reset(mut self: Pin<&mut Self>, t: Instant) { + self.set_at(t) + } + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { + Future::poll(self, cx).map(|_| ()) + } +} + +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +#[derive(Debug)] +struct UdpSocket { + io: Async, + inner: udp::UdpSocketState, +} + +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +impl UdpSocket { + fn new(sock: std::net::UdpSocket) -> io::Result { + Ok(Self { + inner: udp::UdpSocketState::new((&sock).into())?, + io: Async::new_nonblocking(sock)?, + }) + } +} + +#[cfg(any(feature = "runtime-smol", feature = "runtime-async-std"))] +impl AsyncUdpSocket for UdpSocket { + fn create_io_poller(self: Arc) -> Pin> { + Box::pin(UdpPollHelper::new(move || { + let socket = self.clone(); + async move { socket.io.writable().await } + })) + } + + fn try_send(&self, transmit: &udp::Transmit) -> io::Result<()> { + self.inner.send((&self.io).into(), transmit) + } + + fn poll_recv( + &self, + cx: &mut Context, + bufs: &mut [io::IoSliceMut<'_>], + meta: &mut [udp::RecvMeta], + ) -> Poll> { + loop { + ready!(self.io.poll_readable(cx))?; + if let Ok(res) = self.inner.recv((&self.io).into(), bufs, meta) { + return Poll::Ready(Ok(res)); + } + } + } + + fn local_addr(&self) -> io::Result { + self.io.as_ref().local_addr() + } + + fn may_fragment(&self) -> bool { + self.inner.may_fragment() + } + + fn max_transmit_segments(&self) -> usize { + self.inner.max_gso_segments() + } + + fn max_receive_segments(&self) -> usize { + self.inner.gro_segments() + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/runtime/tokio.rs b/vendor/flutter_quic/rust/vendor/quinn/src/runtime/tokio.rs new file mode 100644 index 0000000..0e42366 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/runtime/tokio.rs @@ -0,0 +1,102 @@ +use std::{ + future::Future, + io, + pin::Pin, + sync::Arc, + task::{Context, Poll, ready}, + time::Instant, +}; + +use tokio::{ + io::Interest, + time::{Sleep, sleep_until}, +}; + +use super::{AsyncTimer, AsyncUdpSocket, Runtime, UdpPollHelper}; + +/// A Quinn runtime for Tokio +#[derive(Debug)] +pub struct TokioRuntime; + +impl Runtime for TokioRuntime { + fn new_timer(&self, t: Instant) -> Pin> { + Box::pin(sleep_until(t.into())) + } + + fn spawn(&self, future: Pin + Send>>) { + tokio::spawn(future); + } + + fn wrap_udp_socket(&self, sock: std::net::UdpSocket) -> io::Result> { + Ok(Arc::new(UdpSocket { + inner: udp::UdpSocketState::new((&sock).into())?, + io: tokio::net::UdpSocket::from_std(sock)?, + })) + } + + fn now(&self) -> Instant { + tokio::time::Instant::now().into_std() + } +} + +impl AsyncTimer for Sleep { + fn reset(self: Pin<&mut Self>, t: Instant) { + Self::reset(self, t.into()) + } + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { + Future::poll(self, cx) + } +} + +#[derive(Debug)] +struct UdpSocket { + io: tokio::net::UdpSocket, + inner: udp::UdpSocketState, +} + +impl AsyncUdpSocket for UdpSocket { + fn create_io_poller(self: Arc) -> Pin> { + Box::pin(UdpPollHelper::new(move || { + let socket = self.clone(); + async move { socket.io.writable().await } + })) + } + + fn try_send(&self, transmit: &udp::Transmit) -> io::Result<()> { + self.io.try_io(Interest::WRITABLE, || { + self.inner.send((&self.io).into(), transmit) + }) + } + + fn poll_recv( + &self, + cx: &mut Context, + bufs: &mut [std::io::IoSliceMut<'_>], + meta: &mut [udp::RecvMeta], + ) -> Poll> { + loop { + ready!(self.io.poll_recv_ready(cx))?; + if let Ok(res) = self.io.try_io(Interest::READABLE, || { + self.inner.recv((&self.io).into(), bufs, meta) + }) { + return Poll::Ready(Ok(res)); + } + } + } + + fn local_addr(&self) -> io::Result { + self.io.local_addr() + } + + fn may_fragment(&self) -> bool { + self.inner.may_fragment() + } + + fn max_transmit_segments(&self) -> usize { + self.inner.max_gso_segments() + } + + fn max_receive_segments(&self) -> usize { + self.inner.gro_segments() + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/send_stream.rs b/vendor/flutter_quic/rust/vendor/quinn/src/send_stream.rs new file mode 100644 index 0000000..b1258e0 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/send_stream.rs @@ -0,0 +1,443 @@ +use std::{ + future::{Future, poll_fn}, + io, + pin::{Pin, pin}, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use proto::{ClosedStream, ConnectionError, FinishError, StreamId, Written}; +use thiserror::Error; + +use crate::{ + VarInt, + connection::{ConnectionRef, State}, +}; + +/// A stream that can only be used to send data +/// +/// If dropped, streams that haven't been explicitly [`reset()`] will be implicitly [`finish()`]ed, +/// continuing to (re)transmit previously written data until it has been fully acknowledged or the +/// connection is closed. +/// +/// # Cancellation +/// +/// A `write` method is said to be *cancel-safe* when dropping its future before the future becomes +/// ready will always result in no data being written to the stream. This is true of methods which +/// succeed immediately when any progress is made, and is not true of methods which might need to +/// perform multiple writes internally before succeeding. Each `write` method documents whether it is +/// cancel-safe. +/// +/// [`reset()`]: SendStream::reset +/// [`finish()`]: SendStream::finish +#[derive(Debug)] +pub struct SendStream { + conn: ConnectionRef, + stream: StreamId, + is_0rtt: bool, +} + +impl SendStream { + pub(crate) fn new(conn: ConnectionRef, stream: StreamId, is_0rtt: bool) -> Self { + Self { + conn, + stream, + is_0rtt, + } + } + + /// Write a buffer into this stream, returning how many bytes were written + /// + /// Unless this method errors, it waits until some amount of `buf` can be written into this + /// stream, and then writes as much as it can without waiting again. Due to congestion and flow + /// control, this may be shorter than `buf.len()`. On success this yields the length of the + /// prefix that was written. + /// + /// # Cancel safety + /// + /// This method is cancellation safe. If this does not resolve, no bytes were written. + pub async fn write(&mut self, buf: &[u8]) -> Result { + poll_fn(|cx| self.execute_poll(cx, |s| s.write(buf))).await + } + + /// Write a buffer into this stream in its entirety + /// + /// This method repeatedly calls [`write`](Self::write) until all bytes are written, or an + /// error occurs. + /// + /// # Cancel safety + /// + /// This method is *not* cancellation safe. Even if this does not resolve, some prefix of `buf` + /// may have been written when previously polled. + pub async fn write_all(&mut self, mut buf: &[u8]) -> Result<(), WriteError> { + while !buf.is_empty() { + let written = self.write(buf).await?; + buf = &buf[written..]; + } + Ok(()) + } + + /// Write a slice of [`Bytes`] into this stream, returning how much was written + /// + /// Bytes to try to write are provided to this method as an array of cheaply cloneable chunks. + /// Unless this method errors, it waits until some amount of those bytes can be written into + /// this stream, and then writes as much as it can without waiting again. Due to congestion and + /// flow control, this may be less than the total number of bytes. + /// + /// On success, this method both mutates `bufs` and yields an informative [`Written`] struct + /// indicating how much was written: + /// + /// - [`Bytes`] chunks that were fully written are mutated to be [empty](Bytes::is_empty). + /// - If a [`Bytes`] chunk was partially written, it is [split to](Bytes::split_to) contain + /// only the suffix of bytes that were not written. + /// - The yielded [`Written`] struct indicates how many chunks were fully written as well as + /// how many bytes were written. + /// + /// # Cancel safety + /// + /// This method is cancellation safe. If this does not resolve, no bytes were written. + pub async fn write_chunks(&mut self, bufs: &mut [Bytes]) -> Result { + poll_fn(|cx| self.execute_poll(cx, |s| s.write_chunks(bufs))).await + } + + /// Write a single [`Bytes`] into this stream in its entirety + /// + /// Bytes to write are provided to this method as an single cheaply cloneable chunk. This + /// method repeatedly calls [`write_chunks`](Self::write_chunks) until all bytes are written, + /// or an error occurs. + /// + /// # Cancel safety + /// + /// This method is *not* cancellation safe. Even if this does not resolve, some bytes may have + /// been written when previously polled. + pub async fn write_chunk(&mut self, buf: Bytes) -> Result<(), WriteError> { + self.write_all_chunks(&mut [buf]).await?; + Ok(()) + } + + /// Write a slice of [`Bytes`] into this stream in its entirety + /// + /// Bytes to write are provided to this method as an array of cheaply cloneable chunks. This + /// method repeatedly calls [`write_chunks`](Self::write_chunks) until all bytes are written, + /// or an error occurs. This method mutates `bufs` by mutating all chunks to be + /// [empty](Bytes::is_empty). + /// + /// # Cancel safety + /// + /// This method is *not* cancellation safe. Even if this does not resolve, some bytes may have + /// been written when previously polled. + pub async fn write_all_chunks(&mut self, mut bufs: &mut [Bytes]) -> Result<(), WriteError> { + while !bufs.is_empty() { + let written = self.write_chunks(bufs).await?; + bufs = &mut bufs[written.chunks..]; + } + Ok(()) + } + + fn execute_poll(&mut self, cx: &mut Context, write_fn: F) -> Poll> + where + F: FnOnce(&mut proto::SendStream) -> Result, + { + use proto::WriteError::*; + let mut conn = self.conn.state.lock("SendStream::poll_write"); + if self.is_0rtt { + conn.check_0rtt() + .map_err(|()| WriteError::ZeroRttRejected)?; + } + if let Some(ref x) = conn.error { + return Poll::Ready(Err(WriteError::ConnectionLost(x.clone()))); + } + + let result = match write_fn(&mut conn.inner.send_stream(self.stream)) { + Ok(result) => result, + Err(Blocked) => { + conn.blocked_writers.insert(self.stream, cx.waker().clone()); + return Poll::Pending; + } + Err(Stopped(error_code)) => { + return Poll::Ready(Err(WriteError::Stopped(error_code))); + } + Err(ClosedStream) => { + return Poll::Ready(Err(WriteError::ClosedStream)); + } + }; + + conn.wake(); + Poll::Ready(Ok(result)) + } + + /// Notify the peer that no more data will ever be written to this stream + /// + /// It is an error to write to a [`SendStream`] after `finish()`ing it. [`reset()`](Self::reset) + /// may still be called after `finish` to abandon transmission of any stream data that might + /// still be buffered. + /// + /// To wait for the peer to receive all buffered stream data, see [`stopped()`](Self::stopped). + /// + /// May fail if [`finish()`](Self::finish) or [`reset()`](Self::reset) was previously + /// called. This error is harmless and serves only to indicate that the caller may have + /// incorrect assumptions about the stream's state. + pub fn finish(&mut self) -> Result<(), ClosedStream> { + let mut conn = self.conn.state.lock("finish"); + match conn.inner.send_stream(self.stream).finish() { + Ok(()) => { + conn.wake(); + Ok(()) + } + Err(FinishError::ClosedStream) => Err(ClosedStream::default()), + // Harmless. If the application needs to know about stopped streams at this point, it + // should call `stopped`. + Err(FinishError::Stopped(_)) => Ok(()), + } + } + + /// Close the send stream immediately. + /// + /// No new data can be written after calling this method. Locally buffered data is dropped, and + /// previously transmitted data will no longer be retransmitted if lost. If an attempt has + /// already been made to finish the stream, the peer may still receive all written data. + /// + /// May fail if [`finish()`](Self::finish) or [`reset()`](Self::reset) was previously + /// called. This error is harmless and serves only to indicate that the caller may have + /// incorrect assumptions about the stream's state. + pub fn reset(&mut self, error_code: VarInt) -> Result<(), ClosedStream> { + let mut conn = self.conn.state.lock("SendStream::reset"); + if self.is_0rtt && conn.check_0rtt().is_err() { + return Ok(()); + } + conn.inner.send_stream(self.stream).reset(error_code)?; + conn.wake(); + Ok(()) + } + + /// Set the priority of the send stream + /// + /// Every send stream has an initial priority of 0. Locally buffered data from streams with + /// higher priority will be transmitted before data from streams with lower priority. Changing + /// the priority of a stream with pending data may only take effect after that data has been + /// transmitted. Using many different priority levels per connection may have a negative + /// impact on performance. + pub fn set_priority(&self, priority: i32) -> Result<(), ClosedStream> { + let mut conn = self.conn.state.lock("SendStream::set_priority"); + conn.inner.send_stream(self.stream).set_priority(priority)?; + Ok(()) + } + + /// Get the priority of the send stream + pub fn priority(&self) -> Result { + let mut conn = self.conn.state.lock("SendStream::priority"); + conn.inner.send_stream(self.stream).priority() + } + + /// Completes when the peer stops the stream or reads the stream to completion + /// + /// Yields `Some` with the stop error code if the peer stops the stream. Yields `None` if the + /// local side [`finish()`](Self::finish)es the stream and then the peer acknowledges receipt + /// of all stream data (although not necessarily the processing of it), after which the peer + /// closing the stream is no longer meaningful. + /// + /// For a variety of reasons, the peer may not send acknowledgements immediately upon receiving + /// data. As such, relying on `stopped` to know when the peer has read a stream to completion + /// may introduce more latency than using an application-level response of some sort. + pub fn stopped( + &self, + ) -> impl Future, StoppedError>> + Send + Sync + 'static { + let conn = self.conn.clone(); + let stream = self.stream; + let is_0rtt = self.is_0rtt; + async move { + loop { + // The `Notify::notified` future needs to be created while the lock is being held, + // otherwise a wakeup could be missed if triggered inbetween releasing the lock + // and creating the future. + // The lock may only be held in a block without `await`s, otherwise the future + // becomes `!Send`. `Notify::notified` is lifetime-bound to `Notify`, therefore + // we need to declare `notify` outside of the block, and initialize it inside. + let notify; + { + let mut conn = conn.state.lock("SendStream::stopped"); + if let Some(output) = send_stream_stopped(&mut conn, stream, is_0rtt) { + return output; + } + + notify = conn.stopped.entry(stream).or_default().clone(); + notify.notified() + } + .await + } + } + } + + /// Get the identity of this stream + pub fn id(&self) -> StreamId { + self.stream + } + + /// Attempt to write bytes from buf into the stream. + /// + /// On success, returns Poll::Ready(Ok(num_bytes_written)). + /// + /// If the stream is not ready for writing, the method returns Poll::Pending and arranges + /// for the current task (via cx.waker().wake_by_ref()) to receive a notification when the + /// stream becomes writable or is closed. + pub fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + pin!(self.get_mut().write(buf)).as_mut().poll(cx) + } +} + +/// Check if a send stream is stopped. +/// +/// Returns `Some` if the stream is stopped or the connection is closed. +/// Returns `None` if the stream is not stopped. +fn send_stream_stopped( + conn: &mut State, + stream: StreamId, + is_0rtt: bool, +) -> Option, StoppedError>> { + if is_0rtt && conn.check_0rtt().is_err() { + return Some(Err(StoppedError::ZeroRttRejected)); + } + match conn.inner.send_stream(stream).stopped() { + Err(ClosedStream { .. }) => Some(Ok(None)), + Ok(Some(error_code)) => Some(Ok(Some(error_code))), + Ok(None) => conn.error.clone().map(|error| Err(error.into())), + } +} + +#[cfg(feature = "futures-io")] +impl futures_io::AsyncWrite for SendStream { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll> { + self.poll_write(cx, buf).map_err(Into::into) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(self.get_mut().finish().map_err(Into::into)) + } +} + +impl tokio::io::AsyncWrite for SendStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.poll_write(cx, buf).map_err(Into::into) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(self.get_mut().finish().map_err(Into::into)) + } +} + +impl Drop for SendStream { + fn drop(&mut self) { + let mut conn = self.conn.state.lock("SendStream::drop"); + + // clean up any previously registered wakers + conn.blocked_writers.remove(&self.stream); + + if conn.error.is_some() || (self.is_0rtt && conn.check_0rtt().is_err()) { + return; + } + match conn.inner.send_stream(self.stream).finish() { + Ok(()) => conn.wake(), + Err(FinishError::Stopped(reason)) => { + if conn.inner.send_stream(self.stream).reset(reason).is_ok() { + conn.wake(); + } + } + // Already finished or reset, which is fine. + Err(FinishError::ClosedStream) => {} + } + } +} + +/// Errors that arise from writing to a stream +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum WriteError { + /// The peer is no longer accepting data on this stream + /// + /// Carries an application-defined error code. + #[error("sending stopped by peer: error {0}")] + Stopped(VarInt), + /// The connection was lost + #[error("connection lost")] + ConnectionLost(#[from] ConnectionError), + /// The stream has already been finished or reset + #[error("closed stream")] + ClosedStream, + /// This was a 0-RTT stream and the server rejected it + /// + /// Can only occur on clients for 0-RTT streams, which can be opened using + /// [`Connecting::into_0rtt()`]. + /// + /// [`Connecting::into_0rtt()`]: crate::Connecting::into_0rtt() + #[error("0-RTT rejected")] + ZeroRttRejected, +} + +impl From for WriteError { + #[inline] + fn from(_: ClosedStream) -> Self { + Self::ClosedStream + } +} + +impl From for WriteError { + fn from(x: StoppedError) -> Self { + match x { + StoppedError::ConnectionLost(e) => Self::ConnectionLost(e), + StoppedError::ZeroRttRejected => Self::ZeroRttRejected, + } + } +} + +impl From for io::Error { + fn from(x: WriteError) -> Self { + use WriteError::*; + let kind = match x { + Stopped(_) | ZeroRttRejected => io::ErrorKind::ConnectionReset, + ConnectionLost(_) | ClosedStream => io::ErrorKind::NotConnected, + }; + Self::new(kind, x) + } +} + +/// Errors that arise while monitoring for a send stream stop from the peer +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum StoppedError { + /// The connection was lost + #[error("connection lost")] + ConnectionLost(#[from] ConnectionError), + /// This was a 0-RTT stream and the server rejected it + /// + /// Can only occur on clients for 0-RTT streams, which can be opened using + /// [`Connecting::into_0rtt()`]. + /// + /// [`Connecting::into_0rtt()`]: crate::Connecting::into_0rtt() + #[error("0-RTT rejected")] + ZeroRttRejected, +} + +impl From for io::Error { + fn from(x: StoppedError) -> Self { + use StoppedError::*; + let kind = match x { + ZeroRttRejected => io::ErrorKind::ConnectionReset, + ConnectionLost(_) => io::ErrorKind::NotConnected, + }; + Self::new(kind, x) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/tests.rs b/vendor/flutter_quic/rust/vendor/quinn/src/tests.rs new file mode 100755 index 0000000..f1ec8e8 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/tests.rs @@ -0,0 +1,947 @@ +#![cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] + +#[cfg(all(feature = "rustls-aws-lc-rs", not(feature = "rustls-ring")))] +use rustls::crypto::aws_lc_rs::default_provider; +#[cfg(feature = "rustls-ring")] +use rustls::crypto::ring::default_provider; + +use std::{ + convert::TryInto, + io, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, + str, + sync::Arc, +}; + +use crate::runtime::TokioRuntime; +use crate::{Duration, Instant}; +use bytes::Bytes; +use proto::{RandomConnectionIdGenerator, crypto::rustls::QuicClientConfig}; +use rand::{RngCore, SeedableRng, rngs::StdRng}; +use rustls::{ + RootCertStore, + pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, +}; +use tokio::runtime::{Builder, Runtime}; +use tracing::{error_span, info}; +use tracing_futures::Instrument as _; +use tracing_subscriber::EnvFilter; + +use super::{ClientConfig, Endpoint, EndpointConfig, RecvStream, SendStream, TransportConfig}; + +#[test] +fn handshake_timeout() { + let _guard = subscribe(); + let runtime = rt_threaded(); + let client = { + let _guard = runtime.enter(); + Endpoint::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap() + }; + + // Avoid NoRootAnchors error + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let mut roots = RootCertStore::empty(); + roots.add(cert.cert.into()).unwrap(); + + let mut client_config = crate::ClientConfig::with_root_certificates(Arc::new(roots)).unwrap(); + const IDLE_TIMEOUT: Duration = Duration::from_millis(500); + let mut transport_config = crate::TransportConfig::default(); + transport_config + .max_idle_timeout(Some(IDLE_TIMEOUT.try_into().unwrap())) + .initial_rtt(Duration::from_millis(10)); + client_config.transport_config(Arc::new(transport_config)); + + let start = Instant::now(); + runtime.block_on(async move { + match client + .connect_with( + client_config, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1), + "localhost", + ) + .unwrap() + .await + { + Err(crate::ConnectionError::TimedOut) => {} + Err(e) => panic!("unexpected error: {e:?}"), + Ok(_) => panic!("unexpected success"), + } + }); + let dt = start.elapsed(); + assert!(dt > IDLE_TIMEOUT && dt < 2 * IDLE_TIMEOUT); +} + +#[tokio::test] +async fn close_endpoint() { + let _guard = subscribe(); + + // Avoid NoRootAnchors error + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let mut roots = RootCertStore::empty(); + roots.add(cert.cert.into()).unwrap(); + + let mut endpoint = + Endpoint::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap(); + endpoint + .set_default_client_config(ClientConfig::with_root_certificates(Arc::new(roots)).unwrap()); + + let conn = endpoint + .connect( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234), + "localhost", + ) + .unwrap(); + + tokio::spawn(async move { + let _ = conn.await; + }); + + let conn = endpoint + .connect( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234), + "localhost", + ) + .unwrap(); + endpoint.close(0u32.into(), &[]); + match conn.await { + Err(crate::ConnectionError::LocallyClosed) => (), + Err(e) => panic!("unexpected error: {e}"), + Ok(_) => { + panic!("unexpected success"); + } + } +} + +#[test] +fn local_addr() { + let socket = UdpSocket::bind((Ipv6Addr::LOCALHOST, 0)).unwrap(); + let addr = socket.local_addr().unwrap(); + let runtime = rt_basic(); + let ep = { + let _guard = runtime.enter(); + Endpoint::new(Default::default(), None, socket, Arc::new(TokioRuntime)).unwrap() + }; + assert_eq!( + addr, + ep.local_addr() + .expect("Could not obtain our local endpoint") + ); +} + +#[test] +fn read_after_close() { + let _guard = subscribe(); + let runtime = rt_basic(); + let endpoint = { + let _guard = runtime.enter(); + endpoint() + }; + + const MSG: &[u8] = b"goodbye!"; + let endpoint2 = endpoint.clone(); + runtime.spawn(async move { + let new_conn = endpoint2 + .accept() + .await + .expect("endpoint") + .await + .expect("connection"); + let mut s = new_conn.open_uni().await.unwrap(); + s.write_all(MSG).await.unwrap(); + s.finish().unwrap(); + // Wait for the stream to be closed, one way or another. + _ = s.stopped().await; + }); + runtime.block_on(async move { + let new_conn = endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap() + .await + .expect("connect"); + tokio::time::sleep(Duration::from_millis(100)).await; + let mut stream = new_conn.accept_uni().await.expect("incoming streams"); + let msg = stream.read_to_end(usize::MAX).await.expect("read_to_end"); + assert_eq!(msg, MSG); + }); +} + +#[test] +fn export_keying_material() { + let _guard = subscribe(); + let runtime = rt_basic(); + let endpoint = { + let _guard = runtime.enter(); + endpoint() + }; + + runtime.block_on(async move { + let outgoing_conn_fut = tokio::spawn({ + let endpoint = endpoint.clone(); + async move { + endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap() + .await + .expect("connect") + } + }); + let incoming_conn_fut = tokio::spawn({ + let endpoint = endpoint.clone(); + async move { + endpoint + .accept() + .await + .expect("endpoint") + .await + .expect("connection") + } + }); + let outgoing_conn = outgoing_conn_fut.await.unwrap(); + let incoming_conn = incoming_conn_fut.await.unwrap(); + let mut i_buf = [0u8; 64]; + incoming_conn + .export_keying_material(&mut i_buf, b"asdf", b"qwer") + .unwrap(); + let mut o_buf = [0u8; 64]; + outgoing_conn + .export_keying_material(&mut o_buf, b"asdf", b"qwer") + .unwrap(); + assert_eq!(&i_buf[..], &o_buf[..]); + }); +} + +#[tokio::test] +async fn ip_blocking() { + let _guard = subscribe(); + let endpoint_factory = EndpointFactory::new(); + let client_1 = endpoint_factory.endpoint(); + let client_1_addr = client_1.local_addr().unwrap(); + let client_2 = endpoint_factory.endpoint(); + let server = endpoint_factory.endpoint(); + let server_addr = server.local_addr().unwrap(); + let server_task = tokio::spawn(async move { + loop { + let accepting = server.accept().await.unwrap(); + if accepting.remote_address() == client_1_addr { + accepting.refuse(); + } else if accepting.remote_address_validated() { + accepting.await.expect("connection"); + } else { + accepting.retry().unwrap(); + } + } + }); + tokio::join!( + async move { + let e = client_1 + .connect(server_addr, "localhost") + .unwrap() + .await + .expect_err("server should have blocked this"); + assert!( + matches!(e, crate::ConnectionError::ConnectionClosed(_)), + "wrong error" + ); + }, + async move { + client_2 + .connect(server_addr, "localhost") + .unwrap() + .await + .expect("connect"); + } + ); + server_task.abort(); +} + +/// Construct an endpoint suitable for connecting to itself +fn endpoint() -> Endpoint { + EndpointFactory::new().endpoint() +} + +fn endpoint_with_config(transport_config: TransportConfig) -> Endpoint { + EndpointFactory::new().endpoint_with_config(transport_config) +} + +/// Constructs endpoints suitable for connecting to themselves and each other +struct EndpointFactory { + cert: rcgen::CertifiedKey, + endpoint_config: EndpointConfig, +} + +impl EndpointFactory { + fn new() -> Self { + Self { + cert: rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(), + endpoint_config: EndpointConfig::default(), + } + } + + fn endpoint(&self) -> Endpoint { + self.endpoint_with_config(TransportConfig::default()) + } + + fn endpoint_with_config(&self, transport_config: TransportConfig) -> Endpoint { + let key = PrivateKeyDer::Pkcs8(self.cert.signing_key.serialize_der().into()); + let transport_config = Arc::new(transport_config); + let mut server_config = + crate::ServerConfig::with_single_cert(vec![self.cert.cert.der().clone()], key).unwrap(); + server_config.transport_config(transport_config.clone()); + + let mut roots = rustls::RootCertStore::empty(); + roots.add(self.cert.cert.der().clone()).unwrap(); + let mut endpoint = Endpoint::new( + self.endpoint_config.clone(), + Some(server_config), + UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap(), + Arc::new(TokioRuntime), + ) + .unwrap(); + let mut client_config = ClientConfig::with_root_certificates(Arc::new(roots)).unwrap(); + client_config.transport_config(transport_config); + endpoint.set_default_client_config(client_config); + + endpoint + } +} + +#[tokio::test] +async fn zero_rtt() { + let _guard = subscribe(); + let endpoint = endpoint(); + + const MSG0: &[u8] = b"zero"; + const MSG1: &[u8] = b"one"; + let endpoint2 = endpoint.clone(); + tokio::spawn(async move { + for _ in 0..2 { + let incoming = endpoint2.accept().await.unwrap().accept().unwrap(); + let (connection, established) = incoming.into_0rtt().unwrap_or_else(|_| unreachable!()); + let c = connection.clone(); + tokio::spawn(async move { + while let Ok(mut x) = c.accept_uni().await { + let msg = x.read_to_end(usize::MAX).await.unwrap(); + assert_eq!(msg, MSG0); + } + }); + info!("sending 0.5-RTT"); + let mut s = connection.open_uni().await.expect("open_uni"); + s.write_all(MSG0).await.expect("write"); + s.finish().unwrap(); + established.await; + info!("sending 1-RTT"); + let mut s = connection.open_uni().await.expect("open_uni"); + s.write_all(MSG1).await.expect("write"); + // The peer might close the connection before ACKing + let _ = s.finish(); + } + }); + + let connection = endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap() + .into_0rtt() + .err() + .expect("0-RTT succeeded without keys") + .await + .expect("connect"); + + { + let mut stream = connection.accept_uni().await.expect("incoming streams"); + let msg = stream.read_to_end(usize::MAX).await.expect("read_to_end"); + assert_eq!(msg, MSG0); + // Read a 1-RTT message to ensure the handshake completes fully, allowing the server's + // NewSessionTicket frame to be received. + let mut stream = connection.accept_uni().await.expect("incoming streams"); + let msg = stream.read_to_end(usize::MAX).await.expect("read_to_end"); + assert_eq!(msg, MSG1); + drop(connection); + } + + info!("initial connection complete"); + + let (connection, zero_rtt) = endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap() + .into_0rtt() + .unwrap_or_else(|_| panic!("missing 0-RTT keys")); + // Send something ASAP to use 0-RTT + let c = connection.clone(); + tokio::spawn(async move { + let mut s = c.open_uni().await.expect("0-RTT open uni"); + info!("sending 0-RTT"); + s.write_all(MSG0).await.expect("0-RTT write"); + s.finish().unwrap(); + }); + + let mut stream = connection.accept_uni().await.expect("incoming streams"); + let msg = stream.read_to_end(usize::MAX).await.expect("read_to_end"); + assert_eq!(msg, MSG0); + assert!(zero_rtt.await); + + drop((stream, connection)); + + endpoint.wait_idle().await; +} + +#[test] +#[cfg_attr( + any(target_os = "solaris", target_os = "illumos"), + ignore = "Fails on Solaris and Illumos" +)] +fn echo_v6() { + run_echo(EchoArgs { + client_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), + server_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0), + nr_streams: 1, + stream_size: 10 * 1024, + receive_window: None, + stream_receive_window: None, + }); +} + +#[test] +#[cfg_attr(target_os = "solaris", ignore = "Sometimes hangs in poll() on Solaris")] +fn echo_v4() { + run_echo(EchoArgs { + client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + nr_streams: 1, + stream_size: 10 * 1024, + receive_window: None, + stream_receive_window: None, + }); +} + +#[test] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] +fn echo_dualstack() { + run_echo(EchoArgs { + client_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), + server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + nr_streams: 1, + stream_size: 10 * 1024, + receive_window: None, + stream_receive_window: None, + }); +} + +#[test] +#[ignore] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] +fn stress_receive_window() { + run_echo(EchoArgs { + client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + nr_streams: 50, + stream_size: 25 * 1024 + 11, + receive_window: Some(37), + stream_receive_window: Some(100 * 1024 * 1024), + }); +} + +#[test] +#[ignore] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] +fn stress_stream_receive_window() { + // Note that there is no point in running this with too many streams, + // since the window is only active within a stream. + run_echo(EchoArgs { + client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + nr_streams: 2, + stream_size: 250 * 1024 + 11, + receive_window: Some(100 * 1024 * 1024), + stream_receive_window: Some(37), + }); +} + +#[test] +#[ignore] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] +fn stress_both_windows() { + run_echo(EchoArgs { + client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + nr_streams: 50, + stream_size: 25 * 1024 + 11, + receive_window: Some(37), + stream_receive_window: Some(37), + }); +} + +fn run_echo(args: EchoArgs) { + let _guard = subscribe(); + let runtime = rt_basic(); + let handle = { + // Use small receive windows + let mut transport_config = TransportConfig::default(); + if let Some(receive_window) = args.receive_window { + transport_config.receive_window(receive_window.try_into().unwrap()); + } + if let Some(stream_receive_window) = args.stream_receive_window { + transport_config.stream_receive_window(stream_receive_window.try_into().unwrap()); + } + transport_config.max_concurrent_bidi_streams(1_u8.into()); + transport_config.max_concurrent_uni_streams(1_u8.into()); + let transport_config = Arc::new(transport_config); + + // We don't use the `endpoint` helper here because we want two different endpoints with + // different addresses. + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); + let cert = CertificateDer::from(cert.cert); + let mut server_config = + crate::ServerConfig::with_single_cert(vec![cert.clone()], key.into()).unwrap(); + + server_config.transport = transport_config.clone(); + let server_sock = UdpSocket::bind(args.server_addr).unwrap(); + let server_addr = server_sock.local_addr().unwrap(); + let server = { + let _guard = runtime.enter(); + let _guard = error_span!("server").entered(); + Endpoint::new( + Default::default(), + Some(server_config), + server_sock, + Arc::new(TokioRuntime), + ) + .unwrap() + }; + + let mut roots = rustls::RootCertStore::empty(); + roots.add(cert).unwrap(); + let mut client_crypto = + rustls::ClientConfig::builder_with_provider(default_provider().into()) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(roots) + .with_no_client_auth(); + client_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + + let mut client = { + let _guard = runtime.enter(); + let _guard = error_span!("client").entered(); + Endpoint::client(args.client_addr).unwrap() + }; + let mut client_config = + ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto).unwrap())); + client_config.transport_config(transport_config); + client.set_default_client_config(client_config); + + let handle = runtime.spawn(async move { + let incoming = server.accept().await.unwrap(); + + // Note for anyone modifying the platform support in this test: + // If `local_ip` gets available on additional platforms - which + // requires modifying this test - please update the list of supported + // platforms in the doc comment of `quinn_udp::RecvMeta::dst_ip`. + if cfg!(target_os = "linux") + || cfg!(target_os = "android") + || cfg!(target_os = "freebsd") + || cfg!(target_os = "openbsd") + || cfg!(target_os = "netbsd") + || cfg!(target_os = "macos") + || cfg!(target_os = "windows") + { + let local_ip = incoming.local_ip().expect("Local IP must be available"); + assert!(local_ip.is_loopback()); + } else { + assert_eq!(None, incoming.local_ip()); + } + + let new_conn = incoming.await.unwrap(); + tokio::spawn(async move { + while let Ok(stream) = new_conn.accept_bi().await { + tokio::spawn(echo(stream)); + } + }); + server.wait_idle().await; + }); + + info!("connecting from {} to {}", args.client_addr, server_addr); + runtime.block_on( + async move { + let new_conn = client + .connect(server_addr, "localhost") + .unwrap() + .await + .expect("connect"); + + /// This is just an arbitrary number to generate deterministic test data + const SEED: u64 = 0x12345678; + + for i in 0..args.nr_streams { + println!("Opening stream {i}"); + let (mut send, mut recv) = new_conn.open_bi().await.expect("stream open"); + let msg = gen_data(args.stream_size, SEED); + + let send_task = async { + send.write_all(&msg).await.expect("write"); + send.finish().unwrap(); + }; + let recv_task = async { recv.read_to_end(usize::MAX).await.expect("read") }; + + let (_, data) = tokio::join!(send_task, recv_task); + + assert_eq!(data[..], msg[..], "Data mismatch"); + } + new_conn.close(0u32.into(), b"done"); + client.wait_idle().await; + } + .instrument(error_span!("client")), + ); + handle + }; + runtime.block_on(handle).unwrap(); +} + +struct EchoArgs { + client_addr: SocketAddr, + server_addr: SocketAddr, + nr_streams: usize, + stream_size: usize, + receive_window: Option, + stream_receive_window: Option, +} + +async fn echo((mut send, mut recv): (SendStream, RecvStream)) { + loop { + // These are 32 buffers, for reading approximately 32kB at once + #[rustfmt::skip] + let mut bufs = [ + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + ]; + + match recv.read_chunks(&mut bufs).await.expect("read chunks") { + Some(n) => { + send.write_all_chunks(&mut bufs[..n]) + .await + .expect("write chunks"); + } + None => break, + } + } + + let _ = send.finish(); +} + +fn gen_data(size: usize, seed: u64) -> Vec { + let mut rng: StdRng = SeedableRng::seed_from_u64(seed); + let mut buf = vec![0; size]; + rng.fill_bytes(&mut buf); + buf +} + +fn subscribe() -> tracing::subscriber::DefaultGuard { + let sub = tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_writer(|| TestWriter) + .finish(); + tracing::subscriber::set_default(sub) +} + +struct TestWriter; + +impl std::io::Write for TestWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + print!( + "{}", + str::from_utf8(buf).expect("tried to log invalid UTF-8") + ); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + io::stdout().flush() + } +} + +fn rt_basic() -> Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + +fn rt_threaded() -> Runtime { + Builder::new_multi_thread().enable_all().build().unwrap() +} + +#[tokio::test] +async fn rebind_recv() { + let _guard = subscribe(); + + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); + let cert = CertificateDer::from(cert.cert); + + let mut roots = rustls::RootCertStore::empty(); + roots.add(cert.clone()).unwrap(); + + let mut client = Endpoint::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap(); + let mut client_config = ClientConfig::with_root_certificates(Arc::new(roots)).unwrap(); + client_config.transport_config(Arc::new({ + let mut cfg = TransportConfig::default(); + cfg.max_concurrent_uni_streams(1u32.into()); + cfg + })); + client.set_default_client_config(client_config); + + let server_config = + crate::ServerConfig::with_single_cert(vec![cert.clone()], key.into()).unwrap(); + let server = { + let _guard = tracing::error_span!("server").entered(); + Endpoint::server( + server_config, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + ) + .unwrap() + }; + let server_addr = server.local_addr().unwrap(); + + const MSG: &[u8; 5] = b"hello"; + + let write_send = Arc::new(tokio::sync::Notify::new()); + let write_recv = write_send.clone(); + let connected_send = Arc::new(tokio::sync::Notify::new()); + let connected_recv = connected_send.clone(); + let server = tokio::spawn(async move { + let connection = server.accept().await.unwrap().await.unwrap(); + info!("got conn"); + connected_send.notify_one(); + write_recv.notified().await; + let mut stream = connection.open_uni().await.unwrap(); + stream.write_all(MSG).await.unwrap(); + stream.finish().unwrap(); + // Wait for the stream to be closed, one way or another. + _ = stream.stopped().await; + }); + + let connection = { + let _guard = tracing::error_span!("client").entered(); + client + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap() + }; + info!("connected"); + connected_recv.notified().await; + client + .rebind(UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap()) + .unwrap(); + info!("rebound"); + write_send.notify_one(); + let mut stream = connection.accept_uni().await.unwrap(); + assert_eq!(stream.read_to_end(MSG.len()).await.unwrap(), MSG); + server.await.unwrap(); +} + +#[tokio::test] +async fn stream_id_flow_control() { + let _guard = subscribe(); + let mut cfg = TransportConfig::default(); + cfg.max_concurrent_uni_streams(1u32.into()); + let endpoint = endpoint_with_config(cfg); + + let (client, server) = tokio::join!( + endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap(), + async { endpoint.accept().await.unwrap().await } + ); + let client = client.unwrap(); + let server = server.unwrap(); + + // If `open_uni` doesn't get unblocked when the previous stream is dropped, this will time out. + tokio::join!( + async { + client.open_uni().await.unwrap(); + }, + async { + client.open_uni().await.unwrap(); + }, + async { + client.open_uni().await.unwrap(); + }, + async { + server.accept_uni().await.unwrap(); + server.accept_uni().await.unwrap(); + } + ); +} + +#[tokio::test] +async fn two_datagram_readers() { + let _guard = subscribe(); + let endpoint = endpoint(); + + let (client, server) = tokio::join!( + endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap(), + async { endpoint.accept().await.unwrap().await } + ); + let client = client.unwrap(); + let server = server.unwrap(); + + let done = tokio::sync::Notify::new(); + let (a, b, ()) = tokio::join!( + async { + let x = client.read_datagram().await.unwrap(); + done.notify_waiters(); + x + }, + async { + let x = client.read_datagram().await.unwrap(); + done.notify_waiters(); + x + }, + async { + server.send_datagram(b"one"[..].into()).unwrap(); + done.notified().await; + server.send_datagram_wait(b"two"[..].into()).await.unwrap(); + } + ); + assert!(*a == *b"one" || *b == *b"one"); + assert!(*a == *b"two" || *b == *b"two"); +} + +#[tokio::test] +async fn multiple_conns_with_zero_length_cids() { + let _guard = subscribe(); + let mut factory = EndpointFactory::new(); + factory + .endpoint_config + .cid_generator(|| Box::new(RandomConnectionIdGenerator::new(0))); + let server = { + let _guard = error_span!("server").entered(); + factory.endpoint() + }; + let server_addr = server.local_addr().unwrap(); + + let client1 = { + let _guard = error_span!("client1").entered(); + factory.endpoint() + }; + let client2 = { + let _guard = error_span!("client2").entered(); + factory.endpoint() + }; + + let client1 = async move { + let conn = client1 + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + conn.closed().await; + } + .instrument(error_span!("client1")); + let client2 = async move { + let conn = client2 + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + conn.closed().await; + } + .instrument(error_span!("client2")); + let server = async move { + let client1 = server.accept().await.unwrap().await.unwrap(); + let client2 = server.accept().await.unwrap().await.unwrap(); + // Both connections are now concurrently live. + client1.close(42u32.into(), &[]); + client2.close(42u32.into(), &[]); + } + .instrument(error_span!("server")); + tokio::join!(client1, client2, server); +} + +#[tokio::test] +async fn stream_stopped() { + let _guard = subscribe(); + let factory = EndpointFactory::new(); + let server = { + let _guard = error_span!("server").entered(); + factory.endpoint() + }; + let server_addr = server.local_addr().unwrap(); + + let client = { + let _guard = error_span!("client1").entered(); + factory.endpoint() + }; + + let client = async move { + let conn = client + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + let mut stream = conn.open_uni().await.unwrap(); + let stopped1 = stream.stopped(); + let stopped2 = stream.stopped(); + let stopped3 = stream.stopped(); + + stream.write_all(b"hi").await.unwrap(); + // spawn one of the futures into a task + let stopped1 = tokio::task::spawn(stopped1); + // verify that both futures resolved + let (stopped1, stopped2) = tokio::join!(stopped1, stopped2); + assert!(matches!(stopped1, Ok(Ok(Some(val))) if val == 42u32.into())); + assert!(matches!(stopped2, Ok(Some(val)) if val == 42u32.into())); + // drop the stream + drop(stream); + // verify that a future also resolves after dropping the stream + let stopped3 = stopped3.await; + assert_eq!(stopped3, Ok(Some(42u32.into()))); + }; + let client = + tokio::time::timeout(Duration::from_millis(100), client).instrument(error_span!("client")); + let server = async move { + let conn = server.accept().await.unwrap().await.unwrap(); + let mut stream = conn.accept_uni().await.unwrap(); + let mut buf = [0u8; 2]; + stream.read_exact(&mut buf).await.unwrap(); + stream.stop(42u32.into()).unwrap(); + conn + } + .instrument(error_span!("server")); + let (client, conn) = tokio::join!(client, server); + client.expect("timeout"); + drop(conn); +} + +#[tokio::test] +async fn stream_stopped_2() { + let _guard = subscribe(); + let endpoint = endpoint(); + + let (conn, _server_conn) = tokio::try_join!( + endpoint + .connect(endpoint.local_addr().unwrap(), "localhost") + .unwrap(), + async { endpoint.accept().await.unwrap().await } + ) + .unwrap(); + let send_stream = conn.open_uni().await.unwrap(); + let stopped = tokio::time::timeout(Duration::from_millis(100), send_stream.stopped()) + .instrument(error_span!("stopped")); + tokio::pin!(stopped); + // poll the future once so that the waker is registered. + tokio::select! { + biased; + _x = &mut stopped => {}, + _x = std::future::ready(()) => {} + } + // drop the send stream + drop(send_stream); + // make sure the stopped future still resolves + let res = stopped.await; + assert_eq!(res, Ok(Ok(None))); +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/src/work_limiter.rs b/vendor/flutter_quic/rust/vendor/quinn/src/work_limiter.rs new file mode 100644 index 0000000..c3c3d35 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/src/work_limiter.rs @@ -0,0 +1,224 @@ +use crate::{Duration, Instant}; + +/// Limits the amount of time spent on a certain type of work in a cycle +/// +/// The limiter works dynamically: For a sampled subset of cycles it measures +/// the time that is approximately required for fulfilling 1 work item, and +/// calculates the amount of allowed work items per cycle. +/// The estimates are smoothed over all cycles where the exact duration is measured. +/// +/// In cycles where no measurement is performed the previously determined work limit +/// is used. +/// +/// For the limiter the exact definition of a work item does not matter. +/// It could for example track the amount of transmitted bytes per cycle, +/// or the amount of transmitted datagrams per cycle. +/// It will however work best if the required time to complete a work item is +/// constant. +#[derive(Debug)] +pub(crate) struct WorkLimiter { + /// Whether to measure the required work time, or to use the previous estimates + mode: Mode, + /// The current cycle number + cycle: u16, + /// The time the cycle started - only used in measurement mode + start_time: Option, + /// How many work items have been completed in the cycle + completed: usize, + /// The amount of work items which are allowed for a cycle + allowed: usize, + /// The desired cycle time + desired_cycle_time: Duration, + /// The estimated and smoothed time per work item in nanoseconds + smoothed_time_per_work_item_nanos: f64, +} + +impl WorkLimiter { + pub(crate) fn new(desired_cycle_time: Duration) -> Self { + Self { + mode: Mode::Measure, + cycle: 0, + start_time: None, + completed: 0, + allowed: 0, + desired_cycle_time, + smoothed_time_per_work_item_nanos: 0.0, + } + } + + /// Starts one work cycle + pub(crate) fn start_cycle(&mut self, now: impl Fn() -> Instant) { + self.completed = 0; + if let Mode::Measure = self.mode { + self.start_time = Some(now()); + } + } + + /// Returns whether more work can be performed inside the `desired_cycle_time` + /// + /// Requires that previous work was tracked using `record_work`. + pub(crate) fn allow_work(&mut self, now: impl Fn() -> Instant) -> bool { + match self.mode { + Mode::Measure => (now() - self.start_time.unwrap()) < self.desired_cycle_time, + Mode::HistoricData => self.completed < self.allowed, + } + } + + /// Records that `work` additional work items have been completed inside the cycle + /// + /// Must be called between `start_cycle` and `finish_cycle`. + pub(crate) fn record_work(&mut self, work: usize) { + self.completed += work; + } + + /// Finishes one work cycle + /// + /// For cycles where the exact duration is measured this will update the estimates + /// for the time per work item and the limit of allowed work items per cycle. + /// The estimate is updated using the same exponential averaging (smoothing) + /// mechanism which is used for determining QUIC path rtts: The last value is + /// weighted by 1/8, and the previous average by 7/8. + pub(crate) fn finish_cycle(&mut self, now: impl Fn() -> Instant) { + // If no work was done in the cycle drop the measurement, it won't be useful + if self.completed == 0 { + return; + } + + if let Mode::Measure = self.mode { + let elapsed = now() - self.start_time.unwrap(); + + let time_per_work_item_nanos = (elapsed.as_nanos()) as f64 / self.completed as f64; + + // Calculate the time per work item. We set this to at least 1ns to avoid + // dividing by 0 when calculating the allowed amount of work items. + self.smoothed_time_per_work_item_nanos = if self.allowed == 0 { + // Initial estimate + time_per_work_item_nanos + } else { + // Smoothed estimate + (7.0 * self.smoothed_time_per_work_item_nanos + time_per_work_item_nanos) / 8.0 + } + .max(1.0); + + // Allow at least 1 work item in order to make progress + self.allowed = (((self.desired_cycle_time.as_nanos()) as f64 + / self.smoothed_time_per_work_item_nanos) as usize) + .max(1); + self.start_time = None; + } + + self.cycle = self.cycle.wrapping_add(1); + self.mode = match self.cycle % SAMPLING_INTERVAL { + 0 => Mode::Measure, + _ => Mode::HistoricData, + }; + } +} + +/// We take a measurement sample once every `SAMPLING_INTERVAL` cycles +const SAMPLING_INTERVAL: u16 = 256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + Measure, + HistoricData, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + #[test] + fn limit_work() { + const CYCLE_TIME: Duration = Duration::from_millis(500); + const BATCH_WORK_ITEMS: usize = 12; + const BATCH_TIME: Duration = Duration::from_millis(100); + + const EXPECTED_INITIAL_BATCHES: usize = + (CYCLE_TIME.as_nanos() / BATCH_TIME.as_nanos()) as usize; + const EXPECTED_ALLOWED_WORK_ITEMS: usize = EXPECTED_INITIAL_BATCHES * BATCH_WORK_ITEMS; + + let mut limiter = WorkLimiter::new(CYCLE_TIME); + reset_time(); + + // The initial cycle is measuring + limiter.start_cycle(get_time); + let mut initial_batches = 0; + while limiter.allow_work(get_time) { + limiter.record_work(BATCH_WORK_ITEMS); + advance_time(BATCH_TIME); + initial_batches += 1; + } + limiter.finish_cycle(get_time); + + assert_eq!(initial_batches, EXPECTED_INITIAL_BATCHES); + assert_eq!(limiter.allowed, EXPECTED_ALLOWED_WORK_ITEMS); + let initial_time_per_work_item = limiter.smoothed_time_per_work_item_nanos; + + // The next cycles are using historic data + const BATCH_SIZES: [usize; 4] = [1, 2, 3, 5]; + for &batch_size in &BATCH_SIZES { + limiter.start_cycle(get_time); + let mut allowed_work = 0; + while limiter.allow_work(get_time) { + limiter.record_work(batch_size); + allowed_work += batch_size; + } + limiter.finish_cycle(get_time); + + assert_eq!(allowed_work, EXPECTED_ALLOWED_WORK_ITEMS); + } + + // After `SAMPLING_INTERVAL`, we get into measurement mode again + for _ in 0..(SAMPLING_INTERVAL as usize - BATCH_SIZES.len() - 1) { + limiter.start_cycle(get_time); + limiter.record_work(1); + limiter.finish_cycle(get_time); + } + + // We now do more work per cycle, and expect the estimate of allowed + // work items to go up + const BATCH_WORK_ITEMS_2: usize = 96; + const TIME_PER_WORK_ITEMS_2_NANOS: f64 = + CYCLE_TIME.as_nanos() as f64 / (EXPECTED_INITIAL_BATCHES * BATCH_WORK_ITEMS_2) as f64; + + let expected_updated_time_per_work_item = + (initial_time_per_work_item * 7.0 + TIME_PER_WORK_ITEMS_2_NANOS) / 8.0; + let expected_updated_allowed_work_items = + (CYCLE_TIME.as_nanos() as f64 / expected_updated_time_per_work_item) as usize; + + limiter.start_cycle(get_time); + let mut initial_batches = 0; + while limiter.allow_work(get_time) { + limiter.record_work(BATCH_WORK_ITEMS_2); + advance_time(BATCH_TIME); + initial_batches += 1; + } + limiter.finish_cycle(get_time); + + assert_eq!(initial_batches, EXPECTED_INITIAL_BATCHES); + assert_eq!(limiter.allowed, expected_updated_allowed_work_items); + } + + thread_local! { + /// Mocked time + pub static TIME: RefCell = RefCell::new(Instant::now()); + } + + fn reset_time() { + TIME.with(|t| { + *t.borrow_mut() = Instant::now(); + }) + } + + fn get_time() -> Instant { + TIME.with(|t| *t.borrow()) + } + + fn advance_time(duration: Duration) { + TIME.with(|t| { + *t.borrow_mut() += duration; + }) + } +} diff --git a/vendor/flutter_quic/rust/vendor/quinn/tests/many_connections.rs b/vendor/flutter_quic/rust/vendor/quinn/tests/many_connections.rs new file mode 100644 index 0000000..dc52742 --- /dev/null +++ b/vendor/flutter_quic/rust/vendor/quinn/tests/many_connections.rs @@ -0,0 +1,189 @@ +#![cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))] +use std::{ + convert::TryInto, + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{Arc, Mutex}, + time::Duration, +}; + +use crc::Crc; +use quinn::{ConnectionError, ReadError, StoppedError, TransportConfig, WriteError}; +use rand::{self, RngCore}; +use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; +use tokio::runtime::Builder; + +struct Shared { + errors: Vec, +} + +#[test] +#[ignore] +fn connect_n_nodes_to_1_and_send_1mb_data() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + + let runtime = Builder::new_current_thread().enable_all().build().unwrap(); + let _guard = runtime.enter(); + let shared = Arc::new(Mutex::new(Shared { errors: vec![] })); + + let (cfg, listener_cert) = configure_listener(); + let endpoint = + quinn::Endpoint::server(cfg, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap(); + let listener_addr = endpoint.local_addr().unwrap(); + + let expected_messages = 50; + + let crc = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + let shared2 = shared.clone(); + let endpoint2 = endpoint.clone(); + let read_incoming_data = async move { + for _ in 0..expected_messages { + let conn = endpoint2.accept().await.unwrap().await.unwrap(); + + let shared = shared2.clone(); + let task = async move { + while let Ok(stream) = conn.accept_uni().await { + read_from_peer(stream).await?; + conn.close(0u32.into(), &[]); + } + Ok(()) + }; + tokio::spawn(async move { + if let Err(e) = task.await { + shared.lock().unwrap().errors.push(e); + } + }); + } + }; + runtime.spawn(read_incoming_data); + + let client_cfg = configure_connector(listener_cert); + + for _ in 0..expected_messages { + let data = random_data_with_hash(1024 * 1024, &crc); + let shared = shared.clone(); + let connecting = endpoint + .connect_with(client_cfg.clone(), listener_addr, "localhost") + .unwrap(); + let task = async move { + let conn = connecting.await.map_err(WriteError::ConnectionLost)?; + write_to_peer(conn, data).await?; + Ok(()) + }; + runtime.spawn(async move { + if let Err(e) = task.await { + use quinn::ConnectionError::*; + match e { + WriteError::ConnectionLost(ApplicationClosed { .. }) + | WriteError::ConnectionLost(Reset) => {} + WriteError::ConnectionLost(e) => shared.lock().unwrap().errors.push(e), + _ => panic!("unexpected write error"), + } + } + }); + } + + runtime.block_on(endpoint.wait_idle()); + let shared = shared.lock().unwrap(); + if !shared.errors.is_empty() { + panic!("some connections failed: {:?}", shared.errors); + } +} + +async fn read_from_peer(mut stream: quinn::RecvStream) -> Result<(), quinn::ConnectionError> { + let crc = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + match stream.read_to_end(1024 * 1024 * 5).await { + Ok(data) => { + assert!(hash_correct(&data, &crc)); + Ok(()) + } + Err(e) => { + use ReadError::*; + use quinn::ReadToEndError::*; + match e { + TooLong | Read(ClosedStream) | Read(ZeroRttRejected) | Read(IllegalOrderedRead) => { + unreachable!() + } + Read(Reset(error_code)) => panic!("unexpected stream reset: {error_code}"), + Read(ConnectionLost(e)) => Err(e), + } + } + } +} + +async fn write_to_peer(conn: quinn::Connection, data: Vec) -> Result<(), WriteError> { + let mut s = conn.open_uni().await.map_err(WriteError::ConnectionLost)?; + s.write_all(&data).await?; + s.finish().unwrap(); + // Wait for the stream to be fully received + match s.stopped().await { + Ok(_) => Ok(()), + Err(StoppedError::ConnectionLost(ConnectionError::ApplicationClosed { .. })) => Ok(()), + Err(e) => Err(e.into()), + } +} + +/// Builds client configuration. Trusts given node certificate. +fn configure_connector(node_cert: CertificateDer<'static>) -> quinn::ClientConfig { + let mut roots = rustls::RootCertStore::empty(); + roots.add(node_cert).unwrap(); + + let mut transport_config = TransportConfig::default(); + transport_config.max_idle_timeout(Some(Duration::from_secs(20).try_into().unwrap())); + + let mut peer_cfg = quinn::ClientConfig::with_root_certificates(Arc::new(roots)).unwrap(); + peer_cfg.transport_config(Arc::new(transport_config)); + peer_cfg +} + +/// Builds listener configuration along with its certificate. +fn configure_listener() -> (quinn::ServerConfig, CertificateDer<'static>) { + let (our_cert, our_priv_key) = gen_cert(); + let mut our_cfg = + quinn::ServerConfig::with_single_cert(vec![our_cert.clone()], our_priv_key.into()).unwrap(); + + let transport_config = Arc::get_mut(&mut our_cfg.transport).unwrap(); + transport_config.max_idle_timeout(Some(Duration::from_secs(20).try_into().unwrap())); + + (our_cfg, our_cert) +} + +fn gen_cert() -> (CertificateDer<'static>, PrivatePkcs8KeyDer<'static>) { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); + ( + cert.cert.into(), + PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()), + ) +} + +/// Constructs a buffer with random bytes of given size prefixed with a hash of this data. +fn random_data_with_hash(size: usize, crc: &Crc) -> Vec { + let mut data = random_vec(size + 4); + let hash = crc.checksum(&data[4..]); + // write hash in big endian + data[0] = (hash >> 24) as u8; + data[1] = ((hash >> 16) & 0xff) as u8; + data[2] = ((hash >> 8) & 0xff) as u8; + data[3] = (hash & 0xff) as u8; + data +} + +/// Checks if given data buffer hash is correct. Hash itself is a 4 byte prefix in the data. +fn hash_correct(data: &[u8], crc: &Crc) -> bool { + let encoded_hash = ((data[0] as u32) << 24) + | ((data[1] as u32) << 16) + | ((data[2] as u32) << 8) + | data[3] as u32; + let actual_hash = crc.checksum(&data[4..]); + encoded_hash == actual_hash +} + +fn random_vec(size: usize) -> Vec { + let mut ret = vec![0; size]; + rand::rng().fill_bytes(&mut ret[..]); + ret +} diff --git a/vendor/flutter_quic/scripts/release.sh b/vendor/flutter_quic/scripts/release.sh new file mode 100755 index 0000000..e8900bd --- /dev/null +++ b/vendor/flutter_quic/scripts/release.sh @@ -0,0 +1,219 @@ +#!/bin/bash + +# Flutter QUIC Release Script - Version Bumping Only +# GitHub Actions will handle publishing automatically +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}🚀 Flutter QUIC Release Script${NC}" +echo -e "${YELLOW}📋 This script will: bump version → update changelog → commit → tag${NC}" +echo -e "${YELLOW}📤 GitHub Actions will automatically publish when you push the tag${NC}" +echo "" + +# Check if we're on main branch +CURRENT_BRANCH=$(git branch --show-current) +if [ "$CURRENT_BRANCH" != "main" ]; then + echo -e "${RED}❌ Must be on main branch to release. Current branch: $CURRENT_BRANCH${NC}" + exit 1 +fi + +# Check if working directory is clean +if [ -n "$(git status --porcelain)" ]; then + echo -e "${RED}❌ Working directory is not clean. Please commit or stash changes.${NC}" + exit 1 +fi + +# Get current version +CURRENT_VERSION=$(grep "version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') +echo -e "${YELLOW}Current version: $CURRENT_VERSION${NC}" +echo "" + +# Calculate what each bump type would produce +if [[ $CURRENT_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+)-beta ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + PATCH_VERSION="$MAJOR.$MINOR.$((PATCH + 1))-beta" + MINOR_VERSION="$MAJOR.$((MINOR + 1)).0-beta" + MAJOR_VERSION="$((MAJOR + 1)).0.0-beta" + RELEASE_VERSION="$MAJOR.$MINOR.$PATCH" +elif [[ $CURRENT_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+)-beta\.([0-9]+) ]]; then + # Handle old format (0.1.0-beta.4) + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + PATCH_VERSION="$MAJOR.$MINOR.$((PATCH + 1))-beta" + MINOR_VERSION="$MAJOR.$((MINOR + 1)).0-beta" + MAJOR_VERSION="$((MAJOR + 1)).0.0-beta" + RELEASE_VERSION="$MAJOR.$MINOR.$PATCH" +else + # Fallback for unrecognized formats + PATCH_VERSION="(format not recognized)" + MINOR_VERSION="(format not recognized)" + MAJOR_VERSION="(format not recognized)" + RELEASE_VERSION="(format not recognized)" +fi + +# Ask for version bump type and suggest new version +echo -e "${GREEN}Select version bump type:${NC}" +echo "1) patch ($CURRENT_VERSION → $PATCH_VERSION)" +echo "2) minor ($CURRENT_VERSION → $MINOR_VERSION)" +echo "3) major ($CURRENT_VERSION → $MAJOR_VERSION)" +echo "4) release ($CURRENT_VERSION → $RELEASE_VERSION)" +echo "5) custom (enter your own version)" +read -p "Choose (1-5): " bump_type + +case $bump_type in + 1) # patch - increment patch number + if [[ $CURRENT_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+)-beta ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + NEW_PATCH=$((PATCH + 1)) + SUGGESTED_VERSION="$MAJOR.$MINOR.$NEW_PATCH-beta" + elif [[ $CURRENT_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+)-beta\.([0-9]+) ]]; then + # Handle old format (0.1.0-beta.4) and convert to new format + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + NEW_PATCH=$((PATCH + 1)) + SUGGESTED_VERSION="$MAJOR.$MINOR.$NEW_PATCH-beta" + else + echo -e "${RED}❌ Current version format not recognized for patch bump${NC}" + exit 1 + fi + ;; + 2) # minor - increment minor number, reset patch + if [[ $CURRENT_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + NEW_MINOR=$((MINOR + 1)) + SUGGESTED_VERSION="$MAJOR.$NEW_MINOR.0-beta" + else + echo -e "${RED}❌ Current version format not recognized for minor bump${NC}" + exit 1 + fi + ;; + 3) # major - increment major number, reset minor and patch + if [[ $CURRENT_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + MAJOR="${BASH_REMATCH[1]}" + NEW_MAJOR=$((MAJOR + 1)) + SUGGESTED_VERSION="$NEW_MAJOR.0.0-beta" + else + echo -e "${RED}❌ Current version format not recognized for major bump${NC}" + exit 1 + fi + ;; + 4) # release - remove beta suffix + if [[ $CURRENT_VERSION =~ ([0-9]+\.[0-9]+\.[0-9]+)-beta ]]; then + SUGGESTED_VERSION="${BASH_REMATCH[1]}" + elif [[ $CURRENT_VERSION =~ ([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+) ]]; then + # Handle old format (0.1.0-beta.4) and convert to release + SUGGESTED_VERSION="${BASH_REMATCH[1]}" + else + echo -e "${RED}❌ Current version is not a beta version${NC}" + exit 1 + fi + ;; + 5) # custom + SUGGESTED_VERSION="" + ;; + *) + echo -e "${RED}❌ Invalid choice${NC}" + exit 1 + ;; +esac + +if [[ $bump_type == "5" ]]; then + read -p "Enter custom version (e.g., 0.1.5-beta): " NEW_VERSION +else + echo -e "${GREEN}Suggested new version: $SUGGESTED_VERSION${NC}" + read -p "Press Enter to use suggested version, or type custom version: " custom_input + + if [[ -n "$custom_input" ]]; then + NEW_VERSION="$custom_input" + else + NEW_VERSION="$SUGGESTED_VERSION" + fi +fi + +echo "" +echo -e "${GREEN}New version will be: $NEW_VERSION${NC}" +read -p "Continue with this version? (Y/n): " confirm + +if [[ $confirm == [nN] ]]; then + echo -e "${YELLOW}⚠️ Release cancelled${NC}" + exit 0 +fi + +echo "" +echo -e "${GREEN}🔄 Starting release process...${NC}" + +# Update version in pubspec.yaml +echo -e "${GREEN}📝 Updating pubspec.yaml...${NC}" +sed -i.bak "s/version: $CURRENT_VERSION/version: $NEW_VERSION/" pubspec.yaml +rm pubspec.yaml.bak + +# Update changelog +echo -e "${GREEN}📝 Updating CHANGELOG.md...${NC}" +DATE=$(date +%Y-%m-%d) +# Add new version entry after the header +sed -i.bak "/^and this project adheres to/a\\ +\\ +## [$NEW_VERSION] - $DATE\\ +\\ +### Changed\\ +- Version bump from $CURRENT_VERSION to $NEW_VERSION\\ +" CHANGELOG.md +rm CHANGELOG.md.bak + +# Commit changes +echo -e "${GREEN}📦 Committing version bump...${NC}" +git add pubspec.yaml CHANGELOG.md +git commit -m "chore: bump version to $NEW_VERSION + +- Updated package version from $CURRENT_VERSION to $NEW_VERSION +- Updated changelog with release date" + +# Create tag +echo -e "${GREEN}🏷️ Creating tag v$NEW_VERSION...${NC}" +git tag "v$NEW_VERSION" + +echo "" +echo -e "${GREEN}✅ Release preparation complete!${NC}" +echo -e "${YELLOW}📋 What was done:${NC}" +echo -e " • Version bumped: $CURRENT_VERSION → $NEW_VERSION" +echo -e " • Changelog updated with release date" +echo -e " • Changes committed to git" +echo -e " • Tag v$NEW_VERSION created" +echo "" +echo -e "${GREEN}📤 Next steps:${NC}" +echo -e " 1. Push to GitHub: ${YELLOW}git push origin main && git push origin v$NEW_VERSION${NC}" +echo -e " 2. GitHub Actions will automatically publish to pub.dev" +echo -e " 3. Monitor: https://github.com/ShankarKakumani/flutter_quic/actions" +echo "" + +# Ask if should push to GitHub +read -p "Push to GitHub now? (Y/n): " push_confirm + +if [[ $push_confirm != [nN] ]]; then + echo -e "${GREEN}📤 Pushing to GitHub...${NC}" + git push origin main + git push origin "v$NEW_VERSION" + echo "" + echo -e "${GREEN}🎉 Release $NEW_VERSION completed!${NC}" + echo -e "${YELLOW}📋 Monitor the automated publishing:${NC}" + echo -e " • GitHub Actions: https://github.com/ShankarKakumani/flutter_quic/actions" + echo -e " • pub.dev: https://pub.dev/packages/flutter_quic/versions" +else + echo "" + echo -e "${YELLOW}⚠️ Not pushed to GitHub yet. Run when ready:${NC}" + echo -e "${YELLOW}git push origin main && git push origin v$NEW_VERSION${NC}" +fi \ No newline at end of file diff --git a/vendor/flutter_quic/test/unit/convenience_api_test.dart b/vendor/flutter_quic/test/unit/convenience_api_test.dart new file mode 100644 index 0000000..f718af1 --- /dev/null +++ b/vendor/flutter_quic/test/unit/convenience_api_test.dart @@ -0,0 +1,216 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_quic/flutter_quic.dart' as flutter_quic; + +void main() { + group('QuicClient Convenience API Tests', () { + setUpAll(() async { + // Initialize the Rust library + await flutter_quic.RustLib.init(); + }); + + test('QuicClient creation with default config', () async { + // Test creating a QuicClient with default configuration + final client = await flutter_quic.quicClientCreate(); + expect(client, isNotNull); + expect(client.runtimeType.toString(), contains('QuicClient')); + }); + + test('QuicClient creation with custom config', () async { + // Test creating a custom configuration + final config = await flutter_quic.quicClientConfigNew(); + expect(config, isNotNull); + expect(config.runtimeType.toString(), contains('QuicClientConfig')); + + // Test creating client with custom config + final client = await flutter_quic.quicClientCreateWithConfig( + config: config, + ); + expect(client, isNotNull); + expect(client.runtimeType.toString(), contains('QuicClient')); + }); + + test('QuicClient configuration access', () async { + final client = await flutter_quic.quicClientCreate(); + + // Test accessing configuration + final (updatedClient, config) = await flutter_quic.quicClientConfig( + client: client, + ); + expect(updatedClient, isNotNull); + expect(config, isNotNull); + expect(config.runtimeType.toString(), contains('QuicClientConfig')); + + // Verify default configuration values + expect(config.maxConnectionsPerHost, greaterThan(BigInt.zero)); + expect(config.connectTimeoutMs, greaterThan(BigInt.zero)); + expect(config.requestTimeoutMs, greaterThan(BigInt.zero)); + expect(config.retryAttempts, greaterThan(0)); + expect(config.retryDelayMs, greaterThan(BigInt.zero)); + expect(config.keepAliveTimeoutMs, greaterThan(BigInt.zero)); + }); + + test('QuicClient connection pool management', () async { + final client = await flutter_quic.quicClientCreate(); + + // Test clearing connection pool + final clearedClient = await flutter_quic.quicClientClearPool( + client: client, + ); + expect(clearedClient, isNotNull); + expect(clearedClient.runtimeType.toString(), contains('QuicClient')); + }); + + group('HTTP-style API methods', () { + late flutter_quic.QuicClient client; + + setUp(() async { + client = await flutter_quic.quicClientCreate(); + }); + + test('GET method API structure', () async { + // This will fail due to no server, but tests API structure + try { + final (clientAfterGet, response) = await flutter_quic.quicClientGet( + client: client, + url: 'https://httpbin.org/get', + ); + // If this succeeds somehow, verify the types + expect(clientAfterGet, isNotNull); + expect(response, isA()); + } catch (e) { + // Expected to fail without a QUIC server + expect(e, isNotNull); + // Verify it's a proper error type + expect(e.toString(), isNotEmpty); + } + }); + + test('POST method API structure', () async { + try { + final (clientAfterPost, response) = await flutter_quic.quicClientPost( + client: client, + url: 'https://httpbin.org/post', + data: '{"test": "data"}', + ); + expect(clientAfterPost, isNotNull); + expect(response, isA()); + } catch (e) { + // Expected to fail without a QUIC server + expect(e, isNotNull); + expect(e.toString(), isNotEmpty); + } + }); + + test('GET with timeout API structure', () async { + try { + final (clientAfterTimeout, response) = await flutter_quic + .quicClientGetWithTimeout( + client: client, + url: 'https://httpbin.org/delay/1', + ); + expect(clientAfterTimeout, isNotNull); + expect(response, isA()); + } catch (e) { + // Expected to fail without a QUIC server + expect(e, isNotNull); + expect(e.toString(), isNotEmpty); + } + }); + + test('POST with timeout API structure', () async { + try { + final (clientAfterTimeout, response) = await flutter_quic + .quicClientPostWithTimeout( + client: client, + url: 'https://httpbin.org/post', + data: '{"timeout": "test"}', + ); + expect(clientAfterTimeout, isNotNull); + expect(response, isA()); + } catch (e) { + // Expected to fail without a QUIC server + expect(e, isNotNull); + expect(e.toString(), isNotEmpty); + } + }); + + test('Send method API structure', () async { + try { + final (clientAfterSend, response) = await flutter_quic.quicClientSend( + client: client, + url: 'https://httpbin.org/post', + data: 'test data', + ); + expect(clientAfterSend, isNotNull); + expect(response, isA()); + } catch (e) { + // Expected to fail without a QUIC server + expect(e, isNotNull); + expect(e.toString(), isNotEmpty); + } + }); + + test('Send with timeout API structure', () async { + try { + final (clientAfterTimeout, response) = await flutter_quic + .quicClientSendWithTimeout( + client: client, + url: 'https://httpbin.org/post', + data: 'timeout test data', + ); + expect(clientAfterTimeout, isNotNull); + expect(response, isA()); + } catch (e) { + // Expected to fail without a QUIC server + expect(e, isNotNull); + expect(e.toString(), isNotEmpty); + } + }); + }); + + group('Core API integration', () { + test('Core API still available alongside Convenience API', () async { + // Test that Core API is still accessible + final endpoint = await flutter_quic.createClientEndpoint(); + expect(endpoint, isNotNull); + expect(endpoint.runtimeType.toString(), contains('QuicEndpoint')); + }); + }); + }); + + group('API Design Verification', () { + test('Convenience API built on Core API', () async { + // Verify both APIs are available + final coreEndpoint = await flutter_quic.createClientEndpoint(); + final convenienceClient = await flutter_quic.quicClientCreate(); + + expect(coreEndpoint, isNotNull); + expect(convenienceClient, isNotNull); + + // Both should be different types but available simultaneously + expect(coreEndpoint.runtimeType.toString(), contains('QuicEndpoint')); + expect(convenienceClient.runtimeType.toString(), contains('QuicClient')); + }); + + test('HTTP-style interface consistency', () async { + final client = await flutter_quic.quicClientCreate(); + + // All HTTP methods should have consistent signatures + // This tests the API design without actual network calls + + // GET method should take client and url + expect(() async { + await flutter_quic.quicClientGet(client: client, url: 'test'); + }, throwsA(anything)); // Will throw due to invalid URL/no server + + // POST method should take client, url, and data + expect(() async { + await flutter_quic.quicClientPost( + client: client, + url: 'test', + data: 'data', + ); + }, throwsA(anything)); // Will throw due to invalid URL/no server + }); + }); +} diff --git a/vendor/flutter_quic/windows/CMakeLists.txt b/vendor/flutter_quic/windows/CMakeLists.txt new file mode 100644 index 0000000..8d473ee --- /dev/null +++ b/vendor/flutter_quic/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "flutter_quic") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../rust flutter_quic "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(flutter_quic_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/vendor/xmpp_stone/coverage/lcov.info b/vendor/xmpp_stone/coverage/lcov.info new file mode 100644 index 0000000..03f0e45 --- /dev/null +++ b/vendor/xmpp_stone/coverage/lcov.info @@ -0,0 +1,2633 @@ +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/chat/Message.dart +DA:29,2 +DA:31,1 +DA:41,1 +DA:42,1 +DA:43,1 +DA:44,1 +DA:45,1 +DA:46,1 +DA:47,1 +DA:48,1 +DA:49,1 +DA:52,0 +DA:54,1 +DA:56,2 +DA:57,5 +DA:59,5 +DA:61,1 +DA:63,1 +DA:65,1 +DA:69,1 +DA:70,2 +DA:71,3 +DA:73,1 +DA:75,1 +DA:78,1 +DA:79,2 +DA:80,3 +DA:84,1 +DA:85,2 +DA:86,3 +DA:89,2 +DA:90,2 +DA:91,1 +DA:92,1 +DA:93,1 +DA:94,1 +DA:96,0 +DA:97,1 +DA:107,0 +DA:112,1 +DA:113,1 +DA:114,4 +DA:117,2 +DA:118,2 +DA:119,1 +DA:121,1 +DA:124,1 +DA:125,2 +DA:126,3 +DA:130,1 +DA:131,2 +DA:132,3 +DA:135,2 +DA:136,2 +DA:137,1 +DA:139,3 +DA:140,1 +DA:141,1 +DA:143,0 +DA:144,1 +DA:145,1 +DA:158,0 +DA:163,1 +DA:164,1 +DA:167,1 +DA:169,1 +DA:170,1 +DA:173,0 +DA:176,0 +DA:179,0 +DA:182,0 +DA:190,1 +DA:191,3 +DA:192,2 +DA:195,0 +DA:201,0 +DA:203,0 +DA:205,0 +DA:207,0 +DA:209,0 +DA:211,0 +DA:217,1 +DA:218,1 +DA:219,4 +DA:220,1 +DA:221,1 +DA:222,1 +DA:223,1 +DA:226,1 +DA:227,1 +DA:229,2 +DA:231,1 +DA:234,0 +DA:240,2 +DA:242,0 +DA:244,2 +DA:245,2 +DA:247,2 +DA:249,2 +DA:251,0 +LF:100 +LH:81 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/data/Jid.dart +DA:8,2 +DA:9,2 +DA:10,2 +DA:11,2 +DA:14,0 +DA:16,0 +DA:19,4 +DA:21,2 +DA:23,4 +DA:25,1 +DA:26,1 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,2 +DA:33,0 +DA:35,1 +DA:36,3 +DA:41,1 +DA:42,5 +DA:43,0 +DA:46,0 +DA:47,0 +DA:50,2 +DA:51,2 +DA:52,2 +DA:54,8 +DA:56,1 +DA:60,0 +DA:62,0 +DA:67,2 +LF:32 +LH:20 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/XmppAttribute.dart +DA:5,1 +LF:1 +LH:1 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/XmppElement.dart +DA:10,2 +DA:13,1 +DA:14,5 +DA:17,1 +DA:18,2 +DA:20,0 +DA:22,2 +DA:25,1 +DA:26,2 +DA:29,1 +DA:30,5 +DA:33,0 +DA:34,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:55,0 +DA:56,0 +DA:59,2 +LF:27 +LH:11 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/stanzas/MessageStanza.dart +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,5 +DA:17,2 +DA:18,6 +DA:19,1 +DA:21,1 +DA:22,1 +DA:23,1 +DA:24,1 +DA:25,1 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,2 +DA:40,4 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +LF:30 +LH:16 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/Connection.dart +DA:49,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:74,0 +DA:75,0 +DA:97,0 +DA:98,0 +DA:101,0 +DA:102,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:110,0 +DA:113,0 +DA:114,0 +DA:117,0 +DA:121,0 +DA:122,0 +DA:129,0 +DA:130,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:153,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:171,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:186,0 +DA:187,0 +DA:189,0 +DA:190,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:204,0 +DA:205,0 +DA:207,0 +DA:208,0 +DA:211,0 +DA:214,0 +DA:215,0 +DA:218,0 +DA:219,0 +DA:220,0 +DA:222,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:231,0 +DA:232,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:258,0 +DA:269,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:288,0 +DA:289,0 +DA:290,0 +DA:293,0 +DA:294,0 +DA:295,0 +DA:298,0 +DA:299,0 +DA:300,0 +DA:305,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:311,0 +DA:313,0 +DA:314,0 +DA:319,0 +DA:322,0 +DA:323,0 +DA:324,0 +DA:326,0 +DA:327,0 +DA:328,0 +DA:332,0 +DA:333,0 +DA:334,0 +DA:335,0 +DA:337,0 +DA:338,0 +DA:339,0 +DA:340,0 +DA:342,0 +DA:343,0 +DA:344,0 +DA:345,0 +DA:346,0 +DA:349,0 +DA:350,0 +DA:351,0 +DA:352,0 +DA:356,0 +DA:357,0 +DA:358,0 +DA:360,0 +DA:364,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:371,0 +DA:372,0 +DA:373,0 +DA:374,0 +DA:378,0 +DA:379,0 +DA:380,0 +DA:383,0 +DA:384,0 +DA:385,0 +DA:388,0 +DA:389,0 +DA:390,0 +DA:391,0 +DA:392,0 +DA:395,0 +DA:396,0 +DA:399,0 +DA:400,0 +DA:401,0 +DA:402,0 +DA:403,0 +DA:404,0 +DA:405,0 +DA:409,0 +DA:413,0 +DA:414,0 +DA:415,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:422,0 +DA:423,0 +DA:424,0 +DA:425,0 +DA:426,0 +DA:427,0 +DA:428,0 +DA:429,0 +DA:430,0 +DA:431,0 +DA:435,0 +DA:436,0 +DA:439,0 +DA:440,0 +DA:443,0 +DA:444,0 +DA:445,0 +DA:446,0 +DA:450,0 +DA:451,0 +DA:455,0 +DA:456,0 +DA:457,0 +DA:461,0 +DA:462,0 +DA:463,0 +DA:466,0 +DA:467,0 +DA:470,0 +DA:474,0 +DA:475,0 +DA:476,0 +DA:477,0 +DA:478,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:486,0 +DA:487,0 +DA:490,0 +DA:491,0 +DA:492,0 +DA:495,0 +DA:496,0 +DA:499,0 +DA:500,0 +DA:501,0 +DA:502,0 +DA:503,0 +DA:504,0 +DA:506,0 +DA:510,0 +DA:511,0 +DA:512,0 +DA:515,0 +DA:516,0 +DA:517,0 +LF:252 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/ReconnectionManager.dart +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:57,0 +DA:58,0 +DA:59,0 +LF:31 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/Nonza.dart +DA:11,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +LF:24 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/ConnectionNegotatiorManager.dart +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:70,0 +DA:71,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:83,0 +DA:88,0 +DA:89,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:111,0 +DA:112,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:135,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:157,0 +LF:77 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/servicediscovery/CarbonsNegotiator.dart +DA:12,0 +DA:14,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +LF:34 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/servicediscovery/MAMNegotiator.dart +DA:17,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:45,0 +DA:46,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:70,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:108,0 +DA:109,0 +DA:111,0 +DA:112,0 +DA:118,0 +DA:119,0 +DA:123,0 +DA:124,0 +DA:125,0 +LF:60 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart +DA:19,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:70,0 +DA:72,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:128,0 +DA:129,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:155,0 +DA:156,0 +DA:161,0 +DA:162,0 +LF:83 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/streammanagement/StreamManagmentModule.dart +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:96,0 +DA:97,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:120,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:147,0 +DA:149,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:187,0 +DA:188,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:194,0 +DA:195,0 +DA:198,0 +DA:199,0 +DA:201,0 +DA:202,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:212,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:219,0 +DA:221,0 +DA:222,0 +DA:223,0 +LF:121 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/parser/StanzaParser.dart +DA:20,0 +DA:22,0 +DA:24,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:61,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:78,0 +DA:83,0 +DA:85,0 +DA:86,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +LF:53 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/connection/XmppWebsocketIo.dart +DA:10,0 +DA:11,0 +DA:14,0 +DA:25,0 +DA:27,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:63,0 +DA:65,0 +DA:68,0 +DA:71,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:85,0 +DA:89,0 +DA:92,0 +DA:93,0 +DA:99,0 +DA:106,0 +DA:112,0 +DA:113,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:119,0 +DA:125,0 +DA:127,0 +LF:43 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/logger/Log.dart +DA:4,2 +DA:8,0 +DA:9,0 +DA:10,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:26,1 +DA:27,4 +DA:28,2 +DA:32,0 +DA:33,0 +DA:34,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:47,0 +DA:48,0 +LF:22 +LH:4 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/account/XmppAccountSettings.dart +DA:23,0 +DA:40,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:48,0 +LF:7 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/chat/Chat.dart +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:24,0 +DA:25,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:79,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +LF:49 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/stanzas/AbstractStanza.dart +DA:12,2 +DA:14,1 +DA:15,1 +DA:16,4 +DA:19,2 +DA:21,1 +DA:22,1 +DA:23,2 +DA:24,2 +DA:27,2 +DA:29,1 +DA:30,1 +DA:31,3 +DA:34,0 +DA:37,0 +DA:38,0 +DA:40,0 +LF:17 +LH:13 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/chat/ChatManager.dart +DA:6,0 +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:39,0 +DA:43,0 +DA:44,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +LF:26 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/connection/XmppWebsocketApi.dart +DA:4,0 +DA:5,0 +DA:8,0 +DA:9,0 +LF:4 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/data/privacy_list.dart +DA:7,0 +LF:1 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/data/privacy_list_item.dart +DA:8,0 +DA:14,0 +LF:2 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/data/privacy_lists.dart +DA:6,0 +DA:8,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +LF:6 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/forms/FieldElement.dart +DA:6,0 +DA:7,0 +DA:10,0 +DA:11,0 +DA:13,0 +DA:16,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:26,0 +DA:28,0 +DA:30,0 +LF:13 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/forms/QueryElement.dart +DA:6,0 +DA:7,0 +DA:10,0 +DA:11,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:19,0 +DA:22,0 +LF:9 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/forms/XElement.dart +DA:6,0 +DA:7,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:20,0 +DA:21,0 +LF:10 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/ANonza.dart +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +LF:6 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/EnableNonza.dart +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:15,0 +LF:6 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/EnabledNonza.dart +DA:7,0 +DA:8,0 +DA:10,0 +DA:11,0 +LF:4 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/FailedNonza.dart +DA:7,0 +DA:8,0 +DA:10,0 +DA:11,0 +LF:4 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/RNonza.dart +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +LF:5 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/ResumeNonza.dart +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +LF:7 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/ResumedNonza.dart +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +LF:5 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/nonzas/SMNonza.dart +DA:7,0 +DA:8,0 +DA:10,0 +DA:11,0 +LF:4 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/privacy_lists/active_element.dart +DA:4,0 +DA:5,0 +DA:6,0 +LF:3 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/privacy_lists/named_element.dart +DA:5,0 +DA:6,0 +LF:2 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/privacy_lists/default_element.dart +DA:4,0 +DA:5,0 +DA:6,0 +LF:3 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/privacy_lists/list_element.dart +DA:5,0 +DA:6,0 +DA:7,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:16,0 +DA:17,0 +LF:8 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/privacy_lists/privacy_list_item_element.dart +DA:11,0 +DA:14,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:45,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:80,0 +LF:39 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/utils/string_utils.dart +DA:4,0 +DA:5,0 +DA:6,0 +LF:3 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/stanzas/IqStanza.dart +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +LF:6 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/elements/stanzas/PresenceStanza.dart +DA:7,0 +DA:8,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:43,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:69,0 +DA:71,0 +DA:73,0 +DA:75,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:88,0 +DA:90,0 +DA:92,0 +DA:94,0 +DA:96,0 +DA:103,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:116,0 +DA:117,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:124,0 +LF:59 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/extensions/ping/PingManager.dart +DA:11,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +LF:24 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/extensions/vcard_temp/VCard.dart +DA:13,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:32,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:42,0 +DA:44,0 +DA:46,0 +DA:48,0 +DA:50,0 +DA:52,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:89,0 +DA:91,0 +DA:93,0 +DA:95,0 +DA:97,0 +DA:99,0 +DA:101,0 +DA:103,0 +DA:105,0 +DA:107,0 +DA:109,0 +DA:111,0 +DA:117,0 +DA:118,0 +DA:120,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:133,0 +DA:140,0 +LF:66 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/extensions/vcard_temp/VCardManager.dart +DA:13,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +LF:48 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/BindingResourceNegotiator.dart +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +LF:31 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/Negotiator.dart +DA:13,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:42,0 +LF:12 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/SessionInitiationNegotiator.dart +DA:18,0 +DA:19,0 +DA:21,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +LF:26 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/StartTlsNegotatior.dart +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:53,0 +DA:54,0 +DA:55,0 +LF:27 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/sasl/SaslAuthenticationFeature.dart +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:26,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +LF:58 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/servicediscovery/Feature.dart +DA:5,0 +DA:6,0 +DA:8,0 +DA:9,0 +LF:4 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/message_archive/MessageArchiveManager.dart +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:28,0 +DA:30,0 +DA:32,0 +DA:34,0 +DA:36,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:97,0 +DA:100,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:110,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:135,0 +DA:141,0 +DA:142,0 +LF:74 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/privacy_lists/privacy_lists_manager.dart +DA:30,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:47,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:125,0 +DA:127,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:135,0 +DA:137,0 +DA:138,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:146,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:156,0 +DA:158,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:168,0 +DA:169,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:176,0 +DA:180,0 +DA:182,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:190,0 +DA:192,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:200,0 +DA:204,0 +DA:206,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:214,0 +DA:216,0 +DA:217,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:224,0 +DA:228,0 +DA:230,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:238,0 +DA:240,0 +DA:241,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:248,0 +DA:252,0 +DA:254,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:262,0 +DA:264,0 +DA:265,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:277,0 +DA:278,0 +DA:280,0 +DA:284,0 +DA:286,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:294,0 +DA:296,0 +DA:297,0 +DA:299,0 +DA:300,0 +DA:301,0 +DA:303,0 +DA:305,0 +DA:309,0 +DA:311,0 +DA:314,0 +DA:315,0 +DA:318,0 +DA:319,0 +DA:320,0 +DA:322,0 +LF:162 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/sasl/AbstractSaslHandler.dart +DA:9,0 +LF:1 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/sasl/AnonymousHandler.dart +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +LF:28 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/sasl/PlainSaslHandler.dart +DA:17,0 +DA:18,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +LF:25 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/sasl/ScramSaslHandler.dart +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:106,1 +DA:107,2 +DA:110,0 +DA:111,0 +DA:114,0 +DA:115,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:130,0 +DA:132,0 +DA:133,0 +DA:137,0 +DA:138,0 +DA:140,0 +DA:141,0 +DA:143,0 +DA:144,0 +DA:149,0 +DA:150,0 +DA:153,0 +DA:154,0 +DA:157,0 +DA:158,0 +DA:160,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:174,0 +DA:177,0 +DA:178,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:212,0 +DA:214,0 +DA:215,0 +LF:113 +LH:2 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/servicediscovery/Identity.dart +DA:5,0 +DA:6,0 +DA:9,0 +DA:10,0 +DA:13,0 +DA:15,0 +LF:6 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/features/streammanagement/StreamState.dart +DA:14,0 +LF:1 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/messages/MessageHandler.dart +DA:8,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:26,0 +DA:27,0 +DA:32,0 +DA:34,0 +DA:36,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +LF:20 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/muc/MucManager.dart +DA:16,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:71,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:113,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:123,0 +DA:124,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:147,0 +DA:150,0 +DA:151,0 +DA:154,0 +DA:155,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:173,0 +DA:181,0 +DA:199,0 +DA:217,0 +DA:224,0 +DA:225,0 +LF:93 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/parser/IqParser.dart +DA:8,0 +DA:9,0 +DA:10,0 +DA:13,0 +DA:15,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:25,0 +DA:27,0 +DA:29,0 +LF:11 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/presence/PresenceApi.dart +DA:24,0 +LF:1 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/presence/PresenceManager.dart +DA:25,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:39,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:107,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:115,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:124,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:133,0 +DA:134,0 +DA:136,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:146,0 +DA:148,0 +DA:150,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:164,0 +DA:166,0 +DA:172,0 +DA:173,0 +DA:176,0 +DA:177,0 +DA:179,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:188,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:196,0 +LF:107 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/roster/Buddy.dart +DA:13,0 +DA:15,0 +DA:16,0 +DA:19,0 +DA:21,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:32,0 +LF:10 +LH:0 +end_of_record +SF:/home/dwd/IdeaProjects/Wimsy/vendor/xmpp_stone/lib/src/roster/RosterManager.dart +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:39,0 +DA:40,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:127,0 +DA:130,0 +DA:131,0 +DA:133,0 +DA:139,0 +DA:140,0 +DA:143,0 +DA:144,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:178,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +LF:119 +LH:0 +end_of_record diff --git a/vendor/xmpp_stone/coverage/test/ScramSaslHandler_test.dart.vm.json b/vendor/xmpp_stone/coverage/test/ScramSaslHandler_test.dart.vm.json new file mode 100644 index 0000000..8f548a9 --- /dev/null +++ b/vendor/xmpp_stone/coverage/test/ScramSaslHandler_test.dart.vm.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[{"source":"package:xmpp_stone/src/features/sasl/ScramSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FScramSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/ScramSaslHandler.dart","_kind":"library"},"hits":[38,0,39,0,40,0,41,0,42,0,45,0,47,0,48,0,49,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,62,0,63,0,65,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,78,0,79,0,82,0,83,0,84,0,85,0,86,0,88,0,90,0,91,0,92,0,93,0,94,0,99,0,101,0,102,0,103,0,106,1,107,2,110,0,111,0,114,0,115,0,118,0,119,0,120,0,123,0,125,0,149,0,150,0,153,0,154,0,157,0,158,0,160,0,162,0,163,0,164,0,165,0,166,0,167,0,170,0,171,0,172,0,174,0,177,0,178,0,181,0,182,0,183,0,184,0,185,0,186,0,187,0,188,0,191,0,192,0,193,0,196,0,197,0,198,0,199,0,200,0,201,0,202,0,208,0,209,0,210,0,212,0,214,0,215,0,64,0,126,0,127,0,128,0,130,0,132,0,133,0,137,0,138,0,140,0,141,0,143,0,144,0]},{"source":"package:xmpp_stone/src/Connection.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2FConnection.dart","uri":"package:xmpp_stone/src/Connection.dart","_kind":"library"},"hits":[49,0,137,0,138,0,139,0,140,0,141,0,142,0,143,0,55,0,56,0,57,0,59,0,65,0,66,0,68,0,69,0,74,0,75,0,97,0,98,0,101,0,102,0,105,0,106,0,109,0,110,0,113,0,114,0,117,0,121,0,122,0,129,0,130,0,146,0,147,0,148,0,153,0,157,0,158,0,159,0,160,0,161,0,164,0,165,0,166,0,171,0,175,0,176,0,177,0,178,0,182,0,183,0,184,0,186,0,187,0,189,0,190,0,194,0,195,0,196,0,198,0,199,0,200,0,201,0,202,0,204,0,205,0,207,0,208,0,211,0,214,0,215,0,218,0,219,0,220,0,222,0,236,0,237,0,238,0,242,0,243,0,244,0,246,0,247,0,248,0,249,0,251,0,252,0,253,0,254,0,255,0,258,0,269,0,270,0,271,0,272,0,273,0,274,0,275,0,276,0,277,0,278,0,279,0,280,0,283,0,284,0,285,0,288,0,289,0,290,0,293,0,294,0,295,0,298,0,299,0,300,0,305,0,307,0,308,0,309,0,311,0,313,0,314,0,319,0,322,0,323,0,324,0,326,0,327,0,328,0,332,0,333,0,334,0,335,0,337,0,338,0,339,0,340,0,342,0,343,0,344,0,345,0,349,0,350,0,351,0,352,0,356,0,357,0,358,0,360,0,364,0,365,0,366,0,367,0,368,0,371,0,372,0,373,0,374,0,378,0,379,0,380,0,383,0,384,0,385,0,388,0,389,0,390,0,391,0,392,0,395,0,396,0,399,0,400,0,401,0,402,0,403,0,404,0,405,0,409,0,413,0,414,0,415,0,417,0,418,0,419,0,435,0,436,0,439,0,440,0,443,0,444,0,450,0,451,0,455,0,456,0,457,0,461,0,462,0,463,0,466,0,467,0,470,0,474,0,475,0,476,0,477,0,478,0,479,0,482,0,485,0,486,0,487,0,490,0,491,0,492,0,495,0,496,0,499,0,500,0,501,0,502,0,503,0,504,0,506,0,510,0,511,0,512,0,515,0,516,0,517,0,224,0,225,0,226,0,227,0,228,0,229,0,231,0,232,0,346,0,422,0,423,0,424,0,425,0,426,0,427,0,430,0,431,0,445,0,446,0,428,0,429,0]},{"source":"package:xmpp_stone/src/ReconnectionManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2FReconnectionManager.dart","uri":"package:xmpp_stone/src/ReconnectionManager.dart","_kind":"library"},"hits":[17,0,18,0,19,0,20,0,21,0,22,0,23,0,26,0,27,0,28,0,29,0,30,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,43,0,44,0,45,0,47,0,48,0,49,0,50,0,51,0,53,0,57,0,58,0,59,0]},{"source":"package:xmpp_stone/src/elements/nonzas/Nonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/Nonza.dart","_kind":"library"},"hits":[11,0,13,0,14,0,15,0,18,0,20,0,21,0,22,0,25,0,26,0,27,0,29,0,31,0,32,0,34,0,36,0,37,0,39,0,41,0,40,0,42,0,43,0,44,0,45,0]},{"source":"package:xmpp_stone/src/features/ConnectionNegotatiorManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FConnectionNegotatiorManager.dart","uri":"package:xmpp_stone/src/features/ConnectionNegotatiorManager.dart","_kind":"library"},"hits":[157,0,33,0,35,0,36,0,37,0,38,0,41,0,42,0,44,0,45,0,52,0,53,0,54,0,56,0,59,0,60,0,61,0,62,0,63,0,65,0,66,0,70,0,71,0,73,0,74,0,76,0,77,0,78,0,82,0,83,0,88,0,89,0,93,0,94,0,95,0,96,0,97,0,99,0,100,0,101,0,102,0,104,0,105,0,106,0,107,0,108,0,111,0,112,0,115,0,116,0,117,0,118,0,119,0,120,0,121,0,125,0,126,0,127,0,135,0,139,0,140,0,141,0,142,0,46,0,47,0,48,0,49,0,128,0,129,0,130,0,131,0,132,0,143,0,144,0,145,0,146,0,147,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/CarbonsNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FCarbonsNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/CarbonsNegotiator.dart","_kind":"library"},"hits":[12,0,35,0,36,0,14,0,15,0,17,0,18,0,23,0,24,0,25,0,39,0,41,0,44,0,47,0,49,0,50,0,51,0,52,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,63,0,64,0,67,0,68,0,69,0,70,0,71,0,42,0,43,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/MAMNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FMAMNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/MAMNegotiator.dart","_kind":"library"},"hits":[17,0,45,0,46,0,19,0,20,0,22,0,23,0,28,0,29,0,30,0,49,0,50,0,51,0,53,0,54,0,55,0,57,0,58,0,60,0,63,0,67,0,70,0,72,0,73,0,74,0,75,0,76,0,80,0,81,0,82,0,83,0,84,0,85,0,86,0,89,0,90,0,91,0,93,0,118,0,119,0,123,0,124,0,64,0,65,0,66,0,94,0,95,0,96,0,97,0,99,0,100,0,102,0,103,0,105,0,106,0,108,0,109,0,111,0,112,0,125,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FServiceDiscoveryNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart","_kind":"library"},"hits":[19,0,41,0,42,0,21,0,22,0,24,0,25,0,30,0,31,0,32,0,54,0,55,0,58,0,59,0,60,0,62,0,63,0,64,0,65,0,70,0,72,0,75,0,77,0,78,0,79,0,80,0,81,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,97,0,98,0,99,0,100,0,101,0,103,0,111,0,112,0,114,0,117,0,118,0,119,0,122,0,123,0,124,0,128,0,129,0,132,0,133,0,134,0,135,0,136,0,139,0,142,0,143,0,145,0,146,0,147,0,148,0,149,0,155,0,156,0,161,0,162,0,43,0,104,0,105,0,106,0,107,0,137,0,138,0,150,0,151,0,152,0,153,0]},{"source":"package:xmpp_stone/src/features/streammanagement/StreamManagmentModule.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fstreammanagement%2FStreamManagmentModule.dart","uri":"package:xmpp_stone/src/features/streammanagement/StreamManagmentModule.dart","_kind":"library"},"hits":[21,0,83,0,84,0,85,0,86,0,87,0,88,0,23,0,24,0,26,0,27,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,55,0,56,0,59,0,60,0,61,0,65,0,66,0,67,0,68,0,69,0,71,0,72,0,73,0,75,0,76,0,78,0,103,0,105,0,106,0,110,0,112,0,113,0,114,0,115,0,116,0,117,0,118,0,120,0,125,0,127,0,128,0,129,0,130,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,0,142,0,143,0,144,0,145,0,147,0,149,0,152,0,153,0,154,0,155,0,156,0,161,0,162,0,163,0,166,0,167,0,170,0,171,0,172,0,173,0,174,0,175,0,177,0,178,0,179,0,181,0,182,0,183,0,184,0,187,0,188,0,190,0,191,0,192,0,194,0,195,0,198,0,199,0,201,0,202,0,204,0,205,0,206,0,207,0,208,0,212,0,213,0,214,0,215,0,216,0,219,0,221,0,222,0,223,0,89,0,90,0,92,0,93,0,96,0,97,0]},{"source":"package:xmpp_stone/src/parser/StanzaParser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fparser%2FStanzaParser.dart","uri":"package:xmpp_stone/src/parser/StanzaParser.dart","_kind":"library"},"hits":[20,0,22,0,24,0,27,0,28,0,29,0,30,0,31,0,32,0,34,0,36,0,37,0,39,0,41,0,42,0,44,0,48,0,54,0,55,0,58,0,61,0,64,0,67,0,70,0,73,0,78,0,83,0,85,0,86,0,90,0,92,0,93,0,94,0,95,0,96,0,97,0,98,0,99,0,100,0,101,0,103,0,105,0,106,0,110,0,45,0,46,0,49,0,107,0,108,0,111,0,112,0,113,0,114,0]},{"source":"package:xmpp_stone/src/connection/XmppWebsocketIo.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fconnection%2FXmppWebsocketIo.dart","uri":"package:xmpp_stone/src/connection/XmppWebsocketIo.dart","_kind":"library"},"hits":[25,0,27,0,36,0,37,0,38,0,39,0,41,0,42,0,47,0,48,0,51,0,52,0,53,0,55,0,56,0,63,0,65,0,68,0,71,0,73,0,74,0,76,0,80,0,82,0,83,0,85,0,89,0,92,0,93,0,99,0,106,0,112,0,113,0,115,0,116,0,117,0,125,0,127,0,10,0,11,0,14,0,57,0,119,0]},{"source":"package:xmpp_stone/src/logger/Log.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Flogger%2FLog.dart","uri":"package:xmpp_stone/src/logger/Log.dart","_kind":"library"},"hits":[4,0,8,0,9,0,10,0,14,0,15,0,16,0,20,0,21,0,22,0,26,0,27,0,28,0,32,0,33,0,34,0,38,0,40,0,41,0,45,0,47,0,48,0]},{"source":"package:xmpp_stone/src/account/XmppAccountSettings.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Faccount%2FXmppAccountSettings.dart","uri":"package:xmpp_stone/src/account/XmppAccountSettings.dart","_kind":"library"},"hits":[23,0,40,0,43,0,44,0,46,0,47,0,48,0]},{"source":"package:xmpp_stone/src/data/Jid.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2FJid.dart","uri":"package:xmpp_stone/src/data/Jid.dart","_kind":"library"},"hits":[67,0,8,0,9,0,10,0,11,0,14,0,16,0,19,0,21,0,23,0,25,0,26,0,27,0,28,0,29,0,30,0,32,0,33,0,35,0,36,0,41,0,42,0,43,0,46,0,47,0,50,0,51,0,52,0,54,0,56,0,60,0,62,0]},{"source":"package:xmpp_stone/src/chat/Chat.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FChat.dart","uri":"package:xmpp_stone/src/chat/Chat.dart","_kind":"library"},"hits":[40,0,17,0,18,0,20,0,21,0,24,0,25,0,35,0,36,0,37,0,38,0,42,0,43,0,44,0,45,0,46,0,49,0,50,0,51,0,56,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,79,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0]},{"source":"package:xmpp_stone/src/chat/Message.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FMessage.dart","uri":"package:xmpp_stone/src/chat/Message.dart","_kind":"library"},"hits":[31,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,29,0,52,0,54,0,56,0,59,0,61,0,63,0,65,0,69,0,70,0,73,0,75,0,78,0,79,0,80,0,84,0,85,0,86,0,89,0,90,0,91,0,92,0,93,0,94,0,96,0,97,0,107,0,112,0,113,0,114,0,117,0,118,0,119,0,121,0,124,0,125,0,126,0,130,0,131,0,132,0,135,0,136,0,137,0,139,0,140,0,141,0,143,0,144,0,145,0,158,0,163,0,164,0,167,0,169,0,170,0,173,0,176,0,179,0,182,0,190,0,191,0,195,0,201,0,203,0,205,0,207,0,209,0,211,0,217,0,218,0,219,0,220,0,221,0,222,0,223,0,226,0,227,0,229,0,231,0,234,0,240,0,242,0,244,0,245,0,247,0,249,0,251,0,57,0,71,0,192,0]},{"source":"package:xmpp_stone/src/elements/XmppAttribute.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2FXmppAttribute.dart","uri":"package:xmpp_stone/src/elements/XmppAttribute.dart","_kind":"library"},"hits":[5,0]},{"source":"package:xmpp_stone/src/elements/XmppElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2FXmppElement.dart","uri":"package:xmpp_stone/src/elements/XmppElement.dart","_kind":"library"},"hits":[10,0,13,0,14,0,17,0,18,0,20,0,22,0,25,0,26,0,29,0,30,0,33,0,34,0,37,0,38,0,39,0,40,0,45,0,48,0,49,0,51,0,55,0,56,0,59,0,41,0,42,0,46,0]},{"source":"package:xmpp_stone/src/elements/stanzas/AbstractStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FAbstractStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/AbstractStanza.dart","_kind":"library"},"hits":[12,0,14,0,15,0,16,0,19,0,21,0,22,0,23,0,24,0,27,0,29,0,30,0,31,0,34,0,37,0,40,0,38,0]},{"source":"package:xmpp_stone/src/elements/stanzas/MessageStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FMessageStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/MessageStanza.dart","_kind":"library"},"hits":[9,0,10,0,11,0,12,0,13,0,14,0,17,0,18,0,19,0,21,0,22,0,23,0,24,0,25,0,28,0,29,0,30,0,32,0,33,0,34,0,35,0,36,0,39,0,40,0,41,0,43,0,44,0,45,0,46,0,47,0]},{"source":"package:xmpp_stone/src/chat/ChatManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FChatManager.dart","uri":"package:xmpp_stone/src/chat/ChatManager.dart","_kind":"library"},"hits":[6,0,20,0,21,0,22,0,23,0,24,0,8,0,9,0,11,0,12,0,39,0,43,0,44,0,48,0,49,0,52,0,53,0,55,0,56,0,57,0,25,0,28,0,29,0,30,0,31,0,32,0]},{"source":"package:xmpp_stone/src/connection/XmppWebsocketApi.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fconnection%2FXmppWebsocketApi.dart","uri":"package:xmpp_stone/src/connection/XmppWebsocketApi.dart","_kind":"library"},"hits":[4,0,5,0,8,0,9,0]},{"source":"package:xmpp_stone/src/data/privacy_list.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_list.dart","uri":"package:xmpp_stone/src/data/privacy_list.dart","_kind":"library"},"hits":[7,0]},{"source":"package:xmpp_stone/src/data/privacy_list_item.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_list_item.dart","uri":"package:xmpp_stone/src/data/privacy_list_item.dart","_kind":"library"},"hits":[8,0,14,0]},{"source":"package:xmpp_stone/src/data/privacy_lists.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_lists.dart","uri":"package:xmpp_stone/src/data/privacy_lists.dart","_kind":"library"},"hits":[6,0,8,0,10,0,11,0,12,0,13,0]},{"source":"package:xmpp_stone/src/elements/forms/FieldElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FFieldElement.dart","uri":"package:xmpp_stone/src/elements/forms/FieldElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,13,0,16,0,19,0,20,0,21,0,22,0,26,0,28,0,30,0]},{"source":"package:xmpp_stone/src/elements/forms/QueryElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FQueryElement.dart","uri":"package:xmpp_stone/src/elements/forms/QueryElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,14,0,15,0,18,0,19,0,22,0]},{"source":"package:xmpp_stone/src/elements/forms/XElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FXElement.dart","uri":"package:xmpp_stone/src/elements/forms/XElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,12,0,15,0,16,0,17,0,20,0,21,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ANonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FANonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ANonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,14,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/EnableNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FEnableNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/EnableNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,15,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/EnabledNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FEnabledNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/EnabledNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/nonzas/FailedNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FFailedNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/FailedNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/nonzas/RNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FRNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/RNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ResumeNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FResumeNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ResumeNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,14,0,15,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ResumedNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FResumedNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ResumedNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/SMNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FSMNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/SMNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/active_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Factive_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/active_element.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/named_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fnamed_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/named_element.dart","_kind":"library"},"hits":[5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/default_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fdefault_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/default_element.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/list_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Flist_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/list_element.dart","_kind":"library"},"hits":[5,0,6,0,7,0,10,0,11,0,16,0,17,0,12,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/privacy_list_item_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fprivacy_list_item_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/privacy_list_item_element.dart","_kind":"library"},"hits":[11,0,14,0,17,0,18,0,20,0,21,0,23,0,24,0,26,0,28,0,45,0,53,0,55,0,56,0,57,0,60,0,61,0,64,0,65,0,66,0,68,0,69,0,80,0,29,0,30,0,31,0,33,0,34,0,36,0,37,0,39,0,40,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0]},{"source":"package:xmpp_stone/src/utils/string_utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Futils%2Fstring_utils.dart","uri":"package:xmpp_stone/src/utils/string_utils.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/stanzas/IqStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FIqStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/IqStanza.dart","_kind":"library"},"hits":[8,0,9,0,10,0,11,0,12,0,13,0]},{"source":"package:xmpp_stone/src/elements/stanzas/PresenceStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FPresenceStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/PresenceStanza.dart","_kind":"library"},"hits":[7,0,8,0,11,0,12,0,13,0,16,0,17,0,18,0,21,0,22,0,23,0,26,0,27,0,28,0,31,0,32,0,33,0,37,0,39,0,40,0,43,0,45,0,47,0,48,0,49,0,50,0,52,0,56,0,57,0,59,0,62,0,63,0,66,0,69,0,71,0,73,0,75,0,82,0,84,0,86,0,88,0,90,0,92,0,94,0,96,0,103,0,105,0,107,0,108,0,109,0,110,0,112,0,116,0,117,0,119,0,120,0,121,0,122,0,124,0]},{"source":"package:xmpp_stone/src/extensions/ping/PingManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fping%2FPingManager.dart","uri":"package:xmpp_stone/src/extensions/ping/PingManager.dart","_kind":"library"},"hits":[11,0,16,0,17,0,18,0,19,0,20,0,23,0,24,0,26,0,27,0,32,0,33,0,34,0,35,0,38,0,42,0,43,0,44,0,45,0,47,0,48,0,49,0,50,0,52,0]},{"source":"package:xmpp_stone/src/extensions/vcard_temp/VCard.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fvcard_temp%2FVCard.dart","uri":"package:xmpp_stone/src/extensions/vcard_temp/VCard.dart","_kind":"library"},"hits":[13,0,15,0,17,0,18,0,19,0,22,0,24,0,26,0,28,0,30,0,32,0,34,0,36,0,37,0,39,0,40,0,42,0,44,0,46,0,48,0,50,0,52,0,54,0,56,0,57,0,58,0,59,0,61,0,75,0,76,0,79,0,82,0,83,0,86,0,89,0,91,0,93,0,95,0,97,0,99,0,101,0,103,0,105,0,107,0,109,0,111,0,117,0,118,0,120,0,122,0,123,0,124,0,125,0,126,0,140,0,133,0,60,0,62,0,65,0,66,0,68,0,77,0,78,0,84,0,85,0,63,0]},{"source":"package:xmpp_stone/src/extensions/vcard_temp/VCardManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fvcard_temp%2FVCardManager.dart","uri":"package:xmpp_stone/src/extensions/vcard_temp/VCardManager.dart","_kind":"library"},"hits":[13,0,27,0,28,0,29,0,15,0,16,0,18,0,19,0,36,0,37,0,39,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,65,0,67,0,68,0,71,0,72,0,73,0,74,0,75,0,76,0,78,0,79,0,80,0,82,0,84,0,86,0,87,0,88,0]},{"source":"package:xmpp_stone/src/features/BindingResourceNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FBindingResourceNegotiator.dart","uri":"package:xmpp_stone/src/features/BindingResourceNegotiator.dart","_kind":"library"},"hits":[19,0,20,0,21,0,23,0,26,0,27,0,30,0,32,0,33,0,34,0,35,0,39,0,40,0,41,0,42,0,44,0,45,0,46,0,47,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,63,0]},{"source":"package:xmpp_stone/src/features/Negotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FNegotiator.dart","uri":"package:xmpp_stone/src/features/Negotiator.dart","_kind":"library"},"hits":[13,0,18,0,19,0,22,0,23,0,24,0,32,0,33,0,36,0,37,0,40,0,42,0]},{"source":"package:xmpp_stone/src/features/SessionInitiationNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FSessionInitiationNegotiator.dart","uri":"package:xmpp_stone/src/features/SessionInitiationNegotiator.dart","_kind":"library"},"hits":[18,0,19,0,21,0,24,0,25,0,28,0,30,0,31,0,32,0,33,0,37,0,38,0,39,0,41,0,42,0,43,0,48,0,49,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0]},{"source":"package:xmpp_stone/src/features/StartTlsNegotatior.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FStartTlsNegotatior.dart","uri":"package:xmpp_stone/src/features/StartTlsNegotatior.dart","_kind":"library"},"hits":[16,0,17,0,18,0,19,0,22,0,24,0,25,0,26,0,27,0,28,0,31,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,43,0,45,0,48,0,53,0,54,0,55,0,46,0,47,0]},{"source":"package:xmpp_stone/src/features/sasl/SaslAuthenticationFeature.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FSaslAuthenticationFeature.dart","uri":"package:xmpp_stone/src/features/sasl/SaslAuthenticationFeature.dart","_kind":"library"},"hits":[17,0,18,0,19,0,20,0,21,0,22,0,26,0,29,0,30,0,33,0,35,0,36,0,37,0,41,0,42,0,44,0,45,0,48,0,49,0,51,0,52,0,53,0,55,0,57,0,59,0,60,0,62,0,66,0,67,0,80,0,81,0,82,0,83,0,105,0,108,0,109,0,110,0,111,0,43,0,68,0,69,0,71,0,72,0,73,0,75,0,84,0,85,0,86,0,88,0,89,0,91,0,92,0,94,0,95,0,97,0,98,0,100,0,101,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/Feature.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FFeature.dart","uri":"package:xmpp_stone/src/features/servicediscovery/Feature.dart","_kind":"library"},"hits":[5,0,6,0,8,0,9,0]},{"source":"package:xmpp_stone/src/features/message_archive/MessageArchiveManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fmessage_archive%2FMessageArchiveManager.dart","uri":"package:xmpp_stone/src/features/message_archive/MessageArchiveManager.dart","_kind":"library"},"hits":[15,0,38,0,17,0,18,0,20,0,21,0,28,0,30,0,32,0,34,0,36,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,50,0,52,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,65,0,66,0,67,0,70,0,71,0,72,0,75,0,77,0,78,0,82,0,84,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,97,0,100,0,103,0,105,0,106,0,110,0,114,0,115,0,116,0,118,0,119,0,120,0,121,0,124,0,125,0,126,0,127,0,130,0,131,0,132,0,133,0,135,0,141,0,142,0]},{"source":"package:xmpp_stone/src/features/privacy_lists/privacy_lists_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fprivacy_lists%2Fprivacy_lists_manager.dart","uri":"package:xmpp_stone/src/features/privacy_lists/privacy_lists_manager.dart","_kind":"library"},"hits":[32,0,34,0,35,0,30,0,38,0,39,0,41,0,42,0,47,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,62,0,63,0,66,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,77,0,78,0,79,0,88,0,89,0,90,0,93,0,94,0,95,0,98,0,100,0,101,0,103,0,104,0,105,0,107,0,125,0,127,0,130,0,131,0,132,0,135,0,137,0,138,0,140,0,141,0,142,0,143,0,145,0,156,0,158,0,161,0,162,0,163,0,166,0,168,0,169,0,171,0,172,0,173,0,174,0,176,0,180,0,182,0,185,0,186,0,187,0,190,0,192,0,193,0,195,0,196,0,197,0,198,0,200,0,204,0,206,0,209,0,210,0,211,0,214,0,216,0,217,0,219,0,220,0,221,0,222,0,224,0,228,0,230,0,233,0,234,0,235,0,238,0,240,0,241,0,243,0,244,0,245,0,246,0,248,0,252,0,254,0,257,0,258,0,259,0,262,0,264,0,265,0,267,0,268,0,269,0,271,0,272,0,277,0,278,0,280,0,284,0,286,0,289,0,290,0,291,0,294,0,296,0,297,0,299,0,300,0,301,0,303,0,305,0,309,0,311,0,314,0,315,0,318,0,319,0,320,0,322,0,108,0,110,0,111,0,146,0,148,0,149,0,273,0,112,0,113,0,114,0,115,0,116,0,117,0,118,0,150,0]},{"source":"package:xmpp_stone/src/features/sasl/AbstractSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FAbstractSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/AbstractSaslHandler.dart","_kind":"library"},"hits":[9,0]},{"source":"package:xmpp_stone/src/features/sasl/AnonymousHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FAnonymousHandler.dart","uri":"package:xmpp_stone/src/features/sasl/AnonymousHandler.dart","_kind":"library"},"hits":[25,0,26,0,29,0,31,0,32,0,33,0,36,0,37,0,38,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,63,0,64,0,65,0,66,0]},{"source":"package:xmpp_stone/src/features/sasl/PlainSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FPlainSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/PlainSaslHandler.dart","_kind":"library"},"hits":[17,0,18,0,21,0,23,0,24,0,25,0,28,0,29,0,30,0,31,0,32,0,33,0,34,0,35,0,39,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,50,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/Identity.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FIdentity.dart","uri":"package:xmpp_stone/src/features/servicediscovery/Identity.dart","_kind":"library"},"hits":[5,0,6,0,9,0,10,0,13,0,15,0]},{"source":"package:xmpp_stone/src/features/streammanagement/StreamState.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fstreammanagement%2FStreamState.dart","uri":"package:xmpp_stone/src/features/streammanagement/StreamState.dart","_kind":"library"},"hits":[14,0]},{"source":"package:xmpp_stone/src/messages/MessageHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fmessages%2FMessageHandler.dart","uri":"package:xmpp_stone/src/messages/MessageHandler.dart","_kind":"library"},"hits":[8,0,32,0,10,0,11,0,12,0,13,0,16,0,17,0,19,0,20,0,26,0,27,0,34,0,36,0,39,0,41,0,42,0,43,0,44,0,45,0]},{"source":"package:xmpp_stone/src/muc/MucManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fmuc%2FMucManager.dart","uri":"package:xmpp_stone/src/muc/MucManager.dart","_kind":"library"},"hits":[199,0,217,0,16,0,37,0,38,0,18,0,19,0,21,0,22,0,33,0,34,0,35,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,51,0,52,0,55,0,56,0,57,0,58,0,61,0,62,0,63,0,66,0,67,0,68,0,71,0,75,0,76,0,77,0,78,0,82,0,83,0,84,0,85,0,86,0,90,0,91,0,92,0,93,0,94,0,95,0,98,0,101,0,102,0,103,0,104,0,105,0,107,0,108,0,113,0,118,0,119,0,120,0,123,0,129,0,130,0,131,0,132,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,0,147,0,150,0,151,0,154,0,155,0,158,0,159,0,160,0,163,0,166,0,167,0,168,0,169,0,173,0,181,0,224,0,225,0,124,0]},{"source":"package:xmpp_stone/src/parser/IqParser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fparser%2FIqParser.dart","uri":"package:xmpp_stone/src/parser/IqParser.dart","_kind":"library"},"hits":[8,0,9,0,10,0,13,0,15,0,19,0,21,0,23,0,25,0,27,0,29,0]},{"source":"package:xmpp_stone/src/presence/PresenceApi.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fpresence%2FPresenceApi.dart","uri":"package:xmpp_stone/src/presence/PresenceApi.dart","_kind":"library"},"hits":[24,0]},{"source":"package:xmpp_stone/src/presence/PresenceManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fpresence%2FPresenceManager.dart","uri":"package:xmpp_stone/src/presence/PresenceManager.dart","_kind":"library"},"hits":[46,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,25,0,26,0,27,0,30,0,31,0,34,0,35,0,38,0,39,0,42,0,43,0,48,0,49,0,51,0,52,0,57,0,58,0,59,0,60,0,72,0,74,0,75,0,76,0,77,0,78,0,81,0,83,0,84,0,85,0,86,0,87,0,90,0,92,0,93,0,94,0,95,0,96,0,99,0,101,0,102,0,103,0,104,0,107,0,109,0,110,0,111,0,112,0,115,0,117,0,118,0,119,0,120,0,121,0,124,0,126,0,127,0,128,0,129,0,130,0,133,0,134,0,136,0,138,0,139,0,140,0,141,0,142,0,143,0,145,0,146,0,148,0,150,0,152,0,153,0,154,0,155,0,156,0,158,0,159,0,160,0,161,0,162,0,164,0,166,0,172,0,173,0,176,0,177,0,179,0,183,0,184,0,185,0,188,0,190,0,191,0,192,0,193,0,194,0,196,0]},{"source":"package:xmpp_stone/src/roster/Buddy.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Froster%2FBuddy.dart","uri":"package:xmpp_stone/src/roster/Buddy.dart","_kind":"library"},"hits":[15,0,16,0,13,0,19,0,21,0,24,0,26,0,28,0,30,0,32,0]},{"source":"package:xmpp_stone/src/roster/RosterManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Froster%2FRosterManager.dart","uri":"package:xmpp_stone/src/roster/RosterManager.dart","_kind":"library"},"hits":[15,0,102,0,103,0,104,0,105,0,17,0,18,0,20,0,21,0,26,0,27,0,28,0,29,0,39,0,40,0,47,0,48,0,49,0,50,0,51,0,52,0,53,0,54,0,57,0,58,0,61,0,62,0,65,0,66,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,79,0,80,0,81,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,95,0,96,0,98,0,99,0,108,0,109,0,110,0,111,0,112,0,113,0,117,0,118,0,119,0,120,0,121,0,122,0,123,0,124,0,125,0,127,0,130,0,131,0,133,0,139,0,140,0,143,0,144,0,147,0,148,0,149,0,152,0,153,0,154,0,155,0,156,0,178,0,182,0,183,0,184,0,185,0,188,0,189,0,190,0,194,0,195,0,196,0,197,0,198,0,157,0,158,0,159,0,160,0,161,0,162,0,163,0,164,0,165,0,166,0,172,0,173,0,174,0,175,0,167,0,168,0,169,0]}]} \ No newline at end of file diff --git a/vendor/xmpp_stone/coverage/test/connection_test.dart.vm.json b/vendor/xmpp_stone/coverage/test/connection_test.dart.vm.json new file mode 100644 index 0000000..5a8e6bc --- /dev/null +++ b/vendor/xmpp_stone/coverage/test/connection_test.dart.vm.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[]} \ No newline at end of file diff --git a/vendor/xmpp_stone/coverage/test/jid_test.dart.vm.json b/vendor/xmpp_stone/coverage/test/jid_test.dart.vm.json new file mode 100644 index 0000000..bbccf0f --- /dev/null +++ b/vendor/xmpp_stone/coverage/test/jid_test.dart.vm.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[{"source":"package:xmpp_stone/src/data/Jid.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2FJid.dart","uri":"package:xmpp_stone/src/data/Jid.dart","_kind":"library"},"hits":[67,2,8,1,9,1,10,1,11,1,14,0,16,0,19,2,21,2,23,2,25,0,26,0,27,0,28,0,29,0,30,0,32,0,33,0,35,0,36,0,41,0,42,0,43,0,46,0,47,0,50,1,51,1,52,1,54,4,56,1,60,0,62,0]},{"source":"package:xmpp_stone/src/Connection.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2FConnection.dart","uri":"package:xmpp_stone/src/Connection.dart","_kind":"library"},"hits":[49,0,137,0,138,0,139,0,140,0,141,0,142,0,143,0,55,0,56,0,57,0,59,0,65,0,66,0,68,0,69,0,74,0,75,0,97,0,98,0,101,0,102,0,105,0,106,0,109,0,110,0,113,0,114,0,117,0,121,0,122,0,129,0,130,0,146,0,147,0,148,0,153,0,157,0,158,0,159,0,160,0,161,0,164,0,165,0,166,0,171,0,175,0,176,0,177,0,178,0,182,0,183,0,184,0,186,0,187,0,189,0,190,0,194,0,195,0,196,0,198,0,199,0,200,0,201,0,202,0,204,0,205,0,207,0,208,0,211,0,214,0,215,0,218,0,219,0,220,0,222,0,236,0,237,0,238,0,242,0,243,0,244,0,246,0,247,0,248,0,249,0,251,0,252,0,253,0,254,0,255,0,258,0,269,0,270,0,271,0,272,0,273,0,274,0,275,0,276,0,277,0,278,0,279,0,280,0,283,0,284,0,285,0,288,0,289,0,290,0,293,0,294,0,295,0,298,0,299,0,300,0,305,0,307,0,308,0,309,0,311,0,313,0,314,0,319,0,322,0,323,0,324,0,326,0,327,0,328,0,332,0,333,0,334,0,335,0,337,0,338,0,339,0,340,0,342,0,343,0,344,0,345,0,349,0,350,0,351,0,352,0,356,0,357,0,358,0,360,0,364,0,365,0,366,0,367,0,368,0,371,0,372,0,373,0,374,0,378,0,379,0,380,0,383,0,384,0,385,0,388,0,389,0,390,0,391,0,392,0,395,0,396,0,399,0,400,0,401,0,402,0,403,0,404,0,405,0,409,0,413,0,414,0,415,0,417,0,418,0,419,0,435,0,436,0,439,0,440,0,443,0,444,0,450,0,451,0,455,0,456,0,457,0,461,0,462,0,463,0,466,0,467,0,470,0,474,0,475,0,476,0,477,0,478,0,479,0,482,0,485,0,486,0,487,0,490,0,491,0,492,0,495,0,496,0,499,0,500,0,501,0,502,0,503,0,504,0,506,0,510,0,511,0,512,0,515,0,516,0,517,0,224,0,225,0,226,0,227,0,228,0,229,0,231,0,232,0,346,0,422,0,423,0,424,0,425,0,426,0,427,0,430,0,431,0,445,0,446,0,428,0,429,0]},{"source":"package:xmpp_stone/src/ReconnectionManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2FReconnectionManager.dart","uri":"package:xmpp_stone/src/ReconnectionManager.dart","_kind":"library"},"hits":[17,0,18,0,19,0,20,0,21,0,22,0,23,0,26,0,27,0,28,0,29,0,30,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,43,0,44,0,45,0,47,0,48,0,49,0,50,0,51,0,53,0,57,0,58,0,59,0]},{"source":"package:xmpp_stone/src/elements/nonzas/Nonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/Nonza.dart","_kind":"library"},"hits":[11,0,13,0,14,0,15,0,18,0,20,0,21,0,22,0,25,0,26,0,27,0,29,0,31,0,32,0,34,0,36,0,37,0,39,0,41,0,40,0,42,0,43,0,44,0,45,0]},{"source":"package:xmpp_stone/src/features/ConnectionNegotatiorManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FConnectionNegotatiorManager.dart","uri":"package:xmpp_stone/src/features/ConnectionNegotatiorManager.dart","_kind":"library"},"hits":[157,0,33,0,35,0,36,0,37,0,38,0,41,0,42,0,44,0,45,0,52,0,53,0,54,0,56,0,59,0,60,0,61,0,62,0,63,0,65,0,66,0,70,0,71,0,73,0,74,0,76,0,77,0,78,0,82,0,83,0,88,0,89,0,93,0,94,0,95,0,96,0,97,0,99,0,100,0,101,0,102,0,104,0,105,0,106,0,107,0,108,0,111,0,112,0,115,0,116,0,117,0,118,0,119,0,120,0,121,0,125,0,126,0,127,0,135,0,139,0,140,0,141,0,142,0,46,0,47,0,48,0,49,0,128,0,129,0,130,0,131,0,132,0,143,0,144,0,145,0,146,0,147,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/CarbonsNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FCarbonsNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/CarbonsNegotiator.dart","_kind":"library"},"hits":[12,0,35,0,36,0,14,0,15,0,17,0,18,0,23,0,24,0,25,0,39,0,41,0,44,0,47,0,49,0,50,0,51,0,52,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,63,0,64,0,67,0,68,0,69,0,70,0,71,0,42,0,43,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/MAMNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FMAMNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/MAMNegotiator.dart","_kind":"library"},"hits":[17,0,45,0,46,0,19,0,20,0,22,0,23,0,28,0,29,0,30,0,49,0,50,0,51,0,53,0,54,0,55,0,57,0,58,0,60,0,63,0,67,0,70,0,72,0,73,0,74,0,75,0,76,0,80,0,81,0,82,0,83,0,84,0,85,0,86,0,89,0,90,0,91,0,93,0,118,0,119,0,123,0,124,0,64,0,65,0,66,0,94,0,95,0,96,0,97,0,99,0,100,0,102,0,103,0,105,0,106,0,108,0,109,0,111,0,112,0,125,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FServiceDiscoveryNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart","_kind":"library"},"hits":[19,0,41,0,42,0,21,0,22,0,24,0,25,0,30,0,31,0,32,0,54,0,55,0,58,0,59,0,60,0,62,0,63,0,64,0,65,0,70,0,72,0,75,0,77,0,78,0,79,0,80,0,81,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,97,0,98,0,99,0,100,0,101,0,103,0,111,0,112,0,114,0,117,0,118,0,119,0,122,0,123,0,124,0,128,0,129,0,132,0,133,0,134,0,135,0,136,0,139,0,142,0,143,0,145,0,146,0,147,0,148,0,149,0,155,0,156,0,161,0,162,0,43,0,104,0,105,0,106,0,107,0,137,0,138,0,150,0,151,0,152,0,153,0]},{"source":"package:xmpp_stone/src/features/streammanagement/StreamManagmentModule.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fstreammanagement%2FStreamManagmentModule.dart","uri":"package:xmpp_stone/src/features/streammanagement/StreamManagmentModule.dart","_kind":"library"},"hits":[21,0,83,0,84,0,85,0,86,0,87,0,88,0,23,0,24,0,26,0,27,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,55,0,56,0,59,0,60,0,61,0,65,0,66,0,67,0,68,0,69,0,71,0,72,0,73,0,75,0,76,0,78,0,103,0,105,0,106,0,110,0,112,0,113,0,114,0,115,0,116,0,117,0,118,0,120,0,125,0,127,0,128,0,129,0,130,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,0,142,0,143,0,144,0,145,0,147,0,149,0,152,0,153,0,154,0,155,0,156,0,161,0,162,0,163,0,166,0,167,0,170,0,171,0,172,0,173,0,174,0,175,0,177,0,178,0,179,0,181,0,182,0,183,0,184,0,187,0,188,0,190,0,191,0,192,0,194,0,195,0,198,0,199,0,201,0,202,0,204,0,205,0,206,0,207,0,208,0,212,0,213,0,214,0,215,0,216,0,219,0,221,0,222,0,223,0,89,0,90,0,92,0,93,0,96,0,97,0]},{"source":"package:xmpp_stone/src/parser/StanzaParser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fparser%2FStanzaParser.dart","uri":"package:xmpp_stone/src/parser/StanzaParser.dart","_kind":"library"},"hits":[20,0,22,0,24,0,27,0,28,0,29,0,30,0,31,0,32,0,34,0,36,0,37,0,39,0,41,0,42,0,44,0,48,0,54,0,55,0,58,0,61,0,64,0,67,0,70,0,73,0,78,0,83,0,85,0,86,0,90,0,92,0,93,0,94,0,95,0,96,0,97,0,98,0,99,0,100,0,101,0,103,0,105,0,106,0,110,0,45,0,46,0,49,0,107,0,108,0,111,0,112,0,113,0,114,0]},{"source":"package:xmpp_stone/src/connection/XmppWebsocketIo.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fconnection%2FXmppWebsocketIo.dart","uri":"package:xmpp_stone/src/connection/XmppWebsocketIo.dart","_kind":"library"},"hits":[25,0,27,0,36,0,37,0,38,0,39,0,41,0,42,0,47,0,48,0,51,0,52,0,53,0,55,0,56,0,63,0,65,0,68,0,71,0,73,0,74,0,76,0,80,0,82,0,83,0,85,0,89,0,92,0,93,0,99,0,106,0,112,0,113,0,115,0,116,0,117,0,125,0,127,0,10,0,11,0,14,0,57,0,119,0]},{"source":"package:xmpp_stone/src/logger/Log.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Flogger%2FLog.dart","uri":"package:xmpp_stone/src/logger/Log.dart","_kind":"library"},"hits":[4,0,8,0,9,0,10,0,14,0,15,0,16,0,20,0,21,0,22,0,26,0,27,0,28,0,32,0,33,0,34,0,38,0,40,0,41,0,45,0,47,0,48,0]},{"source":"package:xmpp_stone/src/account/XmppAccountSettings.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Faccount%2FXmppAccountSettings.dart","uri":"package:xmpp_stone/src/account/XmppAccountSettings.dart","_kind":"library"},"hits":[23,0,40,0,43,0,44,0,46,0,47,0,48,0]},{"source":"package:xmpp_stone/src/chat/Chat.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FChat.dart","uri":"package:xmpp_stone/src/chat/Chat.dart","_kind":"library"},"hits":[40,0,17,0,18,0,20,0,21,0,24,0,25,0,35,0,36,0,37,0,38,0,42,0,43,0,44,0,45,0,46,0,49,0,50,0,51,0,56,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,79,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0]},{"source":"package:xmpp_stone/src/chat/Message.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FMessage.dart","uri":"package:xmpp_stone/src/chat/Message.dart","_kind":"library"},"hits":[31,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,29,0,52,0,54,0,56,0,59,0,61,0,63,0,65,0,69,0,70,0,73,0,75,0,78,0,79,0,80,0,84,0,85,0,86,0,89,0,90,0,91,0,92,0,93,0,94,0,96,0,97,0,107,0,112,0,113,0,114,0,117,0,118,0,119,0,121,0,124,0,125,0,126,0,130,0,131,0,132,0,135,0,136,0,137,0,139,0,140,0,141,0,143,0,144,0,145,0,158,0,163,0,164,0,167,0,169,0,170,0,173,0,176,0,179,0,182,0,190,0,191,0,195,0,201,0,203,0,205,0,207,0,209,0,211,0,217,0,218,0,219,0,220,0,221,0,222,0,223,0,226,0,227,0,229,0,231,0,234,0,240,0,242,0,244,0,245,0,247,0,249,0,251,0,57,0,71,0,192,0]},{"source":"package:xmpp_stone/src/elements/XmppAttribute.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2FXmppAttribute.dart","uri":"package:xmpp_stone/src/elements/XmppAttribute.dart","_kind":"library"},"hits":[5,0]},{"source":"package:xmpp_stone/src/elements/XmppElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2FXmppElement.dart","uri":"package:xmpp_stone/src/elements/XmppElement.dart","_kind":"library"},"hits":[10,0,13,0,14,0,17,0,18,0,20,0,22,0,25,0,26,0,29,0,30,0,33,0,34,0,37,0,38,0,39,0,40,0,45,0,48,0,49,0,51,0,55,0,56,0,59,0,41,0,42,0,46,0]},{"source":"package:xmpp_stone/src/elements/stanzas/AbstractStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FAbstractStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/AbstractStanza.dart","_kind":"library"},"hits":[12,0,14,0,15,0,16,0,19,0,21,0,22,0,23,0,24,0,27,0,29,0,30,0,31,0,34,0,37,0,40,0,38,0]},{"source":"package:xmpp_stone/src/elements/stanzas/MessageStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FMessageStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/MessageStanza.dart","_kind":"library"},"hits":[9,0,10,0,11,0,12,0,13,0,14,0,17,0,18,0,19,0,21,0,22,0,23,0,24,0,25,0,28,0,29,0,30,0,32,0,33,0,34,0,35,0,36,0,39,0,40,0,41,0,43,0,44,0,45,0,46,0,47,0]},{"source":"package:xmpp_stone/src/chat/ChatManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FChatManager.dart","uri":"package:xmpp_stone/src/chat/ChatManager.dart","_kind":"library"},"hits":[6,0,20,0,21,0,22,0,23,0,24,0,8,0,9,0,11,0,12,0,39,0,43,0,44,0,48,0,49,0,52,0,53,0,55,0,56,0,57,0,25,0,28,0,29,0,30,0,31,0,32,0]},{"source":"package:xmpp_stone/src/connection/XmppWebsocketApi.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fconnection%2FXmppWebsocketApi.dart","uri":"package:xmpp_stone/src/connection/XmppWebsocketApi.dart","_kind":"library"},"hits":[4,0,5,0,8,0,9,0]},{"source":"package:xmpp_stone/src/data/privacy_list.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_list.dart","uri":"package:xmpp_stone/src/data/privacy_list.dart","_kind":"library"},"hits":[7,0]},{"source":"package:xmpp_stone/src/data/privacy_list_item.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_list_item.dart","uri":"package:xmpp_stone/src/data/privacy_list_item.dart","_kind":"library"},"hits":[8,0,14,0]},{"source":"package:xmpp_stone/src/data/privacy_lists.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_lists.dart","uri":"package:xmpp_stone/src/data/privacy_lists.dart","_kind":"library"},"hits":[6,0,8,0,10,0,11,0,12,0,13,0]},{"source":"package:xmpp_stone/src/elements/forms/FieldElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FFieldElement.dart","uri":"package:xmpp_stone/src/elements/forms/FieldElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,13,0,16,0,19,0,20,0,21,0,22,0,26,0,28,0,30,0]},{"source":"package:xmpp_stone/src/elements/forms/QueryElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FQueryElement.dart","uri":"package:xmpp_stone/src/elements/forms/QueryElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,14,0,15,0,18,0,19,0,22,0]},{"source":"package:xmpp_stone/src/elements/forms/XElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FXElement.dart","uri":"package:xmpp_stone/src/elements/forms/XElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,12,0,15,0,16,0,17,0,20,0,21,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ANonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FANonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ANonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,14,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/EnableNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FEnableNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/EnableNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,15,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/EnabledNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FEnabledNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/EnabledNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/nonzas/FailedNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FFailedNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/FailedNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/nonzas/RNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FRNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/RNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ResumeNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FResumeNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ResumeNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,14,0,15,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ResumedNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FResumedNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ResumedNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/SMNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FSMNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/SMNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/active_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Factive_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/active_element.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/named_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fnamed_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/named_element.dart","_kind":"library"},"hits":[5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/default_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fdefault_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/default_element.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/list_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Flist_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/list_element.dart","_kind":"library"},"hits":[5,0,6,0,7,0,10,0,11,0,16,0,17,0,12,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/privacy_list_item_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fprivacy_list_item_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/privacy_list_item_element.dart","_kind":"library"},"hits":[11,0,14,0,17,0,18,0,20,0,21,0,23,0,24,0,26,0,28,0,45,0,53,0,55,0,56,0,57,0,60,0,61,0,64,0,65,0,66,0,68,0,69,0,80,0,29,0,30,0,31,0,33,0,34,0,36,0,37,0,39,0,40,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0]},{"source":"package:xmpp_stone/src/utils/string_utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Futils%2Fstring_utils.dart","uri":"package:xmpp_stone/src/utils/string_utils.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/stanzas/IqStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FIqStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/IqStanza.dart","_kind":"library"},"hits":[8,0,9,0,10,0,11,0,12,0,13,0]},{"source":"package:xmpp_stone/src/elements/stanzas/PresenceStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FPresenceStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/PresenceStanza.dart","_kind":"library"},"hits":[7,0,8,0,11,0,12,0,13,0,16,0,17,0,18,0,21,0,22,0,23,0,26,0,27,0,28,0,31,0,32,0,33,0,37,0,39,0,40,0,43,0,45,0,47,0,48,0,49,0,50,0,52,0,56,0,57,0,59,0,62,0,63,0,66,0,69,0,71,0,73,0,75,0,82,0,84,0,86,0,88,0,90,0,92,0,94,0,96,0,103,0,105,0,107,0,108,0,109,0,110,0,112,0,116,0,117,0,119,0,120,0,121,0,122,0,124,0]},{"source":"package:xmpp_stone/src/extensions/ping/PingManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fping%2FPingManager.dart","uri":"package:xmpp_stone/src/extensions/ping/PingManager.dart","_kind":"library"},"hits":[11,0,16,0,17,0,18,0,19,0,20,0,23,0,24,0,26,0,27,0,32,0,33,0,34,0,35,0,38,0,42,0,43,0,44,0,45,0,47,0,48,0,49,0,50,0,52,0]},{"source":"package:xmpp_stone/src/extensions/vcard_temp/VCard.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fvcard_temp%2FVCard.dart","uri":"package:xmpp_stone/src/extensions/vcard_temp/VCard.dart","_kind":"library"},"hits":[13,0,15,0,17,0,18,0,19,0,22,0,24,0,26,0,28,0,30,0,32,0,34,0,36,0,37,0,39,0,40,0,42,0,44,0,46,0,48,0,50,0,52,0,54,0,56,0,57,0,58,0,59,0,61,0,75,0,76,0,79,0,82,0,83,0,86,0,89,0,91,0,93,0,95,0,97,0,99,0,101,0,103,0,105,0,107,0,109,0,111,0,117,0,118,0,120,0,122,0,123,0,124,0,125,0,126,0,140,0,133,0,60,0,62,0,65,0,66,0,68,0,77,0,78,0,84,0,85,0,63,0]},{"source":"package:xmpp_stone/src/extensions/vcard_temp/VCardManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fvcard_temp%2FVCardManager.dart","uri":"package:xmpp_stone/src/extensions/vcard_temp/VCardManager.dart","_kind":"library"},"hits":[13,0,27,0,28,0,29,0,15,0,16,0,18,0,19,0,36,0,37,0,39,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,65,0,67,0,68,0,71,0,72,0,73,0,74,0,75,0,76,0,78,0,79,0,80,0,82,0,84,0,86,0,87,0,88,0]},{"source":"package:xmpp_stone/src/features/BindingResourceNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FBindingResourceNegotiator.dart","uri":"package:xmpp_stone/src/features/BindingResourceNegotiator.dart","_kind":"library"},"hits":[19,0,20,0,21,0,23,0,26,0,27,0,30,0,32,0,33,0,34,0,35,0,39,0,40,0,41,0,42,0,44,0,45,0,46,0,47,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,63,0]},{"source":"package:xmpp_stone/src/features/Negotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FNegotiator.dart","uri":"package:xmpp_stone/src/features/Negotiator.dart","_kind":"library"},"hits":[13,0,18,0,19,0,22,0,23,0,24,0,32,0,33,0,36,0,37,0,40,0,42,0]},{"source":"package:xmpp_stone/src/features/SessionInitiationNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FSessionInitiationNegotiator.dart","uri":"package:xmpp_stone/src/features/SessionInitiationNegotiator.dart","_kind":"library"},"hits":[18,0,19,0,21,0,24,0,25,0,28,0,30,0,31,0,32,0,33,0,37,0,38,0,39,0,41,0,42,0,43,0,48,0,49,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0]},{"source":"package:xmpp_stone/src/features/StartTlsNegotatior.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FStartTlsNegotatior.dart","uri":"package:xmpp_stone/src/features/StartTlsNegotatior.dart","_kind":"library"},"hits":[16,0,17,0,18,0,19,0,22,0,24,0,25,0,26,0,27,0,28,0,31,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,43,0,45,0,48,0,53,0,54,0,55,0,46,0,47,0]},{"source":"package:xmpp_stone/src/features/sasl/SaslAuthenticationFeature.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FSaslAuthenticationFeature.dart","uri":"package:xmpp_stone/src/features/sasl/SaslAuthenticationFeature.dart","_kind":"library"},"hits":[17,0,18,0,19,0,20,0,21,0,22,0,26,0,29,0,30,0,33,0,35,0,36,0,37,0,41,0,42,0,44,0,45,0,48,0,49,0,51,0,52,0,53,0,55,0,57,0,59,0,60,0,62,0,66,0,67,0,80,0,81,0,82,0,83,0,105,0,108,0,109,0,110,0,111,0,43,0,68,0,69,0,71,0,72,0,73,0,75,0,84,0,85,0,86,0,88,0,89,0,91,0,92,0,94,0,95,0,97,0,98,0,100,0,101,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/Feature.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FFeature.dart","uri":"package:xmpp_stone/src/features/servicediscovery/Feature.dart","_kind":"library"},"hits":[5,0,6,0,8,0,9,0]},{"source":"package:xmpp_stone/src/features/message_archive/MessageArchiveManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fmessage_archive%2FMessageArchiveManager.dart","uri":"package:xmpp_stone/src/features/message_archive/MessageArchiveManager.dart","_kind":"library"},"hits":[15,0,38,0,17,0,18,0,20,0,21,0,28,0,30,0,32,0,34,0,36,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,50,0,52,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,65,0,66,0,67,0,70,0,71,0,72,0,75,0,77,0,78,0,82,0,84,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,97,0,100,0,103,0,105,0,106,0,110,0,114,0,115,0,116,0,118,0,119,0,120,0,121,0,124,0,125,0,126,0,127,0,130,0,131,0,132,0,133,0,135,0,141,0,142,0]},{"source":"package:xmpp_stone/src/features/privacy_lists/privacy_lists_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fprivacy_lists%2Fprivacy_lists_manager.dart","uri":"package:xmpp_stone/src/features/privacy_lists/privacy_lists_manager.dart","_kind":"library"},"hits":[32,0,34,0,35,0,30,0,38,0,39,0,41,0,42,0,47,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,62,0,63,0,66,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,77,0,78,0,79,0,88,0,89,0,90,0,93,0,94,0,95,0,98,0,100,0,101,0,103,0,104,0,105,0,107,0,125,0,127,0,130,0,131,0,132,0,135,0,137,0,138,0,140,0,141,0,142,0,143,0,145,0,156,0,158,0,161,0,162,0,163,0,166,0,168,0,169,0,171,0,172,0,173,0,174,0,176,0,180,0,182,0,185,0,186,0,187,0,190,0,192,0,193,0,195,0,196,0,197,0,198,0,200,0,204,0,206,0,209,0,210,0,211,0,214,0,216,0,217,0,219,0,220,0,221,0,222,0,224,0,228,0,230,0,233,0,234,0,235,0,238,0,240,0,241,0,243,0,244,0,245,0,246,0,248,0,252,0,254,0,257,0,258,0,259,0,262,0,264,0,265,0,267,0,268,0,269,0,271,0,272,0,277,0,278,0,280,0,284,0,286,0,289,0,290,0,291,0,294,0,296,0,297,0,299,0,300,0,301,0,303,0,305,0,309,0,311,0,314,0,315,0,318,0,319,0,320,0,322,0,108,0,110,0,111,0,146,0,148,0,149,0,273,0,112,0,113,0,114,0,115,0,116,0,117,0,118,0,150,0]},{"source":"package:xmpp_stone/src/features/sasl/AbstractSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FAbstractSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/AbstractSaslHandler.dart","_kind":"library"},"hits":[9,0]},{"source":"package:xmpp_stone/src/features/sasl/AnonymousHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FAnonymousHandler.dart","uri":"package:xmpp_stone/src/features/sasl/AnonymousHandler.dart","_kind":"library"},"hits":[25,0,26,0,29,0,31,0,32,0,33,0,36,0,37,0,38,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,63,0,64,0,65,0,66,0]},{"source":"package:xmpp_stone/src/features/sasl/PlainSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FPlainSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/PlainSaslHandler.dart","_kind":"library"},"hits":[17,0,18,0,21,0,23,0,24,0,25,0,28,0,29,0,30,0,31,0,32,0,33,0,34,0,35,0,39,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,50,0]},{"source":"package:xmpp_stone/src/features/sasl/ScramSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FScramSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/ScramSaslHandler.dart","_kind":"library"},"hits":[38,0,39,0,40,0,41,0,42,0,45,0,47,0,48,0,49,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,62,0,63,0,65,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,78,0,79,0,82,0,83,0,84,0,85,0,86,0,88,0,90,0,91,0,92,0,93,0,94,0,99,0,101,0,102,0,103,0,106,0,107,0,110,0,111,0,114,0,115,0,118,0,119,0,120,0,123,0,125,0,149,0,150,0,153,0,154,0,157,0,158,0,160,0,162,0,163,0,164,0,165,0,166,0,167,0,170,0,171,0,172,0,174,0,177,0,178,0,181,0,182,0,183,0,184,0,185,0,186,0,187,0,188,0,191,0,192,0,193,0,196,0,197,0,198,0,199,0,200,0,201,0,202,0,208,0,209,0,210,0,212,0,214,0,215,0,64,0,126,0,127,0,128,0,130,0,132,0,133,0,137,0,138,0,140,0,141,0,143,0,144,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/Identity.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FIdentity.dart","uri":"package:xmpp_stone/src/features/servicediscovery/Identity.dart","_kind":"library"},"hits":[5,0,6,0,9,0,10,0,13,0,15,0]},{"source":"package:xmpp_stone/src/features/streammanagement/StreamState.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fstreammanagement%2FStreamState.dart","uri":"package:xmpp_stone/src/features/streammanagement/StreamState.dart","_kind":"library"},"hits":[14,0]},{"source":"package:xmpp_stone/src/messages/MessageHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fmessages%2FMessageHandler.dart","uri":"package:xmpp_stone/src/messages/MessageHandler.dart","_kind":"library"},"hits":[8,0,32,0,10,0,11,0,12,0,13,0,16,0,17,0,19,0,20,0,26,0,27,0,34,0,36,0,39,0,41,0,42,0,43,0,44,0,45,0]},{"source":"package:xmpp_stone/src/muc/MucManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fmuc%2FMucManager.dart","uri":"package:xmpp_stone/src/muc/MucManager.dart","_kind":"library"},"hits":[199,0,217,0,16,0,37,0,38,0,18,0,19,0,21,0,22,0,33,0,34,0,35,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,51,0,52,0,55,0,56,0,57,0,58,0,61,0,62,0,63,0,66,0,67,0,68,0,71,0,75,0,76,0,77,0,78,0,82,0,83,0,84,0,85,0,86,0,90,0,91,0,92,0,93,0,94,0,95,0,98,0,101,0,102,0,103,0,104,0,105,0,107,0,108,0,113,0,118,0,119,0,120,0,123,0,129,0,130,0,131,0,132,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,0,147,0,150,0,151,0,154,0,155,0,158,0,159,0,160,0,163,0,166,0,167,0,168,0,169,0,173,0,181,0,224,0,225,0,124,0]},{"source":"package:xmpp_stone/src/parser/IqParser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fparser%2FIqParser.dart","uri":"package:xmpp_stone/src/parser/IqParser.dart","_kind":"library"},"hits":[8,0,9,0,10,0,13,0,15,0,19,0,21,0,23,0,25,0,27,0,29,0]},{"source":"package:xmpp_stone/src/presence/PresenceApi.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fpresence%2FPresenceApi.dart","uri":"package:xmpp_stone/src/presence/PresenceApi.dart","_kind":"library"},"hits":[24,0]},{"source":"package:xmpp_stone/src/presence/PresenceManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fpresence%2FPresenceManager.dart","uri":"package:xmpp_stone/src/presence/PresenceManager.dart","_kind":"library"},"hits":[46,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,25,0,26,0,27,0,30,0,31,0,34,0,35,0,38,0,39,0,42,0,43,0,48,0,49,0,51,0,52,0,57,0,58,0,59,0,60,0,72,0,74,0,75,0,76,0,77,0,78,0,81,0,83,0,84,0,85,0,86,0,87,0,90,0,92,0,93,0,94,0,95,0,96,0,99,0,101,0,102,0,103,0,104,0,107,0,109,0,110,0,111,0,112,0,115,0,117,0,118,0,119,0,120,0,121,0,124,0,126,0,127,0,128,0,129,0,130,0,133,0,134,0,136,0,138,0,139,0,140,0,141,0,142,0,143,0,145,0,146,0,148,0,150,0,152,0,153,0,154,0,155,0,156,0,158,0,159,0,160,0,161,0,162,0,164,0,166,0,172,0,173,0,176,0,177,0,179,0,183,0,184,0,185,0,188,0,190,0,191,0,192,0,193,0,194,0,196,0]},{"source":"package:xmpp_stone/src/roster/Buddy.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Froster%2FBuddy.dart","uri":"package:xmpp_stone/src/roster/Buddy.dart","_kind":"library"},"hits":[15,0,16,0,13,0,19,0,21,0,24,0,26,0,28,0,30,0,32,0]},{"source":"package:xmpp_stone/src/roster/RosterManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Froster%2FRosterManager.dart","uri":"package:xmpp_stone/src/roster/RosterManager.dart","_kind":"library"},"hits":[15,0,102,0,103,0,104,0,105,0,17,0,18,0,20,0,21,0,26,0,27,0,28,0,29,0,39,0,40,0,47,0,48,0,49,0,50,0,51,0,52,0,53,0,54,0,57,0,58,0,61,0,62,0,65,0,66,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,79,0,80,0,81,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,95,0,96,0,98,0,99,0,108,0,109,0,110,0,111,0,112,0,113,0,117,0,118,0,119,0,120,0,121,0,122,0,123,0,124,0,125,0,127,0,130,0,131,0,133,0,139,0,140,0,143,0,144,0,147,0,148,0,149,0,152,0,153,0,154,0,155,0,156,0,178,0,182,0,183,0,184,0,185,0,188,0,189,0,190,0,194,0,195,0,196,0,197,0,198,0,157,0,158,0,159,0,160,0,161,0,162,0,163,0,164,0,165,0,166,0,172,0,173,0,174,0,175,0,167,0,168,0,169,0]}]} \ No newline at end of file diff --git a/vendor/xmpp_stone/coverage/test/message_parsing_test.dart.vm.json b/vendor/xmpp_stone/coverage/test/message_parsing_test.dart.vm.json new file mode 100644 index 0000000..ce213d5 --- /dev/null +++ b/vendor/xmpp_stone/coverage/test/message_parsing_test.dart.vm.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[{"source":"package:xmpp_stone/src/chat/Message.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FMessage.dart","uri":"package:xmpp_stone/src/chat/Message.dart","_kind":"library"},"hits":[31,1,41,1,42,1,43,1,44,1,45,1,46,1,47,1,48,1,49,1,29,2,52,0,54,1,56,2,59,5,61,1,63,1,65,1,69,1,70,2,73,1,75,1,78,1,79,2,80,3,84,1,85,2,86,3,89,2,90,2,91,1,92,1,93,1,94,1,97,1,96,0,107,0,112,1,113,1,114,4,117,2,118,2,119,1,121,1,124,1,125,2,126,3,130,1,131,2,132,3,135,2,136,2,137,1,139,3,140,1,141,1,144,1,145,1,143,0,158,0,163,1,164,1,167,1,169,1,170,1,173,0,176,0,179,0,182,0,190,1,191,3,195,0,201,0,203,0,205,0,207,0,209,0,211,0,217,1,218,1,219,4,220,1,221,1,222,1,223,1,226,1,227,1,229,2,231,1,234,0,240,2,242,0,244,2,245,2,247,2,249,2,251,0,57,5,192,2,71,3]},{"source":"package:xmpp_stone/src/data/Jid.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2FJid.dart","uri":"package:xmpp_stone/src/data/Jid.dart","_kind":"library"},"hits":[67,0,8,1,9,1,10,1,11,1,14,0,16,0,19,2,21,0,23,2,25,1,26,1,32,2,35,1,36,3,27,0,28,0,29,0,30,0,33,0,41,1,42,5,43,0,46,0,47,0,50,1,51,1,52,1,54,4,56,0,60,0,62,0]},{"source":"package:xmpp_stone/src/elements/XmppAttribute.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2FXmppAttribute.dart","uri":"package:xmpp_stone/src/elements/XmppAttribute.dart","_kind":"library"},"hits":[5,1]},{"source":"package:xmpp_stone/src/elements/XmppElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2FXmppElement.dart","uri":"package:xmpp_stone/src/elements/XmppElement.dart","_kind":"library"},"hits":[10,2,13,1,14,5,17,1,18,2,22,2,20,0,25,1,26,2,29,1,30,5,33,0,34,0,37,0,38,0,39,0,40,0,45,0,48,0,49,0,51,0,55,0,56,0,59,2,41,0,42,0,46,0]},{"source":"package:xmpp_stone/src/elements/stanzas/MessageStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FMessageStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/MessageStanza.dart","_kind":"library"},"hits":[9,1,10,1,11,1,12,1,13,1,14,5,17,2,18,6,19,1,21,1,22,1,23,1,24,1,25,1,28,0,29,0,30,0,32,0,33,0,34,0,35,0,36,0,39,2,40,4,41,0,43,0,44,0,45,0,46,0,47,0]},{"source":"package:xmpp_stone/src/Connection.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2FConnection.dart","uri":"package:xmpp_stone/src/Connection.dart","_kind":"library"},"hits":[49,0,137,0,138,0,139,0,140,0,141,0,142,0,143,0,55,0,56,0,57,0,59,0,65,0,66,0,68,0,69,0,74,0,75,0,97,0,98,0,101,0,102,0,105,0,106,0,109,0,110,0,113,0,114,0,117,0,121,0,122,0,129,0,130,0,146,0,147,0,148,0,153,0,157,0,158,0,159,0,160,0,161,0,164,0,165,0,166,0,171,0,175,0,176,0,177,0,178,0,182,0,183,0,184,0,186,0,187,0,189,0,190,0,194,0,195,0,196,0,198,0,199,0,200,0,201,0,202,0,204,0,205,0,207,0,208,0,211,0,214,0,215,0,218,0,219,0,220,0,222,0,236,0,237,0,238,0,242,0,243,0,244,0,246,0,247,0,248,0,249,0,251,0,252,0,253,0,254,0,255,0,258,0,269,0,270,0,271,0,272,0,273,0,274,0,275,0,276,0,277,0,278,0,279,0,280,0,283,0,284,0,285,0,288,0,289,0,290,0,293,0,294,0,295,0,298,0,299,0,300,0,305,0,307,0,308,0,309,0,311,0,313,0,314,0,319,0,322,0,323,0,324,0,326,0,327,0,328,0,332,0,333,0,334,0,335,0,337,0,338,0,339,0,340,0,342,0,343,0,344,0,345,0,349,0,350,0,351,0,352,0,356,0,357,0,358,0,360,0,364,0,365,0,366,0,367,0,368,0,371,0,372,0,373,0,374,0,378,0,379,0,380,0,383,0,384,0,385,0,388,0,389,0,390,0,391,0,392,0,395,0,396,0,399,0,400,0,401,0,402,0,403,0,404,0,405,0,409,0,413,0,414,0,415,0,417,0,418,0,419,0,435,0,436,0,439,0,440,0,443,0,444,0,450,0,451,0,455,0,456,0,457,0,461,0,462,0,463,0,466,0,467,0,470,0,474,0,475,0,476,0,477,0,478,0,479,0,482,0,485,0,486,0,487,0,490,0,491,0,492,0,495,0,496,0,499,0,500,0,501,0,502,0,503,0,504,0,506,0,510,0,511,0,512,0,515,0,516,0,517,0,224,0,225,0,226,0,227,0,228,0,229,0,231,0,232,0,346,0,422,0,423,0,424,0,425,0,426,0,427,0,430,0,431,0,445,0,446,0,428,0,429,0]},{"source":"package:xmpp_stone/src/ReconnectionManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2FReconnectionManager.dart","uri":"package:xmpp_stone/src/ReconnectionManager.dart","_kind":"library"},"hits":[17,0,18,0,19,0,20,0,21,0,22,0,23,0,26,0,27,0,28,0,29,0,30,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,43,0,44,0,45,0,47,0,48,0,49,0,50,0,51,0,53,0,57,0,58,0,59,0]},{"source":"package:xmpp_stone/src/elements/nonzas/Nonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/Nonza.dart","_kind":"library"},"hits":[11,0,13,0,14,0,15,0,18,0,20,0,21,0,22,0,25,0,26,0,27,0,29,0,31,0,32,0,34,0,36,0,37,0,39,0,41,0,40,0,42,0,43,0,44,0,45,0]},{"source":"package:xmpp_stone/src/features/ConnectionNegotatiorManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FConnectionNegotatiorManager.dart","uri":"package:xmpp_stone/src/features/ConnectionNegotatiorManager.dart","_kind":"library"},"hits":[157,0,33,0,35,0,36,0,37,0,38,0,41,0,42,0,44,0,45,0,52,0,53,0,54,0,56,0,59,0,60,0,61,0,62,0,63,0,65,0,66,0,70,0,71,0,73,0,74,0,76,0,77,0,78,0,82,0,83,0,88,0,89,0,93,0,94,0,95,0,96,0,97,0,99,0,100,0,101,0,102,0,104,0,105,0,106,0,107,0,108,0,111,0,112,0,115,0,116,0,117,0,118,0,119,0,120,0,121,0,125,0,126,0,127,0,135,0,139,0,140,0,141,0,142,0,46,0,47,0,48,0,49,0,128,0,129,0,130,0,131,0,132,0,143,0,144,0,145,0,146,0,147,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/CarbonsNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FCarbonsNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/CarbonsNegotiator.dart","_kind":"library"},"hits":[12,0,35,0,36,0,14,0,15,0,17,0,18,0,23,0,24,0,25,0,39,0,41,0,44,0,47,0,49,0,50,0,51,0,52,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,63,0,64,0,67,0,68,0,69,0,70,0,71,0,42,0,43,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/MAMNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FMAMNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/MAMNegotiator.dart","_kind":"library"},"hits":[17,0,45,0,46,0,19,0,20,0,22,0,23,0,28,0,29,0,30,0,49,0,50,0,51,0,53,0,54,0,55,0,57,0,58,0,60,0,63,0,67,0,70,0,72,0,73,0,74,0,75,0,76,0,80,0,81,0,82,0,83,0,84,0,85,0,86,0,89,0,90,0,91,0,93,0,118,0,119,0,123,0,124,0,64,0,65,0,66,0,94,0,95,0,96,0,97,0,99,0,100,0,102,0,103,0,105,0,106,0,108,0,109,0,111,0,112,0,125,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FServiceDiscoveryNegotiator.dart","uri":"package:xmpp_stone/src/features/servicediscovery/ServiceDiscoveryNegotiator.dart","_kind":"library"},"hits":[19,0,41,0,42,0,21,0,22,0,24,0,25,0,30,0,31,0,32,0,54,0,55,0,58,0,59,0,60,0,62,0,63,0,64,0,65,0,70,0,72,0,75,0,77,0,78,0,79,0,80,0,81,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,97,0,98,0,99,0,100,0,101,0,103,0,111,0,112,0,114,0,117,0,118,0,119,0,122,0,123,0,124,0,128,0,129,0,132,0,133,0,134,0,135,0,136,0,139,0,142,0,143,0,145,0,146,0,147,0,148,0,149,0,155,0,156,0,161,0,162,0,43,0,104,0,105,0,106,0,107,0,137,0,138,0,150,0,151,0,152,0,153,0]},{"source":"package:xmpp_stone/src/features/streammanagement/StreamManagmentModule.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fstreammanagement%2FStreamManagmentModule.dart","uri":"package:xmpp_stone/src/features/streammanagement/StreamManagmentModule.dart","_kind":"library"},"hits":[21,0,83,0,84,0,85,0,86,0,87,0,88,0,23,0,24,0,26,0,27,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,55,0,56,0,59,0,60,0,61,0,65,0,66,0,67,0,68,0,69,0,71,0,72,0,73,0,75,0,76,0,78,0,103,0,105,0,106,0,110,0,112,0,113,0,114,0,115,0,116,0,117,0,118,0,120,0,125,0,127,0,128,0,129,0,130,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,0,142,0,143,0,144,0,145,0,147,0,149,0,152,0,153,0,154,0,155,0,156,0,161,0,162,0,163,0,166,0,167,0,170,0,171,0,172,0,173,0,174,0,175,0,177,0,178,0,179,0,181,0,182,0,183,0,184,0,187,0,188,0,190,0,191,0,192,0,194,0,195,0,198,0,199,0,201,0,202,0,204,0,205,0,206,0,207,0,208,0,212,0,213,0,214,0,215,0,216,0,219,0,221,0,222,0,223,0,89,0,90,0,92,0,93,0,96,0,97,0]},{"source":"package:xmpp_stone/src/parser/StanzaParser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fparser%2FStanzaParser.dart","uri":"package:xmpp_stone/src/parser/StanzaParser.dart","_kind":"library"},"hits":[20,0,22,0,24,0,27,0,28,0,29,0,30,0,31,0,32,0,34,0,36,0,37,0,39,0,41,0,42,0,44,0,48,0,54,0,55,0,58,0,61,0,64,0,67,0,70,0,73,0,78,0,83,0,85,0,86,0,90,0,92,0,93,0,94,0,95,0,96,0,97,0,98,0,99,0,100,0,101,0,103,0,105,0,106,0,110,0,45,0,46,0,49,0,107,0,108,0,111,0,112,0,113,0,114,0]},{"source":"package:xmpp_stone/src/connection/XmppWebsocketIo.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fconnection%2FXmppWebsocketIo.dart","uri":"package:xmpp_stone/src/connection/XmppWebsocketIo.dart","_kind":"library"},"hits":[25,0,27,0,36,0,37,0,38,0,39,0,41,0,42,0,47,0,48,0,51,0,52,0,53,0,55,0,56,0,63,0,65,0,68,0,71,0,73,0,74,0,76,0,80,0,82,0,83,0,85,0,89,0,92,0,93,0,99,0,106,0,112,0,113,0,115,0,116,0,117,0,125,0,127,0,10,0,11,0,14,0,57,0,119,0]},{"source":"package:xmpp_stone/src/logger/Log.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Flogger%2FLog.dart","uri":"package:xmpp_stone/src/logger/Log.dart","_kind":"library"},"hits":[4,2,8,0,9,0,10,0,14,0,15,0,16,0,20,0,21,0,22,0,26,1,27,4,28,2,32,0,33,0,34,0,38,0,40,0,41,0,45,0,47,0,48,0]},{"source":"package:xmpp_stone/src/account/XmppAccountSettings.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Faccount%2FXmppAccountSettings.dart","uri":"package:xmpp_stone/src/account/XmppAccountSettings.dart","_kind":"library"},"hits":[23,0,40,0,43,0,44,0,46,0,47,0,48,0]},{"source":"package:xmpp_stone/src/chat/Chat.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FChat.dart","uri":"package:xmpp_stone/src/chat/Chat.dart","_kind":"library"},"hits":[40,0,17,0,18,0,20,0,21,0,24,0,25,0,35,0,36,0,37,0,38,0,42,0,43,0,44,0,45,0,46,0,49,0,50,0,51,0,56,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,79,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0]},{"source":"package:xmpp_stone/src/elements/stanzas/AbstractStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FAbstractStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/AbstractStanza.dart","_kind":"library"},"hits":[12,2,14,1,15,1,16,4,19,2,21,1,22,1,23,2,24,2,27,2,29,1,30,1,31,3,34,0,37,0,40,0,38,0]},{"source":"package:xmpp_stone/src/chat/ChatManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fchat%2FChatManager.dart","uri":"package:xmpp_stone/src/chat/ChatManager.dart","_kind":"library"},"hits":[6,0,20,0,21,0,22,0,23,0,24,0,8,0,9,0,11,0,12,0,39,0,43,0,44,0,48,0,49,0,52,0,53,0,55,0,56,0,57,0,25,0,28,0,29,0,30,0,31,0,32,0]},{"source":"package:xmpp_stone/src/connection/XmppWebsocketApi.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fconnection%2FXmppWebsocketApi.dart","uri":"package:xmpp_stone/src/connection/XmppWebsocketApi.dart","_kind":"library"},"hits":[4,0,5,0,8,0,9,0]},{"source":"package:xmpp_stone/src/data/privacy_list.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_list.dart","uri":"package:xmpp_stone/src/data/privacy_list.dart","_kind":"library"},"hits":[7,0]},{"source":"package:xmpp_stone/src/data/privacy_list_item.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_list_item.dart","uri":"package:xmpp_stone/src/data/privacy_list_item.dart","_kind":"library"},"hits":[8,0,14,0]},{"source":"package:xmpp_stone/src/data/privacy_lists.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fdata%2Fprivacy_lists.dart","uri":"package:xmpp_stone/src/data/privacy_lists.dart","_kind":"library"},"hits":[6,0,8,0,10,0,11,0,12,0,13,0]},{"source":"package:xmpp_stone/src/elements/forms/FieldElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FFieldElement.dart","uri":"package:xmpp_stone/src/elements/forms/FieldElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,13,0,16,0,19,0,20,0,21,0,22,0,26,0,28,0,30,0]},{"source":"package:xmpp_stone/src/elements/forms/QueryElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FQueryElement.dart","uri":"package:xmpp_stone/src/elements/forms/QueryElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,14,0,15,0,18,0,19,0,22,0]},{"source":"package:xmpp_stone/src/elements/forms/XElement.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fforms%2FXElement.dart","uri":"package:xmpp_stone/src/elements/forms/XElement.dart","_kind":"library"},"hits":[6,0,7,0,10,0,11,0,12,0,15,0,16,0,17,0,20,0,21,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ANonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FANonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ANonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,14,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/EnableNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FEnableNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/EnableNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,15,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/EnabledNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FEnabledNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/EnabledNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/nonzas/FailedNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FFailedNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/FailedNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/nonzas/RNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FRNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/RNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ResumeNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FResumeNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ResumeNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,14,0,15,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/ResumedNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FResumedNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/ResumedNonza.dart","_kind":"library"},"hits":[11,0,12,0,13,0,8,0,9,0]},{"source":"package:xmpp_stone/src/elements/nonzas/SMNonza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fnonzas%2FSMNonza.dart","uri":"package:xmpp_stone/src/elements/nonzas/SMNonza.dart","_kind":"library"},"hits":[10,0,11,0,7,0,8,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/active_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Factive_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/active_element.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/named_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fnamed_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/named_element.dart","_kind":"library"},"hits":[5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/default_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fdefault_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/default_element.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/list_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Flist_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/list_element.dart","_kind":"library"},"hits":[5,0,6,0,7,0,10,0,11,0,16,0,17,0,12,0]},{"source":"package:xmpp_stone/src/elements/privacy_lists/privacy_list_item_element.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fprivacy_lists%2Fprivacy_list_item_element.dart","uri":"package:xmpp_stone/src/elements/privacy_lists/privacy_list_item_element.dart","_kind":"library"},"hits":[11,0,14,0,17,0,18,0,20,0,21,0,23,0,24,0,26,0,28,0,45,0,53,0,55,0,56,0,57,0,60,0,61,0,64,0,65,0,66,0,68,0,69,0,80,0,29,0,30,0,31,0,33,0,34,0,36,0,37,0,39,0,40,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0]},{"source":"package:xmpp_stone/src/utils/string_utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Futils%2Fstring_utils.dart","uri":"package:xmpp_stone/src/utils/string_utils.dart","_kind":"library"},"hits":[4,0,5,0,6,0]},{"source":"package:xmpp_stone/src/elements/stanzas/IqStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FIqStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/IqStanza.dart","_kind":"library"},"hits":[8,0,9,0,10,0,11,0,12,0,13,0]},{"source":"package:xmpp_stone/src/elements/stanzas/PresenceStanza.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Felements%2Fstanzas%2FPresenceStanza.dart","uri":"package:xmpp_stone/src/elements/stanzas/PresenceStanza.dart","_kind":"library"},"hits":[7,0,8,0,11,0,12,0,13,0,16,0,17,0,18,0,21,0,22,0,23,0,26,0,27,0,28,0,31,0,32,0,33,0,37,0,39,0,40,0,43,0,45,0,47,0,48,0,49,0,50,0,52,0,56,0,57,0,59,0,62,0,63,0,66,0,69,0,71,0,73,0,75,0,82,0,84,0,86,0,88,0,90,0,92,0,94,0,96,0,103,0,105,0,107,0,108,0,109,0,110,0,112,0,116,0,117,0,119,0,120,0,121,0,122,0,124,0]},{"source":"package:xmpp_stone/src/extensions/ping/PingManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fping%2FPingManager.dart","uri":"package:xmpp_stone/src/extensions/ping/PingManager.dart","_kind":"library"},"hits":[11,0,16,0,17,0,18,0,19,0,20,0,23,0,24,0,26,0,27,0,32,0,33,0,34,0,35,0,38,0,42,0,43,0,44,0,45,0,47,0,48,0,49,0,50,0,52,0]},{"source":"package:xmpp_stone/src/extensions/vcard_temp/VCard.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fvcard_temp%2FVCard.dart","uri":"package:xmpp_stone/src/extensions/vcard_temp/VCard.dart","_kind":"library"},"hits":[13,0,15,0,17,0,18,0,19,0,22,0,24,0,26,0,28,0,30,0,32,0,34,0,36,0,37,0,39,0,40,0,42,0,44,0,46,0,48,0,50,0,52,0,54,0,56,0,57,0,58,0,59,0,61,0,75,0,76,0,79,0,82,0,83,0,86,0,89,0,91,0,93,0,95,0,97,0,99,0,101,0,103,0,105,0,107,0,109,0,111,0,117,0,118,0,120,0,122,0,123,0,124,0,125,0,126,0,140,0,133,0,60,0,62,0,65,0,66,0,68,0,77,0,78,0,84,0,85,0,63,0]},{"source":"package:xmpp_stone/src/extensions/vcard_temp/VCardManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fextensions%2Fvcard_temp%2FVCardManager.dart","uri":"package:xmpp_stone/src/extensions/vcard_temp/VCardManager.dart","_kind":"library"},"hits":[13,0,27,0,28,0,29,0,15,0,16,0,18,0,19,0,36,0,37,0,39,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,65,0,67,0,68,0,71,0,72,0,73,0,74,0,75,0,76,0,78,0,79,0,80,0,82,0,84,0,86,0,87,0,88,0]},{"source":"package:xmpp_stone/src/features/BindingResourceNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FBindingResourceNegotiator.dart","uri":"package:xmpp_stone/src/features/BindingResourceNegotiator.dart","_kind":"library"},"hits":[19,0,20,0,21,0,23,0,26,0,27,0,30,0,32,0,33,0,34,0,35,0,39,0,40,0,41,0,42,0,44,0,45,0,46,0,47,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,63,0]},{"source":"package:xmpp_stone/src/features/Negotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FNegotiator.dart","uri":"package:xmpp_stone/src/features/Negotiator.dart","_kind":"library"},"hits":[13,0,18,0,19,0,22,0,23,0,24,0,32,0,33,0,36,0,37,0,40,0,42,0]},{"source":"package:xmpp_stone/src/features/SessionInitiationNegotiator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FSessionInitiationNegotiator.dart","uri":"package:xmpp_stone/src/features/SessionInitiationNegotiator.dart","_kind":"library"},"hits":[18,0,19,0,21,0,24,0,25,0,28,0,30,0,31,0,32,0,33,0,37,0,38,0,39,0,41,0,42,0,43,0,48,0,49,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0]},{"source":"package:xmpp_stone/src/features/StartTlsNegotatior.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2FStartTlsNegotatior.dart","uri":"package:xmpp_stone/src/features/StartTlsNegotatior.dart","_kind":"library"},"hits":[16,0,17,0,18,0,19,0,22,0,24,0,25,0,26,0,27,0,28,0,31,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,43,0,45,0,48,0,53,0,54,0,55,0,46,0,47,0]},{"source":"package:xmpp_stone/src/features/sasl/SaslAuthenticationFeature.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FSaslAuthenticationFeature.dart","uri":"package:xmpp_stone/src/features/sasl/SaslAuthenticationFeature.dart","_kind":"library"},"hits":[17,0,18,0,19,0,20,0,21,0,22,0,26,0,29,0,30,0,33,0,35,0,36,0,37,0,41,0,42,0,44,0,45,0,48,0,49,0,51,0,52,0,53,0,55,0,57,0,59,0,60,0,62,0,66,0,67,0,80,0,81,0,82,0,83,0,105,0,108,0,109,0,110,0,111,0,43,0,68,0,69,0,71,0,72,0,73,0,75,0,84,0,85,0,86,0,88,0,89,0,91,0,92,0,94,0,95,0,97,0,98,0,100,0,101,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/Feature.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FFeature.dart","uri":"package:xmpp_stone/src/features/servicediscovery/Feature.dart","_kind":"library"},"hits":[5,0,6,0,8,0,9,0]},{"source":"package:xmpp_stone/src/features/message_archive/MessageArchiveManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fmessage_archive%2FMessageArchiveManager.dart","uri":"package:xmpp_stone/src/features/message_archive/MessageArchiveManager.dart","_kind":"library"},"hits":[15,0,38,0,17,0,18,0,20,0,21,0,28,0,30,0,32,0,34,0,36,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,50,0,52,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,61,0,62,0,65,0,66,0,67,0,70,0,71,0,72,0,75,0,77,0,78,0,82,0,84,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,97,0,100,0,103,0,105,0,106,0,110,0,114,0,115,0,116,0,118,0,119,0,120,0,121,0,124,0,125,0,126,0,127,0,130,0,131,0,132,0,133,0,135,0,141,0,142,0]},{"source":"package:xmpp_stone/src/features/privacy_lists/privacy_lists_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fprivacy_lists%2Fprivacy_lists_manager.dart","uri":"package:xmpp_stone/src/features/privacy_lists/privacy_lists_manager.dart","_kind":"library"},"hits":[32,0,34,0,35,0,30,0,38,0,39,0,41,0,42,0,47,0,50,0,51,0,53,0,54,0,55,0,56,0,57,0,58,0,59,0,60,0,62,0,63,0,66,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,77,0,78,0,79,0,88,0,89,0,90,0,93,0,94,0,95,0,98,0,100,0,101,0,103,0,104,0,105,0,107,0,125,0,127,0,130,0,131,0,132,0,135,0,137,0,138,0,140,0,141,0,142,0,143,0,145,0,156,0,158,0,161,0,162,0,163,0,166,0,168,0,169,0,171,0,172,0,173,0,174,0,176,0,180,0,182,0,185,0,186,0,187,0,190,0,192,0,193,0,195,0,196,0,197,0,198,0,200,0,204,0,206,0,209,0,210,0,211,0,214,0,216,0,217,0,219,0,220,0,221,0,222,0,224,0,228,0,230,0,233,0,234,0,235,0,238,0,240,0,241,0,243,0,244,0,245,0,246,0,248,0,252,0,254,0,257,0,258,0,259,0,262,0,264,0,265,0,267,0,268,0,269,0,271,0,272,0,277,0,278,0,280,0,284,0,286,0,289,0,290,0,291,0,294,0,296,0,297,0,299,0,300,0,301,0,303,0,305,0,309,0,311,0,314,0,315,0,318,0,319,0,320,0,322,0,108,0,110,0,111,0,146,0,148,0,149,0,273,0,112,0,113,0,114,0,115,0,116,0,117,0,118,0,150,0]},{"source":"package:xmpp_stone/src/features/sasl/AbstractSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FAbstractSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/AbstractSaslHandler.dart","_kind":"library"},"hits":[9,0]},{"source":"package:xmpp_stone/src/features/sasl/AnonymousHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FAnonymousHandler.dart","uri":"package:xmpp_stone/src/features/sasl/AnonymousHandler.dart","_kind":"library"},"hits":[25,0,26,0,29,0,31,0,32,0,33,0,36,0,37,0,38,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,63,0,64,0,65,0,66,0]},{"source":"package:xmpp_stone/src/features/sasl/PlainSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FPlainSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/PlainSaslHandler.dart","_kind":"library"},"hits":[17,0,18,0,21,0,23,0,24,0,25,0,28,0,29,0,30,0,31,0,32,0,33,0,34,0,35,0,39,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,50,0]},{"source":"package:xmpp_stone/src/features/sasl/ScramSaslHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fsasl%2FScramSaslHandler.dart","uri":"package:xmpp_stone/src/features/sasl/ScramSaslHandler.dart","_kind":"library"},"hits":[38,0,39,0,40,0,41,0,42,0,45,0,47,0,48,0,49,0,52,0,53,0,54,0,55,0,56,0,57,0,58,0,62,0,63,0,65,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,78,0,79,0,82,0,83,0,84,0,85,0,86,0,88,0,90,0,91,0,92,0,93,0,94,0,99,0,101,0,102,0,103,0,106,0,107,0,110,0,111,0,114,0,115,0,118,0,119,0,120,0,123,0,125,0,149,0,150,0,153,0,154,0,157,0,158,0,160,0,162,0,163,0,164,0,165,0,166,0,167,0,170,0,171,0,172,0,174,0,177,0,178,0,181,0,182,0,183,0,184,0,185,0,186,0,187,0,188,0,191,0,192,0,193,0,196,0,197,0,198,0,199,0,200,0,201,0,202,0,208,0,209,0,210,0,212,0,214,0,215,0,64,0,126,0,127,0,128,0,130,0,132,0,133,0,137,0,138,0,140,0,141,0,143,0,144,0]},{"source":"package:xmpp_stone/src/features/servicediscovery/Identity.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fservicediscovery%2FIdentity.dart","uri":"package:xmpp_stone/src/features/servicediscovery/Identity.dart","_kind":"library"},"hits":[5,0,6,0,9,0,10,0,13,0,15,0]},{"source":"package:xmpp_stone/src/features/streammanagement/StreamState.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Ffeatures%2Fstreammanagement%2FStreamState.dart","uri":"package:xmpp_stone/src/features/streammanagement/StreamState.dart","_kind":"library"},"hits":[14,0]},{"source":"package:xmpp_stone/src/messages/MessageHandler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fmessages%2FMessageHandler.dart","uri":"package:xmpp_stone/src/messages/MessageHandler.dart","_kind":"library"},"hits":[8,0,32,0,10,0,11,0,12,0,13,0,16,0,17,0,19,0,20,0,26,0,27,0,34,0,36,0,39,0,41,0,42,0,43,0,44,0,45,0]},{"source":"package:xmpp_stone/src/muc/MucManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fmuc%2FMucManager.dart","uri":"package:xmpp_stone/src/muc/MucManager.dart","_kind":"library"},"hits":[199,0,217,0,16,0,37,0,38,0,18,0,19,0,21,0,22,0,33,0,34,0,35,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,51,0,52,0,55,0,56,0,57,0,58,0,61,0,62,0,63,0,66,0,67,0,68,0,71,0,75,0,76,0,77,0,78,0,82,0,83,0,84,0,85,0,86,0,90,0,91,0,92,0,93,0,94,0,95,0,98,0,101,0,102,0,103,0,104,0,105,0,107,0,108,0,113,0,118,0,119,0,120,0,123,0,129,0,130,0,131,0,132,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,0,147,0,150,0,151,0,154,0,155,0,158,0,159,0,160,0,163,0,166,0,167,0,168,0,169,0,173,0,181,0,224,0,225,0,124,0]},{"source":"package:xmpp_stone/src/parser/IqParser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fparser%2FIqParser.dart","uri":"package:xmpp_stone/src/parser/IqParser.dart","_kind":"library"},"hits":[8,0,9,0,10,0,13,0,15,0,19,0,21,0,23,0,25,0,27,0,29,0]},{"source":"package:xmpp_stone/src/presence/PresenceApi.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fpresence%2FPresenceApi.dart","uri":"package:xmpp_stone/src/presence/PresenceApi.dart","_kind":"library"},"hits":[24,0]},{"source":"package:xmpp_stone/src/presence/PresenceManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Fpresence%2FPresenceManager.dart","uri":"package:xmpp_stone/src/presence/PresenceManager.dart","_kind":"library"},"hits":[46,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,25,0,26,0,27,0,30,0,31,0,34,0,35,0,38,0,39,0,42,0,43,0,48,0,49,0,51,0,52,0,57,0,58,0,59,0,60,0,72,0,74,0,75,0,76,0,77,0,78,0,81,0,83,0,84,0,85,0,86,0,87,0,90,0,92,0,93,0,94,0,95,0,96,0,99,0,101,0,102,0,103,0,104,0,107,0,109,0,110,0,111,0,112,0,115,0,117,0,118,0,119,0,120,0,121,0,124,0,126,0,127,0,128,0,129,0,130,0,133,0,134,0,136,0,138,0,139,0,140,0,141,0,142,0,143,0,145,0,146,0,148,0,150,0,152,0,153,0,154,0,155,0,156,0,158,0,159,0,160,0,161,0,162,0,164,0,166,0,172,0,173,0,176,0,177,0,179,0,183,0,184,0,185,0,188,0,190,0,191,0,192,0,193,0,194,0,196,0]},{"source":"package:xmpp_stone/src/roster/Buddy.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Froster%2FBuddy.dart","uri":"package:xmpp_stone/src/roster/Buddy.dart","_kind":"library"},"hits":[15,0,16,0,13,0,19,0,21,0,24,0,26,0,28,0,30,0,32,0]},{"source":"package:xmpp_stone/src/roster/RosterManager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Axmpp_stone%2Fsrc%2Froster%2FRosterManager.dart","uri":"package:xmpp_stone/src/roster/RosterManager.dart","_kind":"library"},"hits":[15,0,102,0,103,0,104,0,105,0,17,0,18,0,20,0,21,0,26,0,27,0,28,0,29,0,39,0,40,0,47,0,48,0,49,0,50,0,51,0,52,0,53,0,54,0,57,0,58,0,61,0,62,0,65,0,66,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,79,0,80,0,81,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,94,0,95,0,96,0,98,0,99,0,108,0,109,0,110,0,111,0,112,0,113,0,117,0,118,0,119,0,120,0,121,0,122,0,123,0,124,0,125,0,127,0,130,0,131,0,133,0,139,0,140,0,143,0,144,0,147,0,148,0,149,0,152,0,153,0,154,0,155,0,156,0,178,0,182,0,183,0,184,0,185,0,188,0,189,0,190,0,194,0,195,0,196,0,197,0,198,0,157,0,158,0,159,0,160,0,161,0,162,0,163,0,164,0,165,0,166,0,172,0,173,0,174,0,175,0,167,0,168,0,169,0]}]} \ No newline at end of file diff --git a/vendor/xmpp_stone/coverage/test/xmpp_dart_test.dart.vm.json b/vendor/xmpp_stone/coverage/test/xmpp_dart_test.dart.vm.json new file mode 100644 index 0000000..5a8e6bc --- /dev/null +++ b/vendor/xmpp_stone/coverage/test/xmpp_dart_test.dart.vm.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[]} \ No newline at end of file diff --git a/vendor/xmpp_stone/lib/src/Connection.dart b/vendor/xmpp_stone/lib/src/Connection.dart index 00b0a1d..d7eaf71 100644 --- a/vendor/xmpp_stone/lib/src/Connection.dart +++ b/vendor/xmpp_stone/lib/src/Connection.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; - import 'package:collection/collection.dart' show IterableExtension; import 'package:synchronized/synchronized.dart'; import 'package:universal_io/io.dart'; @@ -77,10 +76,13 @@ class Connection { String? _serverName; - static Connection getInstance(XmppAccountSettings account) { + static Connection getInstance( + XmppAccountSettings account, { + XmppSocketFactory? socketFactory, + }) { var connection = instances[account.fullJid.userAtDomain]; if (connection == null) { - connection = Connection(account); + connection = Connection(account, socketFactory: socketFactory); instances[account.fullJid.userAtDomain] = connection; } return connection; @@ -202,10 +204,13 @@ class Connection { bool get iapAdvertisedInCurrentStream => _iapAdvertisedInCurrentStream; bool get sasl2PipelinedAuthInFlight => _sasl2PipelinedAuthInFlight; + bool get isQuic => _socket?.isQuic ?? false; xmppSocket.XmppWebSocket? _socket; StreamSubscription? _socketSubscription; + xmppSocket.XmppWebSocket? get socket => _socket; + // for testing purpose set socket(xmppSocket.XmppWebSocket? value) { _socket = value; @@ -238,26 +243,209 @@ class Connection { String restOfResponse = ''; - String extractWholeChild(String response) { - return response; + /// Extracts all complete top-level XML elements from [input], returning them + /// wrapped in `` and leaving any trailing + /// incomplete fragment in [remainder]. + /// + /// This replaces the old "parse whole buffer, fail if incomplete" approach + /// which would hold back all complete stanzas whenever a partial fragment + /// was present at the end of the buffer — causing stanza starvation under + /// high latency / burst conditions (e.g. a large MUC presence flood). + /// + /// The algorithm: + /// 1. Strip any `` prolog. + /// 2. Walk the string character-by-character tracking element depth. + /// 3. Each time depth returns to 0 after opening at least one element, + /// we have a complete top-level element — record its end offset. + /// 4. Emit all complete elements wrapped in ``, keep the rest. + /// + /// Special cases: + /// • `` — signals connection close; caller handles it. + /// • `` opener (depth never closes) — emitted as-is with + /// a synthetic `` appended so the XML parser accepts it. + static _ExtractResult _extractCompleteElements( + String input, + ) { + // Strip XML prolog(s) for depth-counting purposes but keep them in the + // output so the XML parser in handleResponse can strip them too. + final stripped = input.replaceAll(RegExp(r'<\?(xml[^?]*)\?>'), ''); + + int depth = 0; + bool inTag = false; + bool inClosingTag = false; + bool inSelfClosing = false; + bool inString = false; + String stringChar = ''; + bool inComment = false; + int i = 0; + final len = stripped.length; + // Offsets (in `stripped`) where complete top-level elements end. + final List completeEnds = []; + // Whether we have seen a ` + if (!inString && !inComment && i + 3 < len && + ch == '<' && stripped[i + 1] == '!' && + stripped[i + 2] == '-' && stripped[i + 3] == '-') { + final end = stripped.indexOf('-->', i + 4); + if (end < 0) break; // incomplete comment — stop + i = end + 3; + continue; + } + + if (inComment) { + // (handled above) + i++; + continue; + } + + if (inString) { + if (ch == stringChar) inString = false; + i++; + continue; + } + + if (ch == '"' || ch == "'") { + if (inTag) { + inString = true; + stringChar = ch; + } + i++; + continue; + } + + if (ch == '<') { + inClosingTag = i + 1 < len && stripped[i + 1] == '/'; + inSelfClosing = false; + if (!inClosingTag) { + // Check for stream:stream opener — it never gets a closing tag in + // the normal flow, so we handle it specially. + if (depth == 0) { + final tagEnd = stripped.indexOf('>', i); + if (tagEnd >= 0) { + final tagContent = stripped.substring(i + 1, tagEnd); + if (tagContent.trimLeft().startsWith('stream:stream')) { + // Emit the opener with a synthetic close so the XML parser + // accepts it, then continue scanning after it. + hasUnclosedStreamOpener = true; + completeEnds.add(tagEnd + 1); + i = tagEnd + 1; + continue; + } + } + } + // Opening tag: increment depth now so self-closing can decrement it. + depth++; + } + inTag = true; + i++; + continue; + } + + if (ch == '/' && inTag) { + // Could be self-closing `/>` + if (i + 1 < len && stripped[i + 1] == '>') { + inSelfClosing = true; + } + i++; + continue; + } + + if (ch == '>') { + if (inTag) { + inTag = false; + if (inSelfClosing) { + // Self-closing tag : depth was incremented when we saw `<` + // opening, so decrement it back. If depth returns to 0 this is a + // complete top-level element. + depth--; + if (depth == 0) completeEnds.add(i + 1); + } else if (inClosingTag) { + // Closing tag : decrement depth. + depth--; + if (depth == 0) completeEnds.add(i + 1); + } + // Opening tag: depth was already incremented when we saw `<`. + inSelfClosing = false; + inClosingTag = false; + } + i++; + continue; + } + + i++; + } + + if (completeEnds.isEmpty) { + // Nothing complete yet — keep buffering. + return _ExtractResult(emitted: '', remainder: input); + } + + // The last complete element ends at completeEnds.last in `stripped`. + // We need the corresponding offset in the original `input`. Since we only + // stripped `` prologs (which appear at the very start), the offset + // in `stripped` equals the offset in `input` plus the length of any + // stripped prologs that appeared before that position. For simplicity — + // and because prologs only appear at the very start — we use the original + // `input` string for slicing and just re-strip prologs in the output. + // Find the end offset in the original input by scanning for the same + // character position accounting for stripped prologs. + final prologLength = input.length - stripped.length; + final cutInStripped = completeEnds.last; + final cutInInput = cutInStripped + prologLength; + + final completeRaw = input.substring(0, cutInInput); + final remainder = input.substring(cutInInput); + + // Wrap complete elements for the XML parser in handleResponse. + String wrapped = completeRaw; + if (hasUnclosedStreamOpener) { + wrapped = '$wrapped'; + } + wrapped = '$wrapped'; + + return _ExtractResult(emitted: wrapped, remainder: remainder); + } + + /// Returns a new independent stream-response mapper closure. + /// Each QUIC aux stream must get its own mapper so that partial XML + /// fragments from different streams are buffered independently and do not + /// corrupt each other's parse state. + String Function(String) makeStreamResponseMapper() { + var buffer = ''; + return (String response) { + final combined = buffer + response; + if (combined.contains('')) { + buffer = ''; + close(); + return ''; + } + final result = _extractCompleteElements(combined); + buffer = result.remainder; + return result.emitted; + }; } String prepareStreamResponse(String response) { - Log.xmppp_receiving(response); - var response1 = extractWholeChild(restOfResponse + response); - if (response1.contains('')) { + // Accumulate with any previously incomplete data. + // Receive logging (with channel label) is done at the transport layer + // (XmppWebsocketIo.listen for TCP/WS, _startRecvLoop for QUIC) so that + // the log line can identify which stream the data arrived on. + final combined = restOfResponse + response; + + if (combined.contains('')) { + restOfResponse = ''; close(); return ''; } - if (response1.contains('stream:stream') && - !(response1.contains(''))) { - response1 = response1 + - ''; // fix for crashing xml library without ending - } - //fix for multiple roots issue - response1 = '$response1'; - return response1; + final result = _extractCompleteElements(combined); + restOfResponse = result.remainder; + return result.emitted; } void reconnect() { @@ -314,6 +502,11 @@ class Connection { final socketPort = useWebSocket ? (account.wsPort ?? account.port) : account.port; final wsUri = account.wsUrl != null ? Uri.tryParse(account.wsUrl!) : null; + final quicEndpoints = useWebSocket + ? const [] + : (account.quicEndpoints != null && account.quicEndpoints!.isNotEmpty + ? account.quicEndpoints! + : const []); final endpoints = useWebSocket ? const [] : (account.tcpEndpoints != null && account.tcpEndpoints!.isNotEmpty @@ -345,6 +538,47 @@ class Connection { } Object? lastError; + for (final endpoint in quicEndpoints) { + final socket = _socketFactory(); + try { + Log.i( + TAG, + 'QUIC endpoint attempt host=${endpoint.host} port=${endpoint.port}', + ); + await socket.connect( + endpoint.host, + endpoint.port, + useWebSocket: false, + useQuic: true, + directTls: false, + tlsHost: endpoint.tlsHost ?? account.domain, + map: prepareStreamResponse, + ); + // Supply a per-aux-stream mapper factory so each QUIC aux stream + // gets its own independent XML buffer. Without this, all streams + // share the single restOfResponse field in prepareStreamResponse, + // causing interleaved chunks from different streams to corrupt + // each other's parse state and silently drop stanzas. + if (socket.isQuic) { + (socket as dynamic).setAuxMapperFactory(makeStreamResponseMapper); + } + _attachOpenedSocket(socket); + return; + } catch (error) { + lastError = error; + try { + socket.close(); + } catch (_) { + // ignore close errors while failing over endpoints + } + Log.w( + TAG, + 'QUIC endpoint failed host=${endpoint.host} ' + 'port=${endpoint.port} error=$error', + ); + } + } + for (final endpoint in endpoints) { final socket = _socketFactory(); try { @@ -357,6 +591,7 @@ class Connection { endpoint.host, endpoint.port, useWebSocket: false, + useQuic: false, directTls: endpoint.directTls, tlsHost: endpoint.tlsHost ?? account.domain, map: prepareStreamResponse, @@ -396,6 +631,7 @@ class Connection { _pendingWriteBuffer.clear(); _flushScheduled = false; _inboundProcessingDepth = 0; + restOfResponse = ''; _openStream(); } else { Log.d(TAG, 'Closed in meantime'); @@ -467,35 +703,18 @@ class Connection { return (name == 'stream:features' || name == 'features'); } - String _unparsedXmlResponse = ''; - void handleResponse(String response) { _inboundProcessingDepth++; - String fullResponse; - if (_unparsedXmlResponse.isNotEmpty) { - if (response.length > 12) { - fullResponse = '$_unparsedXmlResponse${response.substring(12)}'; // - } else { - fullResponse = _unparsedXmlResponse; - } - Log.v(TAG, 'full response = $fullResponse'); - _unparsedXmlResponse = ''; - } else { - fullResponse = response; - } + // prepareStreamResponse (the map function) already buffers incomplete + // chunks and returns '' until a complete, parseable XML fragment is ready. + final fullResponse = response; try { if (fullResponse.isNotEmpty) { xml.XmlNode? xmlResponse; - try { - xmlResponse = xml.XmlDocument.parse( - fullResponse.replaceAll(RegExp(r'<\?(xml.+?)\>'), '')) - .firstChild; - } catch (e) { - _unparsedXmlResponse += fullResponse.substring( - 0, fullResponse.length - 13); //remove xmpp_stone end tag - xmlResponse = xml.XmlElement(xml.XmlName('error')); - } + xmlResponse = xml.XmlDocument.parse( + fullResponse.replaceAll(RegExp(r'<\?(xml.+?)\>'), '')) + .firstChild; //TODO: Improve parser for children only xmlResponse!.descendants @@ -550,7 +769,13 @@ class Connection { } void write(message) { - Log.xmppp_sending(message); + // Note: the actual "Xmpp Sending" log entry is emitted by the socket at the + // moment the data is handed to the transport (TCP/WS/QUIC). This gives an + // accurate picture of what is on the wire and, for QUIC, which stream + // (control vs aux slot) the data was written to. Logging here would only + // record queueing, which can differ significantly from actual send time + // when writes are buffered or routed to an aux QUIC stream that is still + // being opened. if (!isOpened()) { return; } @@ -879,3 +1104,10 @@ class Connection { state == XmppConnectionState.Closing; } } + +/// Result type for [Connection._extractCompleteElements]. +class _ExtractResult { + const _ExtractResult({required this.emitted, required this.remainder}); + final String emitted; + final String remainder; +} diff --git a/vendor/xmpp_stone/lib/src/account/XmppAccountSettings.dart b/vendor/xmpp_stone/lib/src/account/XmppAccountSettings.dart index 7be9ba3..49a0598 100644 --- a/vendor/xmpp_stone/lib/src/account/XmppAccountSettings.dart +++ b/vendor/xmpp_stone/lib/src/account/XmppAccountSettings.dart @@ -14,6 +14,18 @@ class XmppTcpEndpoint { final String? tlsHost; } +class XmppQuicEndpoint { + const XmppQuicEndpoint({ + required this.host, + required this.port, + this.tlsHost, + }); + + final String host; + final int port; + final String? tlsHost; +} + class XmppAccountSettings { String name; String username; @@ -30,6 +42,7 @@ class XmppAccountSettings { String? wsPath; List? wsProtocols; List? tcpEndpoints; + List? quicEndpoints; bool preferSasl2 = true; bool sasl2SendUserAgent = true; String? sasl2UserAgentId; @@ -64,6 +77,7 @@ class XmppAccountSettings { this.wsPath, this.wsProtocols, this.tcpEndpoints, + this.quicEndpoints, }); Jid get fullJid => Jid(username, domain, resource); diff --git a/vendor/xmpp_stone/lib/src/connection/XmppWebsocketApi.dart b/vendor/xmpp_stone/lib/src/connection/XmppWebsocketApi.dart index 7a69a5c..2921b3a 100644 --- a/vendor/xmpp_stone/lib/src/connection/XmppWebsocketApi.dart +++ b/vendor/xmpp_stone/lib/src/connection/XmppWebsocketApi.dart @@ -16,6 +16,7 @@ abstract class XmppWebSocket extends Stream { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost}); @@ -23,6 +24,10 @@ abstract class XmppWebSocket extends Stream { void close(); + bool get isQuic => false; + + Future getQuicStats() => Future.value(null); + Future secure( {host, SecurityContext? context, diff --git a/vendor/xmpp_stone/lib/src/connection/XmppWebsocketHtml.dart b/vendor/xmpp_stone/lib/src/connection/XmppWebsocketHtml.dart index 8890a7b..206f473 100644 --- a/vendor/xmpp_stone/lib/src/connection/XmppWebsocketHtml.dart +++ b/vendor/xmpp_stone/lib/src/connection/XmppWebsocketHtml.dart @@ -4,7 +4,6 @@ import 'package:universal_io/io.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:xmpp_stone/src/connection/XmppWebsocketApi.dart'; - export 'XmppWebsocketApi.dart'; XmppWebSocket createSocket() { @@ -31,6 +30,7 @@ class XmppWebSocketHtml extends XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = true, + bool useQuic = false, bool directTls = false, String? tlsHost}) { final uri = wsUri ?? diff --git a/vendor/xmpp_stone/lib/src/connection/XmppWebsocketIo.dart b/vendor/xmpp_stone/lib/src/connection/XmppWebsocketIo.dart index 6dc5de8..9a08d6c 100644 --- a/vendor/xmpp_stone/lib/src/connection/XmppWebsocketIo.dart +++ b/vendor/xmpp_stone/lib/src/connection/XmppWebsocketIo.dart @@ -51,8 +51,12 @@ class XmppWebSocketIo extends XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost}) async { + if (useQuic) { + throw UnsupportedError('QUIC is not implemented in XmppWebSocketIo'); + } _useWebSocket = useWebSocket || wsUri != null || wsPath != null; Log.i(TAG, 'Socket connect: host=$host port=$port useWebSocket=$_useWebSocket directTls=$directTls'); @@ -107,27 +111,41 @@ class XmppWebSocketIo extends XmppWebSocket { @override void write(Object? message) { if (_useWebSocket) { - _webSocket?.sink.add(message); + if (_webSocket == null) { + return; + } + Log.xmppp_sending(message.toString(), channel: 'ws'); + _webSocket!.sink.add(message); } else { - _tcpSocket?.write(message); + if (_tcpSocket == null) { + return; + } + Log.xmppp_sending(message.toString(), channel: 'tcp'); + _tcpSocket!.write(message); } } @override StreamSubscription listen(void Function(String event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { + final channel = _useWebSocket ? 'ws' : 'tcp'; + void logAndDeliver(String event) { + Log.xmppp_receiving(event, channel: channel); + onData?.call(event); + } + if (_useWebSocket) { return _webSocket!.stream .map((event) => event.toString()) .map(_map) - .listen(onData, + .listen(logAndDeliver, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } return _tcpSocket! .cast>() .transform(utf8.decoder) .map(_map) - .listen(onData, + .listen(logAndDeliver, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } diff --git a/vendor/xmpp_stone/lib/src/features/streammanagement/StreamManagmentModule.dart b/vendor/xmpp_stone/lib/src/features/streammanagement/StreamManagmentModule.dart index fde9415..c684481 100644 --- a/vendor/xmpp_stone/lib/src/features/streammanagement/StreamManagmentModule.dart +++ b/vendor/xmpp_stone/lib/src/features/streammanagement/StreamManagmentModule.dart @@ -110,6 +110,23 @@ class StreamManagementModule extends Negotiator { @override void negotiate(List nonzas) { + // Per XEP-0467 §Stream Management: XEP-0198 cannot apply to QUIC + // connections, and "MUST NOT be advertised or negotiated" over QUIC. + // QUIC provides its own per-stream reliability and ordering at the + // transport layer, so SM's resumption / per-stanza acknowledgement + // machinery is redundant (and would in fact misbehave across the + // multiple aux streams introduced by XEP-0467 §Multiple Streams). + // + // We never emit `` on QUIC and we ignore any `` feature + // the server might still advertise. The doap.xml entry for XEP-0198 + // remains because we DO support SM on TCP — only the QUIC path opts out. + // + // We must still transition to DONE so the negotiator queue advances + // (otherwise the connection never reaches Ready post-bind). + if (_connection.isQuic) { + state = NegotiatorState.DONE; + return; + } if (nonzas.isNotEmpty && SMNonza.match(nonzas[0]) && _connection.authenticated) { @@ -125,6 +142,9 @@ class StreamManagementModule extends Negotiator { @override bool isReady() { + // Over QUIC, SM is skipped but we must still report ready so that + // negotiate() is called and can advance the negotiator queue to DONE. + if (_connection.isQuic) return true; return super.isReady() && (isResumeAvailable() || (_connection.fullJid.resource != null && @@ -387,6 +407,14 @@ class StreamManagementModule extends Negotiator { } if (state == XmppConnectionState.Ready || state == XmppConnectionState.Resumed) { + // On QUIC, XEP-0198 SM is disabled (see negotiate()), so there is no + // SM ack machinery. QUIC also has its own transport-level keepalive + // (PING frames, configured in flutter_quic's endpoint.rs), so we do + // not need — and must not start — the XMPP-level ping keepalive timer + // here. Starting it would fire a ping every 30 s whose reply is never + // matched (the IQ router does not see the reply on QUIC), causing a + // keepaliveTimeout reconnect loop every ~60 s. + if (_connection.isQuic) return; _restartKeepaliveTimer(); probeKeepalive(shortTimeout: false); } diff --git a/vendor/xmpp_stone/lib/src/logger/Log.dart b/vendor/xmpp_stone/lib/src/logger/Log.dart index 32491de..e1b8480 100644 --- a/vendor/xmpp_stone/lib/src/logger/Log.dart +++ b/vendor/xmpp_stone/lib/src/logger/Log.dart @@ -4,7 +4,7 @@ class Log { static LogLevel logLevel = LogLevel.VERBOSE; static bool logXmpp = true; - static bool logToConsole = false; + static bool logToConsole = true; static String _timestamp() { return DateTime.now().toUtc().toIso8601String(); @@ -50,16 +50,24 @@ class Log { } } - static void xmppp_receiving(String message) { + static void xmppp_receiving(String message, {String? channel}) { if (logXmpp) { - _emit('---Xmpp Receiving:---'); + if (channel == null) { + _emit('---Xmpp Receiving:---'); + } else { + _emit('---Xmpp Receiving [$channel]:---'); + } _emit('$message'); } } - static void xmppp_sending(String message) { + static void xmppp_sending(String message, {String? channel}) { if (logXmpp) { - _emit('---Xmpp Sending:---'); + if (channel == null) { + _emit('---Xmpp Sending:---'); + } else { + _emit('---Xmpp Sending [$channel]:---'); + } _emit('$message'); } } diff --git a/vendor/xmpp_stone/test/connection_endpoint_failover_test.dart b/vendor/xmpp_stone/test/connection_endpoint_failover_test.dart index 7ca7a0b..88626d7 100644 --- a/vendor/xmpp_stone/test/connection_endpoint_failover_test.dart +++ b/vendor/xmpp_stone/test/connection_endpoint_failover_test.dart @@ -26,6 +26,7 @@ class _FakeXmppSocket extends XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost, }) async { diff --git a/vendor/xmpp_stone/test/connection_quic_failover_test.dart b/vendor/xmpp_stone/test/connection_quic_failover_test.dart new file mode 100644 index 0000000..15d9e5a --- /dev/null +++ b/vendor/xmpp_stone/test/connection_quic_failover_test.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:universal_io/io.dart'; +import 'package:xmpp_stone/src/Connection.dart'; +import 'package:xmpp_stone/src/account/XmppAccountSettings.dart'; +import 'package:xmpp_stone/src/connection/XmppWebsocketApi.dart'; + +class _RecordingSocket extends Stream implements XmppWebSocket { + _RecordingSocket(this.attempts); + + final List attempts; + final _controller = StreamController.broadcast(); + + @override + Future connect( + String host, + int port, { + String Function(String event)? map, + List? wsProtocols, + String? wsPath, + Uri? wsUri, + bool useWebSocket = false, + bool useQuic = false, + bool directTls = false, + String? tlsHost, + }) async { + final transport = useWebSocket + ? 'websocket' + : (useQuic ? 'quic' : (directTls ? 'tcp-direct-tls' : 'tcp')); + attempts.add('$transport:$host:$port'); + if (useQuic) { + throw Exception('quic failed'); + } + return this; + } + + @override + void write(Object? message) {} + + @override + void close() { + _controller.close(); + } + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; + + @override + Future secure({ + host, + SecurityContext? context, + bool Function(X509Certificate certificate)? onBadCertificate, + List? supportedProtocols, + }) async { + return null; + } + + @override + String getStreamOpeningElement(String domain) { + return ""; + } + + @override + StreamSubscription listen( + void Function(String event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return _controller.stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} + +void main() { + test('Connection falls back to TCP endpoints when QUIC endpoints fail', + () async { + final account = XmppAccountSettings.fromJid('alice@example.com', 'secret'); + account.quicEndpoints = const [ + XmppQuicEndpoint( + host: 'quic.example.com', port: 443, tlsHost: 'example.com'), + ]; + account.tcpEndpoints = const [ + XmppTcpEndpoint( + host: 'tcp.example.com', + port: 5222, + directTls: false, + tlsHost: 'example.com', + ), + ]; + + final attempts = []; + final connection = Connection( + account, + socketFactory: () => _RecordingSocket(attempts), + ); + + await connection.openSocket(); + + expect( + attempts, + equals([ + 'quic:quic.example.com:443', + 'tcp:tcp.example.com:5222', + ]), + ); + expect(connection.state, XmppConnectionState.SocketOpened); + + connection.dispose(); + }); +} diff --git a/vendor/xmpp_stone/test/ibb_manager_test.dart b/vendor/xmpp_stone/test/ibb_manager_test.dart index fb358fa..739683c 100644 --- a/vendor/xmpp_stone/test/ibb_manager_test.dart +++ b/vendor/xmpp_stone/test/ibb_manager_test.dart @@ -39,21 +39,25 @@ void main() { ''; connection.handleResponse(connection.prepareStreamResponse(closeIq)); - final openEvent = await openCompleter.future.timeout(const Duration(seconds: 1)); + final openEvent = + await openCompleter.future.timeout(const Duration(seconds: 1)); expect(openEvent.sid, 'sid1'); expect(openEvent.blockSize, 4096); - final dataEvent = await dataCompleter.future.timeout(const Duration(seconds: 1)); + final dataEvent = + await dataCompleter.future.timeout(const Duration(seconds: 1)); expect(dataEvent.sid, 'sid1'); expect(String.fromCharCodes(dataEvent.bytes), 'hi'); - final closeEvent = await closeCompleter.future.timeout(const Duration(seconds: 1)); + final closeEvent = + await closeCompleter.future.timeout(const Duration(seconds: 1)); expect(closeEvent.sid, 'sid1'); }); } class _FakeSocket extends Stream implements XmppWebSocket { - final StreamController _controller = StreamController.broadcast(); + final StreamController _controller = + StreamController.broadcast(); @override StreamSubscription listen(void Function(String event)? onData, @@ -69,6 +73,7 @@ class _FakeSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost}) async { return this; @@ -79,6 +84,10 @@ class _FakeSocket extends Stream implements XmppWebSocket { @override void close() {} + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure( diff --git a/vendor/xmpp_stone/test/iq_fallback_test.dart b/vendor/xmpp_stone/test/iq_fallback_test.dart index 3312fd0..f8d3eaf 100644 --- a/vendor/xmpp_stone/test/iq_fallback_test.dart +++ b/vendor/xmpp_stone/test/iq_fallback_test.dart @@ -35,7 +35,8 @@ void main() { } class _FakeSocket extends Stream implements XmppWebSocket { - final StreamController _controller = StreamController.broadcast(); + final StreamController _controller = + StreamController.broadcast(); @override StreamSubscription listen(void Function(String event)? onData, @@ -51,6 +52,7 @@ class _FakeSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost}) async { return this; @@ -61,6 +63,10 @@ class _FakeSocket extends Stream implements XmppWebSocket { @override void close() {} + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure( diff --git a/vendor/xmpp_stone/test/iq_router_test.dart b/vendor/xmpp_stone/test/iq_router_test.dart index a7a53fe..3ac9d18 100644 --- a/vendor/xmpp_stone/test/iq_router_test.dart +++ b/vendor/xmpp_stone/test/iq_router_test.dart @@ -65,7 +65,9 @@ void main() { expect(responses.first.type, IqStanzaType.RESULT); }); - test('IQ router returns internal-server-error on handler exception and reports', () async { + test( + 'IQ router returns internal-server-error on handler exception and reports', + () async { final account = XmppAccountSettings.fromJid('user@example.com/res', 'pass'); final connection = Connection(account); connection.socket = _FakeSocket(); @@ -199,10 +201,12 @@ void main() { final manager = RosterManager.getInstance(connection); final requestIdFuture = _waitForRosterRequestId(connection); - final future = manager.addRosterItem(Buddy(Jid.fromFullJid('alice@example.com'))); + final future = + manager.addRosterItem(Buddy(Jid.fromFullJid('alice@example.com'))); final requestId = await requestIdFuture; - final response = '' + final response = + '' ''; connection.handleResponse(connection.prepareStreamResponse(response)); @@ -279,7 +283,8 @@ Future _waitForRosterRequestId(Connection connection) { } class _FakeSocket extends Stream implements XmppWebSocket { - final StreamController _controller = StreamController.broadcast(); + final StreamController _controller = + StreamController.broadcast(); @override StreamSubscription listen(void Function(String event)? onData, @@ -295,6 +300,7 @@ class _FakeSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost}) async { return this; @@ -305,6 +311,10 @@ class _FakeSocket extends Stream implements XmppWebSocket { @override void close() {} + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure( diff --git a/vendor/xmpp_stone/test/jingle_manager_test.dart b/vendor/xmpp_stone/test/jingle_manager_test.dart index ed06c57..b91a9d6 100644 --- a/vendor/xmpp_stone/test/jingle_manager_test.dart +++ b/vendor/xmpp_stone/test/jingle_manager_test.dart @@ -20,7 +20,8 @@ void main() { completer.complete(event); }); - final iq = '' + final iq = + '' '' '' '' @@ -116,7 +117,10 @@ void main() { expect(event.content!.rtpDescription!.payloadTypes.first.name, 'opus'); expect(event.content!.rtpDescription!.payloadTypes.first.clockRate, 48000); expect(event.content!.rtpDescription!.payloadTypes.first.channels, 2); - expect(event.content!.rtpDescription!.payloadTypes.first.parameters['minptime'], '10'); + expect( + event + .content!.rtpDescription!.payloadTypes.first.parameters['minptime'], + '10'); expect(event.content!.rtpDescription!.rtcpFeedback, hasLength(1)); expect(event.content!.rtpDescription!.rtcpFeedback.first.type, 'nack'); expect(event.content!.rtpDescription!.rtcpFeedback.first.subtype, 'pli'); @@ -228,8 +232,8 @@ void main() { expect(jingle, isNotNull); final content = jingle!.getChild('content'); final description = content?.getChild('description'); - expect(description?.getAttribute('xmlns')?.value, - JingleManager.rtpNamespace); + expect( + description?.getAttribute('xmlns')?.value, JingleManager.rtpNamespace); expect(description?.getAttribute('media')?.value, 'audio'); expect(description?.getChild('rtcp-fb')?.getAttribute('xmlns')?.value, JingleManager.rtcpFbNamespace); @@ -241,8 +245,8 @@ void main() { expect(description?.getChild('source')?.getAttribute('xmlns')?.value, JingleManager.ssmaNamespace); final transport = content?.getChild('transport'); - expect(transport?.getAttribute('xmlns')?.value, - JingleManager.iceUdpNamespace); + expect( + transport?.getAttribute('xmlns')?.value, JingleManager.iceUdpNamespace); expect(transport?.getChild('fingerprint')?.textValue, 'AB:CD'); }); @@ -353,7 +357,8 @@ void main() { } class _FakeSocket extends Stream implements XmppWebSocket { - final StreamController _controller = StreamController.broadcast(); + final StreamController _controller = + StreamController.broadcast(); @override StreamSubscription listen(void Function(String event)? onData, @@ -369,6 +374,7 @@ class _FakeSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost}) async { return this; @@ -379,6 +385,10 @@ class _FakeSocket extends Stream implements XmppWebSocket { @override void close() {} + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure( diff --git a/vendor/xmpp_stone/test/keepalive_test.dart b/vendor/xmpp_stone/test/keepalive_test.dart index d1b1c66..3a4d33a 100644 --- a/vendor/xmpp_stone/test/keepalive_test.dart +++ b/vendor/xmpp_stone/test/keepalive_test.dart @@ -21,6 +21,7 @@ class _FakeSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost, }) async { @@ -34,6 +35,10 @@ class _FakeSocket extends Stream implements XmppWebSocket { @override void close() {} + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure({ diff --git a/vendor/xmpp_stone/test/mam_query_xml_test.dart b/vendor/xmpp_stone/test/mam_query_xml_test.dart index b0616c6..83e5a71 100644 --- a/vendor/xmpp_stone/test/mam_query_xml_test.dart +++ b/vendor/xmpp_stone/test/mam_query_xml_test.dart @@ -77,6 +77,38 @@ void main() { expect(_childText(set, 'max'), equals('10')); expect(_childText(set, 'before'), equals('')); }); + + // R2.1: unified server-archive catch-up query shape. + test('R2.1: unified server-archive query has after-id field and no toJid', + () async { + final connection = _buildConnection(); + final manager = MessageArchiveManager.getInstance(connection); + final sent = _waitForOutgoingIq(connection); + + // Simulate what _startUnifiedDmCatchUp does: queryById with afterId only, + // no toJid and no jid — targets the user's own server archive. + manager.queryById( + afterId: 'anchor-mam-id-99', + max: 50, + ); + + final iq = await sent; + final xml = iq.buildXmlString(); + + // Must NOT have a to= attribute (server archive, not a specific JID). + expect(iq.toJid, isNull, + reason: 'unified query must not target a specific JID'); + + // Must carry the after-id field. + expect(xml, contains('var="after-id"')); + expect(xml, contains('anchor-mam-id-99')); + + // Must carry RSM max. + expect(xml, contains('50')); + + // Must use MAM:2 namespace. + expect(xml, contains('urn:xmpp:mam:2')); + }); } Connection _buildConnection() { @@ -131,6 +163,7 @@ class _FakeSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost, }) async { @@ -142,6 +175,10 @@ class _FakeSocket extends Stream implements XmppWebSocket { @override void close() {} + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure({ diff --git a/vendor/xmpp_stone/test/sasl2_test.dart b/vendor/xmpp_stone/test/sasl2_test.dart index ea123e9..a0b8bcc 100644 --- a/vendor/xmpp_stone/test/sasl2_test.dart +++ b/vendor/xmpp_stone/test/sasl2_test.dart @@ -238,6 +238,7 @@ class _RecordingSocket extends Stream implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost, }) async { @@ -253,6 +254,10 @@ class _RecordingSocket extends Stream implements XmppWebSocket { void close() { _controller.close(); } + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure({ diff --git a/vendor/xmpp_stone/test/stream_response_mapper_test.dart b/vendor/xmpp_stone/test/stream_response_mapper_test.dart new file mode 100644 index 0000000..c6a3139 --- /dev/null +++ b/vendor/xmpp_stone/test/stream_response_mapper_test.dart @@ -0,0 +1,311 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:universal_io/io.dart'; +import 'package:xmpp_stone/src/Connection.dart'; +import 'package:xmpp_stone/src/account/XmppAccountSettings.dart'; +import 'package:xmpp_stone/src/connection/XmppWebsocketApi.dart'; + +// Minimal fake socket — does nothing, just satisfies the interface. +class _FakeSocket extends Stream implements XmppWebSocket { + @override + Future connect( + String host, + int port, { + String Function(String event)? map, + List? wsProtocols, + String? wsPath, + Uri? wsUri, + bool useWebSocket = false, + bool useQuic = false, + bool directTls = false, + String? tlsHost, + }) async => this; + + @override + void write(Object? message) {} + + @override + void close() {} + @override + Future getQuicStats() => Future.value(null); + + @override + bool get isQuic => false; + + @override + Future secure({ + host, + SecurityContext? context, + bool Function(X509Certificate certificate)? onBadCertificate, + List? supportedProtocols, + }) async => null; + + @override + String getStreamOpeningElement(String domain) => ''; + + @override + StreamSubscription listen( + void Function(String event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) => + const Stream.empty().listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); +} + +Connection _newConnection() { + final account = XmppAccountSettings.fromJid('alice@example.com', 'secret') + ..bufferedWritesEnabled = false; + final connection = Connection.getInstance(account); + connection.socket = _FakeSocket(); + return connection; +} + +void main() { + tearDown(() { + for (final connection in Connection.instances.values.toList()) { + connection.dispose(); + Connection.removeInstance(connection.account); + } + }); + + group('makeStreamResponseMapper — independence', () { + test('two mappers maintain separate buffers', () { + final connection = _newConnection(); + final mapperA = connection.makeStreamResponseMapper(); + final mapperB = connection.makeStreamResponseMapper(); + + // Feed an incomplete stanza to A only. + const partial = ''; + expect(mapperA(partial), isEmpty, + reason: 'incomplete XML should be buffered, not returned'); + + // B has not received anything — it should still return empty for its own + // partial chunk, not be contaminated by A's buffer. + expect(mapperB(''); + expect(resultB, isNotEmpty, + reason: 'B must not be affected by A buffering a partial stanza'); + }); + }); + + group('makeStreamResponseMapper — buffering behaviour', () { + test('returns empty string for incomplete XML', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + expect(mapper(''); + expect(result, isNotEmpty, reason: 'second half completes the stanza'); + expect(result, contains('message')); + }); + + test('buffer is cleared after a complete stanza', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + // Complete stanza. + mapper(''); + + // A subsequent complete stanza should also succeed (buffer was cleared). + final result = mapper(''); + expect(result, isNotEmpty, + reason: 'mapper should work correctly after buffer was cleared'); + }); + + test('wraps output in xmpp_stone root element', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + final result = mapper(''); + expect(result, startsWith('')); + expect(result, endsWith('')); + }); + + test('handles stream:stream opener by appending synthetic close tag', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + // A stream:stream opener without a matching close is valid mid-session. + const opener = + ""; + final result = mapper(opener); + // The mapper should not return empty — it appends to + // make the fragment parseable. + expect(result, isNotEmpty, + reason: 'stream opener should be returned after synthetic close'); + }); + + test('multiple complete stanzas in one chunk are all returned', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + const chunk = ''; + final result = mapper(chunk); + expect(result, isNotEmpty); + expect(result, contains('unavailable')); + }); + }); + + group('makeStreamResponseMapper — stream close', () { + test('returns empty string when is received', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + final result = mapper(''); + expect(result, isEmpty, + reason: 'stream close should not produce output'); + }); + }); + + group('makeStreamResponseMapper — burst with trailing partial (regression)', () { + // This is the core bug: under high latency a burst of complete stanzas + // arrives followed by a partial fragment. The old implementation would + // hold back ALL complete stanzas until the partial was completed, causing + // stanza starvation. The new implementation must emit the complete stanzas + // immediately and only buffer the trailing partial. + + test('emits complete stanzas even when followed by a partial fragment', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + // Two complete stanzas followed by an incomplete one — all in one chunk. + const chunk = + '' + '' + ' + final result = mapper(chunk); + expect(result, isNotEmpty, + reason: 'complete stanzas must be emitted despite trailing partial'); + expect(result, contains('unavailable'), + reason: 'both complete stanzas must be present in output'); + }); + + test('trailing partial is buffered and completed on next chunk', () { + final connection = _newConnection(); + final mapper = connection.makeStreamResponseMapper(); + + // First chunk: two complete stanzas + start of a third. + const chunk1 = + '' + '' + ''); + expect(result2, isNotEmpty, + reason: 'completing the partial stanza must produce output'); + expect(result2, contains('message')); + }); + + test('prepareStreamResponse also emits complete stanzas before partial', () { + final connection = _newConnection(); + + const chunk = + '' + '' + ''); + } + buffer.write(' implements XmppWebSocket { String? wsPath, Uri? wsUri, bool useWebSocket = false, + bool useQuic = false, bool directTls = false, String? tlsHost, }) async { @@ -86,6 +87,10 @@ class _RecordingSocket extends Stream implements XmppWebSocket { void close() { _controller.close(); } + @override + Future getQuicStats() => Future.value(null); + @override + bool get isQuic => false; @override Future secure({ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9429a14..6fe685a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_local_notifications_windows + flutter_quic jni )