From fe6e07353e8cb90535a2ad3d08e0dcec0a2fecde Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Fri, 29 May 2026 14:02:04 +0900 Subject: [PATCH] chore: restore product scaffold to rollback baseline --- .github/workflows/bench-advisory.yml | 2 - .github/workflows/bench-baseline-update.yml | 2 - .github/workflows/ci.yml | 65 - .gitignore | 6 - Cargo.lock | 335 +- Cargo.toml | 18 +- about.toml | 1 + .../android/app/build.gradle.kts | 19 +- .../chanora_flutter/android/gradle.properties | 4 - .../ios/Runner/AppDelegate.swift | 73 +- .../lib/design/platform_capabilities.dart | 2 +- apps/chanora_flutter/lib/l10n/app_en.arb | 58 + apps/chanora_flutter/lib/l10n/app_zh.arb | 53 + .../lib/l10n/generated/app_localizations.dart | 318 + .../l10n/generated/app_localizations_en.dart | 163 + .../l10n/generated/app_localizations_zh.dart | 162 + apps/chanora_flutter/lib/main.dart | 1344 +- .../lib/services/app_bootstrap.dart | 11 +- .../lib/services/connection_phase_state.dart | 21 + .../lib/services/link_trust_service.dart | 71 +- .../lib/services/prefetch_debouncer.dart | 31 + .../services/startup_dependency_check.dart | 444 - .../lib/services/ui_preferences_service.dart | 140 +- apps/chanora_flutter/lib/src/rust/api.dart | 187 +- .../lib/src/rust/api.freezed.dart | 38 +- .../lib/src/rust/frb_generated.dart | 456 +- .../lib/src/rust/frb_generated.io.dart | 65 + .../lib/src/rust/frb_generated.web.dart | 65 + .../lib/widgets/audio_output_tile.dart | 82 +- .../audio_processing_config_state.dart | 25 +- .../lib/widgets/chat_views.dart | 364 +- .../lib/widgets/client_info_sheet.dart | 491 + .../lib/widgets/connect_widgets.dart | 44 +- .../lib/widgets/input_dialogs.dart | 134 +- .../lib/widgets/ptt_capability_badge.dart | 71 +- .../lib/widgets/snapshot_view.dart | 261 +- .../widgets/startup_dependency_screen.dart | 468 - .../lib/widgets/voice_compact.dart | 33 +- .../lib/widgets/voice_settings.dart | 31 +- .../lib/widgets/voice_settings_controls.dart | 33 +- apps/chanora_flutter/linux/CMakeLists.txt | 60 - .../linux/runner/my_application.cc | 22 - .../services/connection_phase_state_test.dart | 81 + .../services/prefetch_debouncer_test.dart | 48 + .../services/snapshot_state_mapper_test.dart | 2 - .../startup_dependency_check_test.dart | 167 - .../services/ui_preferences_service_test.dart | 87 +- apps/chanora_flutter/test/widget_test.dart | 32 + .../audio_processing_config_state_test.dart | 30 +- .../test/widgets/chat_views_test.dart | 105 +- .../test/widgets/client_info_sheet_test.dart | 112 + .../test/widgets/connect_widgets_test.dart | 41 - .../test/widgets/input_dialogs_test.dart | 41 - .../widgets/mobile_ui_resilience_test.dart | 160 + .../test/widgets/snapshot_view_test.dart | 362 +- .../startup_dependency_screen_test.dart | 67 - .../widgets/voice_settings_controls_test.dart | 19 +- core/chanora_core/Cargo.toml | 2 + core/chanora_core/src/lib.rs | 191 +- core/chanora_core/tests/alpha_smoke.rs | 1 + crates/chanora_audio/Cargo.toml | 64 +- crates/chanora_audio/src/engine.rs | 343 +- crates/chanora_audio/src/ios_voice_unit.rs | 26 +- crates/chanora_audio/src/lib.rs | 6 +- .../chanora_audio/src/linux_pipewire_input.rs | 967 -- .../chanora_audio/src/ptt_backends/linux.rs | 86 +- crates/chanora_audio/src/sdl_output.rs | 203 + crates/chanora_audio/src/vad/silero_onnx.rs | 139 +- crates/chanora_bridge/src/api.rs | 130 +- crates/chanora_bridge/src/frb_generated.rs | 391 +- crates/chanora_diagnostics/src/lib.rs | 18 + crates/chanora_protocol/Cargo.toml | 17 +- crates/chanora_protocol/src/adapter.rs | 537 +- crates/chanora_protocol/src/dto.rs | 68 +- crates/chanora_protocol/src/lib.rs | 7 +- crates/chanora_resolver/Cargo.toml | 2 + crates/chanora_resolver/README.md | 11 +- crates/chanora_resolver/src/lib.rs | 218 +- crates/chanora_server_prefetch/Cargo.toml | 19 + crates/chanora_server_prefetch/src/lib.rs | 312 + crates/chanora_state/Cargo.toml | 1 + crates/chanora_state/src/lib.rs | 779 +- deny.toml | 10 +- docs/00_AGENT_ROUTER.md | 31 - docs/P0_MVP_AGENT_SPEC.md | 136 - docs/P1_BETA_AGENT_SPEC.md | 58 - docs/P2_PRODUCTION_AGENT_SPEC.md | 34 - docs/README.md | 11 - docs/SOP_AGENT_OPERATIONS.md | 46 - docs/architecture/desktop-ptt-architecture.md | 43 + docs/architecture/sad.md | 186 + docs/architecture/sdd.md | 154 + docs/architecture/sysdes.md | 21 + docs/chanora_p1_voice_agent_spec_v1_1.json | 767 -- docs/governance/baseline-approval-record.md | 32 + .../baseline-candidate-validation-report.md | 38 + docs/governance/decision-impact-assessment.md | 18 + docs/governance/document-index.md | 35 + docs/governance/document-naming-convention.md | 33 + docs/governance/document-review-report.md | 37 + .../git-commit-message-convention.md | 21 + docs/governance/path-migration-map.md | 18 + docs/governance/product-decision-register.md | 22 + .../repo-format-validation-report.md | 22 + docs/governance/traceability-matrix.md | 73 + docs/i18n/localization-architecture.md | 44 + docs/implementation-status-2026-05-28.md | 160 + .../legal/trademark-and-attribution-review.md | 47 + .../material3-guideline.md | 0 docs/phase_index.json | 7684 ----------- docs/privacy/privacy-policy.md | 51 + .../aspice-swe2-swe3-integration-note.md | 28 + docs/references/external-references.md | 21 + docs/release/dv-waiver-register.md | 35 + docs/release/linux-build.md | 102 - docs/release/platform-release-policy.md | 26 + .../release-readiness-go-nogo-record.md | 102 + docs/requirements/srs.md | 22 + docs/requirements/sysrs.md | 22 + .../dependency-and-supply-chain-report.md | 43 + .../diagnostic-redaction-audit-report.md | 31 + docs/security/flutter-license-inventory.md | 5284 ++++++++ docs/security/license-inventory.md | 10765 ++++++++++++++++ docs/security/secure-storage-audit-report.md | 31 + .../security-privacy-legal-guideline.md | 63 + docs/security/threat-model.md | 33 + docs/{additional_docs => }/srs.md | 0 ...026-05-28-chanora-server-prefetch-crate.md | 689 + .../2026-05-28-server-resolution-prefetch.md | 899 ++ .../plans/2026-05-29-dv-evidence-pack.md | 114 + .../2026-05-29-finish-dv-document-tree.md | 79 + ...05-29-state-sync-ui-settings-validation.md | 108 + .../plans/2026-05-29-swe2-swe3-baselines.md | 68 + ...28-chanora-server-prefetch-crate-design.md | 116 + ...05-28-server-resolution-prefetch-design.md | 203 + ...tate-sync-ui-settings-validation-design.md | 70 + docs/{additional_docs => }/sysdes.md | 0 docs/{additional_docs => }/sysrs.md | 0 docs/ts3_development_manual_en_v6.md | 4616 ------- docs/ui-ux/adaptive-layout-platform-guide.md | 27 + docs/ui-ux/material3-component-catalog.md | 38 + docs/ui-ux/material3-design-tokens.md | 21 + docs/ui-ux/material3-guideline.md | 8 + .../swe4-unit-verification-plan.md | 57 + ...-software-integration-verification-plan.md | 58 + .../swe6-software-verification-plan.md | 62 + ...s4-system-integration-verification-plan.md | 57 + docs/verification/verification-master-plan.md | 90 + poc/README.md | 49 - .../Cargo.lock | 929 -- .../Cargo.toml | 32 - .../README.md | 138 - .../VERIFICATION.md | 154 - .../android/app/build.gradle.kts | 124 - .../android/app/src/main/AndroidManifest.xml | 21 - .../app/chanora/poc/audio/MainActivity.kt | 134 - .../java/app/chanora/poc/audio/NativeAudio.kt | 22 - .../android/build.gradle.kts | 5 - .../android/gradle.properties | 4 - .../android/gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - .../android/gradlew | 249 - .../android/settings.gradle.kts | 18 - .../src/lib.rs | 443 - poc/audio-capture-playback-spike/Cargo.lock | 1315 -- poc/audio-capture-playback-spike/Cargo.toml | 32 - poc/audio-capture-playback-spike/README.md | 119 - .../VERIFICATION.md | 123 - .../src/capture.rs | 179 - poc/audio-capture-playback-spike/src/lib.rs | 36 - poc/audio-capture-playback-spike/src/main.rs | 89 - .../src/playback.rs | 153 - poc/audio-capture-playback-spike/src/synth.rs | 38 - .../tests/audio.rs | 46 - poc/diagnostics-redaction-spike/Cargo.lock | 190 - poc/diagnostics-redaction-spike/Cargo.toml | 28 - poc/diagnostics-redaction-spike/README.md | 79 - .../VERIFICATION.md | 104 - poc/diagnostics-redaction-spike/src/bundle.rs | 96 - poc/diagnostics-redaction-spike/src/lib.rs | 31 - poc/diagnostics-redaction-spike/src/main.rs | 30 - poc/diagnostics-redaction-spike/src/policy.rs | 153 - .../src/redactor.rs | 127 - .../tests/redaction.rs | 235 - poc/flutter_rust_bridge_hello/.gitignore | 45 - poc/flutter_rust_bridge_hello/.metadata | 45 - poc/flutter_rust_bridge_hello/README.md | 88 - poc/flutter_rust_bridge_hello/VERIFICATION.md | 68 - .../analysis_options.yaml | 28 - .../android/.gitignore | 14 - .../android/app/build.gradle.kts | 44 - .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 45 - .../flutter_rust_bridge_hello/MainActivity.kt | 5 - .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values/styles.xml | 18 - .../app/src/profile/AndroidManifest.xml | 7 - .../android/build.gradle.kts | 24 - .../android/gradle.properties | 2 - .../gradle/wrapper/gradle-wrapper.properties | 5 - .../android/settings.gradle.kts | 26 - .../flutter_rust_bridge.yaml | 3 - .../integration_test/simple_test.dart | 48 - poc/flutter_rust_bridge_hello/ios/.gitignore | 34 - .../ios/Flutter/AppFrameworkInfo.plist | 24 - .../ios/Flutter/Debug.xcconfig | 1 - .../ios/Flutter/Release.xcconfig | 1 - .../ios/Runner.xcodeproj/project.pbxproj | 620 - .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 101 - .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../ios/Runner/AppDelegate.swift | 16 - .../AppIcon.appiconset/Contents.json | 122 - .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 - .../ios/Runner/Base.lproj/Main.storyboard | 26 - .../ios/Runner/Info.plist | 70 - .../ios/Runner/Runner-Bridging-Header.h | 1 - .../ios/Runner/SceneDelegate.swift | 6 - .../ios/RunnerTests/RunnerTests.swift | 12 - poc/flutter_rust_bridge_hello/lib/main.dart | 175 - .../lib/src/rust/api/simple.dart | 82 - .../lib/src/rust/frb_generated.dart | 488 - .../lib/src/rust/frb_generated.io.dart | 174 - .../lib/src/rust/frb_generated.web.dart | 174 - .../linux/.gitignore | 1 - .../linux/CMakeLists.txt | 128 - .../linux/flutter/CMakeLists.txt | 88 - .../linux/flutter/generated_plugins.cmake | 24 - .../linux/runner/CMakeLists.txt | 26 - .../linux/runner/main.cc | 6 - .../linux/runner/my_application.cc | 148 - .../linux/runner/my_application.h | 21 - .../macos/.gitignore | 7 - .../macos/Flutter/Flutter-Debug.xcconfig | 1 - .../macos/Flutter/Flutter-Release.xcconfig | 1 - .../macos/Runner.xcodeproj/project.pbxproj | 705 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 99 - .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../macos/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 68 - .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 0 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 0 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 0 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 0 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 0 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 0 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 0 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 - .../macos/Runner/Configs/AppInfo.xcconfig | 14 - .../macos/Runner/Configs/Debug.xcconfig | 2 - .../macos/Runner/Configs/Release.xcconfig | 2 - .../macos/Runner/Configs/Warnings.xcconfig | 13 - .../macos/Runner/DebugProfile.entitlements | 12 - .../macos/Runner/Info.plist | 32 - .../macos/Runner/MainFlutterWindow.swift | 15 - .../macos/Runner/Release.entitlements | 8 - .../macos/RunnerTests/RunnerTests.swift | 12 - poc/flutter_rust_bridge_hello/pubspec.lock | 315 - poc/flutter_rust_bridge_hello/pubspec.yaml | 94 - poc/flutter_rust_bridge_hello/rust/.gitignore | 1 - poc/flutter_rust_bridge_hello/rust/Cargo.lock | 763 -- poc/flutter_rust_bridge_hello/rust/Cargo.toml | 15 - .../rust/src/api/mod.rs | 1 - .../rust/src/api/simple.rs | 97 - .../rust/src/frb_generated.rs | 558 - poc/flutter_rust_bridge_hello/rust/src/lib.rs | 2 - .../rust_builder/.gitignore | 29 - .../rust_builder/README.md | 1 - .../rust_builder/android/.gitignore | 9 - .../rust_builder/android/build.gradle | 56 - .../rust_builder/android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 3 - .../rust_builder/cargokit/.gitignore | 4 - .../rust_builder/cargokit/LICENSE | 42 - .../rust_builder/cargokit/README | 11 - .../rust_builder/cargokit/build_pod.sh | 58 - .../cargokit/build_tool/README.md | 5 - .../cargokit/build_tool/analysis_options.yaml | 34 - .../cargokit/build_tool/bin/build_tool.dart | 8 - .../cargokit/build_tool/lib/build_tool.dart | 8 - .../lib/src/android_environment.dart | 195 - .../lib/src/artifacts_provider.dart | 266 - .../build_tool/lib/src/build_cmake.dart | 40 - .../build_tool/lib/src/build_gradle.dart | 49 - .../build_tool/lib/src/build_pod.dart | 89 - .../build_tool/lib/src/build_tool.dart | 276 - .../cargokit/build_tool/lib/src/builder.dart | 209 - .../cargokit/build_tool/lib/src/cargo.dart | 48 - .../build_tool/lib/src/crate_hash.dart | 124 - .../build_tool/lib/src/environment.dart | 68 - .../cargokit/build_tool/lib/src/logging.dart | 52 - .../cargokit/build_tool/lib/src/options.dart | 309 - .../lib/src/precompile_binaries.dart | 205 - .../cargokit/build_tool/lib/src/rustup.dart | 149 - .../cargokit/build_tool/lib/src/target.dart | 147 - .../cargokit/build_tool/lib/src/util.dart | 172 - .../build_tool/lib/src/verify_binaries.dart | 84 - .../cargokit/build_tool/pubspec.lock | 453 - .../cargokit/build_tool/pubspec.yaml | 33 - .../cargokit/cmake/cargokit.cmake | 99 - .../cargokit/cmake/resolve_symlinks.ps1 | 34 - .../cargokit/gradle/plugin.gradle | 184 - .../rust_builder/cargokit/run_build_tool.cmd | 91 - .../rust_builder/cargokit/run_build_tool.sh | 99 - .../rust_builder/ios/Classes/dummy_file.c | 1 - ...rust_lib_flutter_rust_bridge_hello.podspec | 45 - .../rust_builder/linux/CMakeLists.txt | 19 - .../rust_builder/macos/Classes/dummy_file.c | 1 - ...rust_lib_flutter_rust_bridge_hello.podspec | 44 - .../rust_builder/pubspec.yaml | 34 - .../rust_builder/windows/.gitignore | 17 - .../rust_builder/windows/CMakeLists.txt | 20 - .../test/poc_verification_test.dart | 40 - .../test_driver/integration_test.dart | 3 - poc/flutter_rust_bridge_hello/web/favicon.png | Bin 917 -> 0 bytes .../web/icons/Icon-192.png | Bin 5292 -> 0 bytes .../web/icons/Icon-512.png | Bin 8252 -> 0 bytes .../web/icons/Icon-maskable-192.png | Bin 5594 -> 0 bytes .../web/icons/Icon-maskable-512.png | Bin 20998 -> 0 bytes poc/flutter_rust_bridge_hello/web/index.html | 46 - .../web/manifest.json | 35 - .../windows/.gitignore | 17 - .../windows/CMakeLists.txt | 108 - .../windows/flutter/CMakeLists.txt | 109 - .../windows/flutter/generated_plugins.cmake | 24 - .../windows/runner/CMakeLists.txt | 40 - .../windows/runner/Runner.rc | 121 - .../windows/runner/flutter_window.cpp | 71 - .../windows/runner/flutter_window.h | 33 - .../windows/runner/main.cpp | 43 - .../windows/runner/resource.h | 16 - .../windows/runner/resources/app_icon.ico | Bin 33772 -> 0 bytes .../windows/runner/runner.exe.manifest | 14 - .../windows/runner/utils.cpp | 65 - .../windows/runner/utils.h | 19 - .../windows/runner/win32_window.cpp | 288 - .../windows/runner/win32_window.h | 102 - poc/linux_voice_test/Cargo.lock | 1293 -- poc/linux_voice_test/Cargo.toml | 17 - poc/linux_voice_test/README.md | 37 - poc/linux_voice_test/VERIFICATION.md | 59 - poc/linux_voice_test/src/audio_buffer.rs | 29 - poc/linux_voice_test/src/linux_pipewire.rs | 318 - poc/linux_voice_test/src/linux_pulseaudio.rs | 309 - poc/linux_voice_test/src/linux_shared.rs | 384 - poc/linux_voice_test/src/main.rs | 26 - poc/linux_voice_test/src/windows_cpal.rs | 168 - poc/secure-storage-spike/Cargo.lock | 1854 --- poc/secure-storage-spike/Cargo.toml | 42 - poc/secure-storage-spike/README.md | 89 - poc/secure-storage-spike/VERIFICATION.md | 105 - poc/secure-storage-spike/src/lib.rs | 28 - poc/secure-storage-spike/src/main.rs | 80 - poc/secure-storage-spike/src/secret.rs | 206 - poc/secure-storage-spike/src/sqlite_repo.rs | 56 - poc/secure-storage-spike/src/test_logger.rs | 54 - poc/secure-storage-spike/tests/audit.rs | 229 - poc/sqlite-storage-spike/Cargo.lock | 746 -- poc/sqlite-storage-spike/Cargo.toml | 27 - poc/sqlite-storage-spike/README.md | 85 - poc/sqlite-storage-spike/VERIFICATION.md | 94 - poc/sqlite-storage-spike/src/lib.rs | 40 - poc/sqlite-storage-spike/src/main.rs | 66 - poc/sqlite-storage-spike/src/migrate.rs | 145 - poc/sqlite-storage-spike/src/repo.rs | 218 - poc/sqlite-storage-spike/tests/storage.rs | 237 - poc/ts3-manual-research/README.md | 35 - poc/ts3-manual-research/VERIFICATION.md | 60 - .../.gitignore | 1 - .../Cargo.lock | 3453 ----- .../Cargo.toml | 19 - poc/tsclientlib-channel-query-spike/README.md | 45 - .../VERIFICATION.md | 60 - .../src/main.rs | 413 - poc/tsclientlib-connect-spike/Cargo.lock | 3472 ----- poc/tsclientlib-connect-spike/Cargo.toml | 21 - poc/tsclientlib-connect-spike/README.md | 57 - poc/tsclientlib-connect-spike/VERIFICATION.md | 68 - poc/tsclientlib-connect-spike/src/main.rs | 194 - poc/tsclientlib-filetransfer-spike/.gitignore | 2 - poc/tsclientlib-filetransfer-spike/Cargo.lock | 3453 ----- poc/tsclientlib-filetransfer-spike/Cargo.toml | 19 - poc/tsclientlib-filetransfer-spike/README.md | 41 - .../VERIFICATION.md | 45 - .../src/main.rs | 297 - poc/tsclientlib-query-spike/.gitignore | 1 - poc/tsclientlib-query-spike/Cargo.lock | 3454 ----- poc/tsclientlib-query-spike/Cargo.toml | 20 - poc/tsclientlib-query-spike/README.md | 48 - poc/tsclientlib-query-spike/VERIFICATION.md | 82 - poc/tsclientlib-query-spike/src/main.rs | 776 -- poc/tsidentity-import-spike/.gitignore | 2 - poc/tsidentity-import-spike/Cargo.lock | 3452 ----- poc/tsidentity-import-spike/Cargo.toml | 18 - poc/tsidentity-import-spike/README.md | 35 - poc/tsidentity-import-spike/VERIFICATION.md | 59 - poc/tsidentity-import-spike/src/main.rs | 249 - tools/build-android-aab.sh | 116 - tools/build-linux-deb.sh | 160 - tools/build-linux.sh | 128 +- tools/dev-env.sh | 18 - 434 files changed, 27278 insertions(+), 63230 deletions(-) create mode 100644 apps/chanora_flutter/lib/services/prefetch_debouncer.dart delete mode 100644 apps/chanora_flutter/lib/services/startup_dependency_check.dart create mode 100644 apps/chanora_flutter/lib/widgets/client_info_sheet.dart delete mode 100644 apps/chanora_flutter/lib/widgets/startup_dependency_screen.dart create mode 100644 apps/chanora_flutter/test/services/prefetch_debouncer_test.dart delete mode 100644 apps/chanora_flutter/test/services/startup_dependency_check_test.dart create mode 100644 apps/chanora_flutter/test/widgets/client_info_sheet_test.dart delete mode 100644 apps/chanora_flutter/test/widgets/connect_widgets_test.dart delete mode 100644 apps/chanora_flutter/test/widgets/input_dialogs_test.dart create mode 100644 apps/chanora_flutter/test/widgets/mobile_ui_resilience_test.dart delete mode 100644 apps/chanora_flutter/test/widgets/startup_dependency_screen_test.dart delete mode 100644 crates/chanora_audio/src/linux_pipewire_input.rs create mode 100644 crates/chanora_audio/src/sdl_output.rs create mode 100644 crates/chanora_server_prefetch/Cargo.toml create mode 100644 crates/chanora_server_prefetch/src/lib.rs delete mode 100644 docs/00_AGENT_ROUTER.md delete mode 100644 docs/P0_MVP_AGENT_SPEC.md delete mode 100644 docs/P1_BETA_AGENT_SPEC.md delete mode 100644 docs/P2_PRODUCTION_AGENT_SPEC.md delete mode 100644 docs/README.md delete mode 100644 docs/SOP_AGENT_OPERATIONS.md create mode 100644 docs/architecture/desktop-ptt-architecture.md create mode 100644 docs/architecture/sad.md create mode 100644 docs/architecture/sdd.md create mode 100644 docs/architecture/sysdes.md delete mode 100644 docs/chanora_p1_voice_agent_spec_v1_1.json create mode 100644 docs/governance/baseline-approval-record.md create mode 100644 docs/governance/baseline-candidate-validation-report.md create mode 100644 docs/governance/decision-impact-assessment.md create mode 100644 docs/governance/document-index.md create mode 100644 docs/governance/document-naming-convention.md create mode 100644 docs/governance/document-review-report.md create mode 100644 docs/governance/git-commit-message-convention.md create mode 100644 docs/governance/path-migration-map.md create mode 100644 docs/governance/product-decision-register.md create mode 100644 docs/governance/repo-format-validation-report.md create mode 100644 docs/governance/traceability-matrix.md create mode 100644 docs/i18n/localization-architecture.md create mode 100644 docs/implementation-status-2026-05-28.md create mode 100644 docs/legal/trademark-and-attribution-review.md rename docs/{additional_docs => }/material3-guideline.md (100%) delete mode 100644 docs/phase_index.json create mode 100644 docs/privacy/privacy-policy.md create mode 100644 docs/references/aspice-swe2-swe3-integration-note.md create mode 100644 docs/references/external-references.md create mode 100644 docs/release/dv-waiver-register.md delete mode 100644 docs/release/linux-build.md create mode 100644 docs/release/platform-release-policy.md create mode 100644 docs/release/release-readiness-go-nogo-record.md create mode 100644 docs/requirements/srs.md create mode 100644 docs/requirements/sysrs.md create mode 100644 docs/security/dependency-and-supply-chain-report.md create mode 100644 docs/security/diagnostic-redaction-audit-report.md create mode 100644 docs/security/flutter-license-inventory.md create mode 100644 docs/security/license-inventory.md create mode 100644 docs/security/secure-storage-audit-report.md create mode 100644 docs/security/security-privacy-legal-guideline.md create mode 100644 docs/security/threat-model.md rename docs/{additional_docs => }/srs.md (100%) create mode 100644 docs/superpowers/plans/2026-05-28-chanora-server-prefetch-crate.md create mode 100644 docs/superpowers/plans/2026-05-28-server-resolution-prefetch.md create mode 100644 docs/superpowers/plans/2026-05-29-dv-evidence-pack.md create mode 100644 docs/superpowers/plans/2026-05-29-finish-dv-document-tree.md create mode 100644 docs/superpowers/plans/2026-05-29-state-sync-ui-settings-validation.md create mode 100644 docs/superpowers/plans/2026-05-29-swe2-swe3-baselines.md create mode 100644 docs/superpowers/specs/2026-05-28-chanora-server-prefetch-crate-design.md create mode 100644 docs/superpowers/specs/2026-05-28-server-resolution-prefetch-design.md create mode 100644 docs/superpowers/specs/2026-05-29-state-sync-ui-settings-validation-design.md rename docs/{additional_docs => }/sysdes.md (100%) rename docs/{additional_docs => }/sysrs.md (100%) delete mode 100644 docs/ts3_development_manual_en_v6.md create mode 100644 docs/ui-ux/adaptive-layout-platform-guide.md create mode 100644 docs/ui-ux/material3-component-catalog.md create mode 100644 docs/ui-ux/material3-design-tokens.md create mode 100644 docs/ui-ux/material3-guideline.md create mode 100644 docs/verification/swe4-unit-verification-plan.md create mode 100644 docs/verification/swe5-software-integration-verification-plan.md create mode 100644 docs/verification/swe6-software-verification-plan.md create mode 100644 docs/verification/sys4-system-integration-verification-plan.md create mode 100644 docs/verification/verification-master-plan.md delete mode 100644 poc/README.md delete mode 100644 poc/audio-capture-playback-android-spike/Cargo.lock delete mode 100644 poc/audio-capture-playback-android-spike/Cargo.toml delete mode 100644 poc/audio-capture-playback-android-spike/README.md delete mode 100644 poc/audio-capture-playback-android-spike/VERIFICATION.md delete mode 100644 poc/audio-capture-playback-android-spike/android/app/build.gradle.kts delete mode 100644 poc/audio-capture-playback-android-spike/android/app/src/main/AndroidManifest.xml delete mode 100644 poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/MainActivity.kt delete mode 100644 poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/NativeAudio.kt delete mode 100644 poc/audio-capture-playback-android-spike/android/build.gradle.kts delete mode 100644 poc/audio-capture-playback-android-spike/android/gradle.properties delete mode 100644 poc/audio-capture-playback-android-spike/android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 poc/audio-capture-playback-android-spike/android/gradle/wrapper/gradle-wrapper.properties delete mode 100755 poc/audio-capture-playback-android-spike/android/gradlew delete mode 100644 poc/audio-capture-playback-android-spike/android/settings.gradle.kts delete mode 100644 poc/audio-capture-playback-android-spike/src/lib.rs delete mode 100644 poc/audio-capture-playback-spike/Cargo.lock delete mode 100644 poc/audio-capture-playback-spike/Cargo.toml delete mode 100644 poc/audio-capture-playback-spike/README.md delete mode 100644 poc/audio-capture-playback-spike/VERIFICATION.md delete mode 100644 poc/audio-capture-playback-spike/src/capture.rs delete mode 100644 poc/audio-capture-playback-spike/src/lib.rs delete mode 100644 poc/audio-capture-playback-spike/src/main.rs delete mode 100644 poc/audio-capture-playback-spike/src/playback.rs delete mode 100644 poc/audio-capture-playback-spike/src/synth.rs delete mode 100644 poc/audio-capture-playback-spike/tests/audio.rs delete mode 100644 poc/diagnostics-redaction-spike/Cargo.lock delete mode 100644 poc/diagnostics-redaction-spike/Cargo.toml delete mode 100644 poc/diagnostics-redaction-spike/README.md delete mode 100644 poc/diagnostics-redaction-spike/VERIFICATION.md delete mode 100644 poc/diagnostics-redaction-spike/src/bundle.rs delete mode 100644 poc/diagnostics-redaction-spike/src/lib.rs delete mode 100644 poc/diagnostics-redaction-spike/src/main.rs delete mode 100644 poc/diagnostics-redaction-spike/src/policy.rs delete mode 100644 poc/diagnostics-redaction-spike/src/redactor.rs delete mode 100644 poc/diagnostics-redaction-spike/tests/redaction.rs delete mode 100644 poc/flutter_rust_bridge_hello/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/.metadata delete mode 100644 poc/flutter_rust_bridge_hello/README.md delete mode 100644 poc/flutter_rust_bridge_hello/VERIFICATION.md delete mode 100644 poc/flutter_rust_bridge_hello/analysis_options.yaml delete mode 100644 poc/flutter_rust_bridge_hello/android/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/android/app/build.gradle.kts delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/debug/AndroidManifest.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/AndroidManifest.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/kotlin/com/example/flutter_rust_bridge_hello/MainActivity.kt delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/values-night/styles.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/main/res/values/styles.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/app/src/profile/AndroidManifest.xml delete mode 100644 poc/flutter_rust_bridge_hello/android/build.gradle.kts delete mode 100644 poc/flutter_rust_bridge_hello/android/gradle.properties delete mode 100644 poc/flutter_rust_bridge_hello/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 poc/flutter_rust_bridge_hello/android/settings.gradle.kts delete mode 100644 poc/flutter_rust_bridge_hello/flutter_rust_bridge.yaml delete mode 100644 poc/flutter_rust_bridge_hello/integration_test/simple_test.dart delete mode 100644 poc/flutter_rust_bridge_hello/ios/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/ios/Flutter/AppFrameworkInfo.plist delete mode 100644 poc/flutter_rust_bridge_hello/ios/Flutter/Debug.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/ios/Flutter/Release.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.pbxproj delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/AppDelegate.swift delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/Main.storyboard delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Info.plist delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/Runner-Bridging-Header.h delete mode 100644 poc/flutter_rust_bridge_hello/ios/Runner/SceneDelegate.swift delete mode 100644 poc/flutter_rust_bridge_hello/ios/RunnerTests/RunnerTests.swift delete mode 100644 poc/flutter_rust_bridge_hello/lib/main.dart delete mode 100644 poc/flutter_rust_bridge_hello/lib/src/rust/api/simple.dart delete mode 100644 poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.dart delete mode 100644 poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.io.dart delete mode 100644 poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.web.dart delete mode 100644 poc/flutter_rust_bridge_hello/linux/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/linux/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/linux/flutter/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/linux/flutter/generated_plugins.cmake delete mode 100644 poc/flutter_rust_bridge_hello/linux/runner/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/linux/runner/main.cc delete mode 100644 poc/flutter_rust_bridge_hello/linux/runner/my_application.cc delete mode 100644 poc/flutter_rust_bridge_hello/linux/runner/my_application.h delete mode 100644 poc/flutter_rust_bridge_hello/macos/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Release.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.pbxproj delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/AppDelegate.swift delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Configs/Debug.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Configs/Release.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Configs/Warnings.xcconfig delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/DebugProfile.entitlements delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Info.plist delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/MainFlutterWindow.swift delete mode 100644 poc/flutter_rust_bridge_hello/macos/Runner/Release.entitlements delete mode 100644 poc/flutter_rust_bridge_hello/macos/RunnerTests/RunnerTests.swift delete mode 100644 poc/flutter_rust_bridge_hello/pubspec.lock delete mode 100644 poc/flutter_rust_bridge_hello/pubspec.yaml delete mode 100644 poc/flutter_rust_bridge_hello/rust/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/rust/Cargo.lock delete mode 100644 poc/flutter_rust_bridge_hello/rust/Cargo.toml delete mode 100644 poc/flutter_rust_bridge_hello/rust/src/api/mod.rs delete mode 100644 poc/flutter_rust_bridge_hello/rust/src/api/simple.rs delete mode 100644 poc/flutter_rust_bridge_hello/rust/src/frb_generated.rs delete mode 100644 poc/flutter_rust_bridge_hello/rust/src/lib.rs delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/README.md delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/android/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/android/build.gradle delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/android/settings.gradle delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/android/src/main/AndroidManifest.xml delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/LICENSE delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/README delete mode 100755 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_pod.sh delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/README.md delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/analysis_options.yaml delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/bin/build_tool.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/build_tool.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/android_environment.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_pod.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_tool.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/builder.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/cargo.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/environment.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/logging.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/options.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/rustup.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/target.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/util.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.lock delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.yaml delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/cargokit.cmake delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/resolve_symlinks.ps1 delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/cargokit/gradle/plugin.gradle delete mode 100755 poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.cmd delete mode 100755 poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.sh delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/ios/Classes/dummy_file.c delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/ios/rust_lib_flutter_rust_bridge_hello.podspec delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/linux/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/macos/Classes/dummy_file.c delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/macos/rust_lib_flutter_rust_bridge_hello.podspec delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/pubspec.yaml delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/windows/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/rust_builder/windows/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/test/poc_verification_test.dart delete mode 100644 poc/flutter_rust_bridge_hello/test_driver/integration_test.dart delete mode 100644 poc/flutter_rust_bridge_hello/web/favicon.png delete mode 100644 poc/flutter_rust_bridge_hello/web/icons/Icon-192.png delete mode 100644 poc/flutter_rust_bridge_hello/web/icons/Icon-512.png delete mode 100644 poc/flutter_rust_bridge_hello/web/icons/Icon-maskable-192.png delete mode 100644 poc/flutter_rust_bridge_hello/web/icons/Icon-maskable-512.png delete mode 100644 poc/flutter_rust_bridge_hello/web/index.html delete mode 100644 poc/flutter_rust_bridge_hello/web/manifest.json delete mode 100644 poc/flutter_rust_bridge_hello/windows/.gitignore delete mode 100644 poc/flutter_rust_bridge_hello/windows/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/windows/flutter/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/windows/flutter/generated_plugins.cmake delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/CMakeLists.txt delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/Runner.rc delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/flutter_window.cpp delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/flutter_window.h delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/main.cpp delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/resource.h delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/resources/app_icon.ico delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/runner.exe.manifest delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/utils.cpp delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/utils.h delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/win32_window.cpp delete mode 100644 poc/flutter_rust_bridge_hello/windows/runner/win32_window.h delete mode 100644 poc/linux_voice_test/Cargo.lock delete mode 100644 poc/linux_voice_test/Cargo.toml delete mode 100644 poc/linux_voice_test/README.md delete mode 100644 poc/linux_voice_test/VERIFICATION.md delete mode 100644 poc/linux_voice_test/src/audio_buffer.rs delete mode 100644 poc/linux_voice_test/src/linux_pipewire.rs delete mode 100644 poc/linux_voice_test/src/linux_pulseaudio.rs delete mode 100644 poc/linux_voice_test/src/linux_shared.rs delete mode 100644 poc/linux_voice_test/src/main.rs delete mode 100644 poc/linux_voice_test/src/windows_cpal.rs delete mode 100644 poc/secure-storage-spike/Cargo.lock delete mode 100644 poc/secure-storage-spike/Cargo.toml delete mode 100644 poc/secure-storage-spike/README.md delete mode 100644 poc/secure-storage-spike/VERIFICATION.md delete mode 100644 poc/secure-storage-spike/src/lib.rs delete mode 100644 poc/secure-storage-spike/src/main.rs delete mode 100644 poc/secure-storage-spike/src/secret.rs delete mode 100644 poc/secure-storage-spike/src/sqlite_repo.rs delete mode 100644 poc/secure-storage-spike/src/test_logger.rs delete mode 100644 poc/secure-storage-spike/tests/audit.rs delete mode 100644 poc/sqlite-storage-spike/Cargo.lock delete mode 100644 poc/sqlite-storage-spike/Cargo.toml delete mode 100644 poc/sqlite-storage-spike/README.md delete mode 100644 poc/sqlite-storage-spike/VERIFICATION.md delete mode 100644 poc/sqlite-storage-spike/src/lib.rs delete mode 100644 poc/sqlite-storage-spike/src/main.rs delete mode 100644 poc/sqlite-storage-spike/src/migrate.rs delete mode 100644 poc/sqlite-storage-spike/src/repo.rs delete mode 100644 poc/sqlite-storage-spike/tests/storage.rs delete mode 100644 poc/ts3-manual-research/README.md delete mode 100644 poc/ts3-manual-research/VERIFICATION.md delete mode 100644 poc/tsclientlib-channel-query-spike/.gitignore delete mode 100644 poc/tsclientlib-channel-query-spike/Cargo.lock delete mode 100644 poc/tsclientlib-channel-query-spike/Cargo.toml delete mode 100644 poc/tsclientlib-channel-query-spike/README.md delete mode 100644 poc/tsclientlib-channel-query-spike/VERIFICATION.md delete mode 100644 poc/tsclientlib-channel-query-spike/src/main.rs delete mode 100644 poc/tsclientlib-connect-spike/Cargo.lock delete mode 100644 poc/tsclientlib-connect-spike/Cargo.toml delete mode 100644 poc/tsclientlib-connect-spike/README.md delete mode 100644 poc/tsclientlib-connect-spike/VERIFICATION.md delete mode 100644 poc/tsclientlib-connect-spike/src/main.rs delete mode 100644 poc/tsclientlib-filetransfer-spike/.gitignore delete mode 100644 poc/tsclientlib-filetransfer-spike/Cargo.lock delete mode 100644 poc/tsclientlib-filetransfer-spike/Cargo.toml delete mode 100644 poc/tsclientlib-filetransfer-spike/README.md delete mode 100644 poc/tsclientlib-filetransfer-spike/VERIFICATION.md delete mode 100644 poc/tsclientlib-filetransfer-spike/src/main.rs delete mode 100644 poc/tsclientlib-query-spike/.gitignore delete mode 100644 poc/tsclientlib-query-spike/Cargo.lock delete mode 100644 poc/tsclientlib-query-spike/Cargo.toml delete mode 100644 poc/tsclientlib-query-spike/README.md delete mode 100644 poc/tsclientlib-query-spike/VERIFICATION.md delete mode 100644 poc/tsclientlib-query-spike/src/main.rs delete mode 100644 poc/tsidentity-import-spike/.gitignore delete mode 100644 poc/tsidentity-import-spike/Cargo.lock delete mode 100644 poc/tsidentity-import-spike/Cargo.toml delete mode 100644 poc/tsidentity-import-spike/README.md delete mode 100644 poc/tsidentity-import-spike/VERIFICATION.md delete mode 100644 poc/tsidentity-import-spike/src/main.rs delete mode 100755 tools/build-android-aab.sh delete mode 100755 tools/build-linux-deb.sh delete mode 100755 tools/dev-env.sh diff --git a/.github/workflows/bench-advisory.yml b/.github/workflows/bench-advisory.yml index c6ef433..265e698 100644 --- a/.github/workflows/bench-advisory.yml +++ b/.github/workflows/bench-advisory.yml @@ -25,8 +25,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - name: System deps (cpal / Opus) run: | sudo apt-get update diff --git a/.github/workflows/bench-baseline-update.yml b/.github/workflows/bench-baseline-update.yml index 59ba036..cdc04b2 100644 --- a/.github/workflows/bench-baseline-update.yml +++ b/.github/workflows/bench-baseline-update.yml @@ -18,8 +18,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - name: System deps (cpal / Opus) run: | sudo apt-get update diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fed4e98..030c1a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - name: System deps (cpal / Opus / SQLite) run: | sudo apt-get update @@ -40,8 +38,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check @@ -55,8 +51,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Install cargo-about @@ -119,71 +113,12 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - uses: subosito/flutter-action@v2 with: channel: stable - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - name: flutter pub get working-directory: apps/chanora_flutter run: flutter pub get - name: flutter build ios --no-codesign working-directory: apps/chanora_flutter run: flutter build ios --release --no-codesign - - flutter-linux-release-package: - name: flutter Linux release package (.deb) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - - name: System deps (Flutter Linux + audio + packaging) - run: | - sudo apt-get update - sudo apt-get install -y \ - clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev \ - libasound2-dev libpulse-dev libopus-dev libsdl2-dev - - uses: subosito/flutter-action@v2 - with: - channel: stable - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Build Linux .deb package - run: ./tools/build-linux-deb.sh - - name: Upload Linux package artifact - uses: actions/upload-artifact@v4 - with: - name: chanora-linux-deb - path: dist/linux/*.deb - - flutter-android-release-aab: - name: flutter Android release AAB - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Checkout oboe-rs fork - run: git clone --depth 1 https://github.com/edisonjwa/oboe-rs.git ../oboe-rs - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - - uses: android-actions/setup-android@v3 - - uses: subosito/flutter-action@v2 - with: - channel: stable - - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-linux-android,x86_64-linux-android - - uses: Swatinem/rust-cache@v2 - - name: Install cargo-ndk - run: cargo install --locked cargo-ndk - - name: Build Android release AAB - run: ./tools/build-android-aab.sh - - name: Upload Android bundle artifact - uses: actions/upload-artifact@v4 - with: - name: chanora-android-aab - path: dist/android/*.aab diff --git a/.gitignore b/.gitignore index 54f51fa..f6947b5 100644 --- a/.gitignore +++ b/.gitignore @@ -70,16 +70,10 @@ temp/ # Upstream documentation snapshots (kept as reference, not project source) chanora-docs-repo-format-*.zip -# Local vendored-crate experiments. Product dependencies must use -# crates.io/git dependencies or explicitly committed source with -# license provenance. -/third_party/ - # Cargo **/Cargo.lock.bak # Flutter / Dart generated (but keep pubspec.lock for the PoC apps) -.flutter-home/ **/.dart_tool/ **/.flutter-plugins **/.flutter-plugins-dependencies diff --git a/Cargo.lock b/Cargo.lock index 93086bc..35cbf40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -121,16 +121,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "annotate-snippets" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" -dependencies = [ - "anstyle", - "unicode-width", -] - [[package]] name = "anstyle" version = "1.0.14" @@ -270,31 +260,18 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "annotate-snippets", - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.10.5", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.2", - "shlex", - "syn", -] - [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -390,25 +367,6 @@ 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 7.1.3", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -469,15 +427,13 @@ dependencies = [ "dispatch2", "futures-util", "jni 0.21.1", - "libpulse-binding", - "libpulse-simple-binding", "ndarray", "ndk-context", "oboe", "ort", - "pipewire", "rand 0.8.6", "rustfft", + "sdl2", "serde_json", "sonora", "thiserror 2.0.18", @@ -515,6 +471,7 @@ dependencies = [ "chanora_audio", "chanora_diagnostics", "chanora_protocol", + "chanora_server_prefetch", "chanora_state", "chanora_storage", "rusqlite", @@ -543,6 +500,7 @@ dependencies = [ "reqwest 0.13.3", "serde", "thiserror 2.0.18", + "time", "tokio", "tracing", "ts-bookkeeping", @@ -563,10 +521,21 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "chanora_server_prefetch" +version = "0.2.0-beta.1" +dependencies = [ + "chanora_resolver", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "chanora_state" version = "0.2.0-beta.1" dependencies = [ + "chanora_protocol", "thiserror 2.0.18", ] @@ -624,17 +593,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.9", -] - [[package]] name = "clap" version = "4.6.1" @@ -714,12 +672,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" - [[package]] name = "core-foundation" version = "0.9.4" @@ -752,7 +704,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -1117,7 +1069,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -1576,12 +1528,6 @@ dependencies = [ "time", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "group" version = "0.13.0" @@ -2203,16 +2149,6 @@ dependencies = [ "pkg-config", ] -[[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 = "libloading" version = "0.9.0" @@ -2223,81 +2159,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "libpulse-binding" -version = "2.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" -dependencies = [ - "bitflags", - "libc", - "libpulse-sys", - "num-derive", - "num-traits", - "winapi", -] - -[[package]] -name = "libpulse-simple-binding" -version = "2.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7bebef0381c8e3e4b23cc24aaf36fab37472bece128de96f6a111efa464cfef" -dependencies = [ - "libpulse-binding", - "libpulse-simple-sys", - "libpulse-sys", -] - -[[package]] -name = "libpulse-simple-sys" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd96888fe37ad270d16abf5e82cccca1424871cf6afa2861824d2a52758eebc" -dependencies = [ - "libpulse-sys", - "pkg-config", -] - -[[package]] -name = "libpulse-sys" -version = "1.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74371848b22e989f829cc1621d2ebd74960711557d8b45cfe740f60d0a05e61" -dependencies = [ - "libc", - "num-derive", - "num-traits", - "pkg-config", - "winapi", -] - -[[package]] -name = "libspa" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2909f3be29d674e7f10604aff18d1bbe1bb03c4cd61c8a8ba19c0b1d162f7d4e" -dependencies = [ - "bitflags", - "cc", - "cookie-factory", - "libc", - "libspa-sys", - "nom 8.0.0", - "rustix", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ad52764fca54818486f3cf75afec844d1f1a1568c24dcee25d41b1ab007dda" -dependencies = [ - "bindgen", - "cc", - "system-deps", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -2315,7 +2176,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", ] @@ -2498,7 +2359,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags", + "bitflags 2.11.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2531,15 +2392,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2650,7 +2502,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "objc2", "objc2-core-audio", @@ -2688,7 +2540,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags", + "bitflags 2.11.1", "objc2", ] @@ -2698,7 +2550,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.11.1", "block2", "dispatch2", "libc", @@ -2717,7 +2569,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -2736,6 +2588,7 @@ dependencies = [ [[package]] name = "oboe" version = "0.6.3" +source = "git+https://github.com/EdisonJwa/oboe-rs?rev=a14f9b83ecea8c93f5a692f2ee7808445b938c35#a14f9b83ecea8c93f5a692f2ee7808445b938c35" dependencies = [ "num-derive", "num-traits", @@ -2745,6 +2598,7 @@ dependencies = [ [[package]] name = "oboe-sys" version = "0.6.3" +source = "git+https://github.com/EdisonJwa/oboe-rs?rev=a14f9b83ecea8c93f5a692f2ee7808445b938c35#a14f9b83ecea8c93f5a692f2ee7808445b938c35" dependencies = [ "cc", ] @@ -2786,7 +2640,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", @@ -2839,7 +2693,7 @@ version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" dependencies = [ - "libloading 0.9.0", + "libloading", "ndarray", "ort-sys", "smallvec", @@ -2931,31 +2785,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pipewire" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8585aba8a52ad74ccc633b8e293c1dc4277976bd5d510b925533f34fd6685f38" -dependencies = [ - "bitflags", - "libc", - "libspa", - "libspa-sys", - "pipewire-sys", - "rustix", -] - -[[package]] -name = "pipewire-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2089f245b548723e60325773c27f586b7a2372c79ea941b246cd0d654706adc" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -3303,7 +3132,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -3470,7 +3299,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags", + "bitflags 2.11.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3525,7 +3354,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -3644,6 +3473,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdl2" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + [[package]] name = "sec1" version = "0.7.3" @@ -3664,7 +3516,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3677,7 +3529,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4032,7 +3884,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4047,26 +3899,13 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - [[package]] name = "t4rust-derive" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e411692d6e0c8ce07f65a8210e95dcfb56cc4600940456592a49a3579ef6396" dependencies = [ - "nom 7.1.3", + "nom", "proc-macro2", "quote", "syn", @@ -4078,12 +3917,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - [[package]] name = "tempfile" version = "3.27.0" @@ -4371,7 +4204,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -4567,7 +4400,7 @@ version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" dependencies = [ "base64", - "bitflags", + "bitflags 2.11.1", "num-derive", "num-traits", "omnom", @@ -4595,7 +4428,7 @@ version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" dependencies = [ "base64", - "bitflags", + "bitflags 2.11.1", "curve25519-dalek-ng", "elliptic-curve", "generic-array", @@ -4638,12 +4471,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -4710,9 +4537,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.1" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" [[package]] name = "version_check" @@ -4846,7 +4673,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -4905,22 +4732,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" -[[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.11" @@ -4930,12 +4741,6 @@ dependencies = [ "windows-sys 0.61.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" version = "0.54.0" @@ -5381,7 +5186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 98b5235..64157ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,15 +12,15 @@ # crates/chanora_audio/ — capture, DSP, Opus, jitter, mixer # crates/chanora_storage/ — bookmarks, settings, identity refs # crates/chanora_diagnostics/ — logs, redaction, export +# crates/chanora_server_prefetch — server-resolution prefetch cache/policy # crates/chanora_bridge/ — Flutter/Rust typed DTOs + glue # # The Flutter app is not part of this Cargo workspace; it is owned by # Gradle / Xcode / Flutter tooling under apps/chanora_flutter/. # -# Non-promotion reminder (proof-of-concept-plan.md §4): nothing under -# poc/ is product code. The crates here are blank scaffolds at this -# stage; promotion of PoC code happens crate-by-crate with explicit -# audit-trail commits. +# Proof-of-concept spikes are archived separately in the private +# chanoraapp/chanora-poc repository. Promotion of PoC findings into +# product code happens crate-by-crate with explicit audit-trail commits. [workspace] resolver = "2" @@ -31,20 +31,12 @@ members = [ "crates/chanora_audio", "crates/chanora_storage", "crates/chanora_diagnostics", + "crates/chanora_server_prefetch", "crates/chanora_bridge", "crates/chanora_resolver", ] -# Excludes: the PoC trees stay outside the product workspace so that -# `cargo check --workspace` at the root does not pull in PoC builds. exclude = [ - "poc/tsclientlib-connect-spike", - "poc/secure-storage-spike", - "poc/sqlite-storage-spike", - "poc/diagnostics-redaction-spike", - "poc/audio-capture-playback-spike", - "poc/audio-capture-playback-android-spike", - "poc/flutter_rust_bridge_hello", "apps/chanora_flutter", ] diff --git a/about.toml b/about.toml index 781897c..3c62317 100644 --- a/about.toml +++ b/about.toml @@ -30,6 +30,7 @@ accepted = [ "Unicode-DFS-2016", "Unicode-3.0", "CC0-1.0", + "CDLA-Permissive-2.0", "OpenSSL", ] diff --git a/apps/chanora_flutter/android/app/build.gradle.kts b/apps/chanora_flutter/android/app/build.gradle.kts index adcaa58..886d22b 100644 --- a/apps/chanora_flutter/android/app/build.gradle.kts +++ b/apps/chanora_flutter/android/app/build.gradle.kts @@ -352,22 +352,7 @@ fun registerCargoNdkBuildTask(profile: String): TaskProvider<*> { ).firstOrNull { File(it).resolve("cargo").exists() } val rustupHome = System.getenv("RUSTUP_HOME") ?: "$homeDir/.rustup" val cargoHome = System.getenv("CARGO_HOME") ?: "$homeDir/.cargo" - val hostArch = System.getProperty("os.arch").lowercase() - val defaultRustupToolchain = when { - org.gradle.internal.os.OperatingSystem.current().isMacOsX && hostArch.contains("aarch64") -> - "stable-aarch64-apple-darwin" - org.gradle.internal.os.OperatingSystem.current().isMacOsX -> - "stable-x86_64-apple-darwin" - org.gradle.internal.os.OperatingSystem.current().isWindows && hostArch.contains("aarch64") -> - "stable-aarch64-pc-windows-msvc" - org.gradle.internal.os.OperatingSystem.current().isWindows -> - "stable-x86_64-pc-windows-msvc" - hostArch.contains("aarch64") || hostArch.contains("arm64") -> - "stable-aarch64-unknown-linux-gnu" - else -> - "stable-x86_64-unknown-linux-gnu" - } - val rustupToolchain = System.getenv("RUSTUP_TOOLCHAIN") ?: defaultRustupToolchain + val rustupToolchain = System.getenv("RUSTUP_TOOLCHAIN") ?: "stable-aarch64-apple-darwin" // SDD-118 item 13 (corrected): per-ABI Exec sub-tasks. Each runs an // isolated `cargo ndk -t ... -- build ...` so ANDROID_ABI is set @@ -553,7 +538,7 @@ fun registerJniLibsCopyTask(profile: String): TaskProvider { // version bump replacing libc++_shared.so). inputs.file(bridgeSo).withPropertyName("bridgeSo_$abi") inputs.file(cxxSharedSo).withPropertyName("cxxSharedSo_$abi") - inputs.file(llvmStrip).withPropertyName("llvmStrip") + inputs.file(llvmStrip).withPropertyName("llvmStrip_$abi") outputs.file(strippedBridgeSo) outputs.file(File(jniLibsDir, "$abi/libchanora_bridge.so")) outputs.file(File(jniLibsDir, "$abi/libc++_shared.so")) diff --git a/apps/chanora_flutter/android/gradle.properties b/apps/chanora_flutter/android/gradle.properties index d5da727..fbee1d8 100644 --- a/apps/chanora_flutter/android/gradle.properties +++ b/apps/chanora_flutter/android/gradle.properties @@ -1,6 +1,2 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -# This builtInKotlin flag was added automatically by Flutter migrator -android.builtInKotlin=false -# This newDsl flag was added automatically by Flutter migrator -android.newDsl=false diff --git a/apps/chanora_flutter/ios/Runner/AppDelegate.swift b/apps/chanora_flutter/ios/Runner/AppDelegate.swift index 3865d3d..4d851b9 100644 --- a/apps/chanora_flutter/ios/Runner/AppDelegate.swift +++ b/apps/chanora_flutter/ios/Runner/AppDelegate.swift @@ -34,54 +34,32 @@ import AVFoundation let session = AVAudioSession.sharedInstance() try session.setCategory( .playAndRecord, - mode: .default, - // Mode rationale (re-revisited after the "low playback - // volume" investigation, May 2026): + mode: .voiceChat, + // Mode rationale (May 2026, .voiceChat reinstated): // - // We've cycled through .voiceChat -> .default -> .voiceChat - // -> .default. Final answer is .default with - // .defaultToSpeaker, driven by these findings: + // We previously used .default mode after discovering that + // .voiceChat routed output through iOS's in-call audio + // channel, which made speaker output barely audible. That + // bug was caused by cpal's RemoteIO unit binding to a stale + // physical transducer — after migrating to coreaudio-rs + + // kAudioUnitSubType_VoiceProcessingIO (see + // crates/chanora_audio/src/ios_voice_unit.rs) the route + // binding is correct under either mode because VPIO re-binds + // on overrideOutputAudioPort. // - // The earlier "speaker selector silent" bug under - // .voiceChat was caused by cpal's RemoteIO unit binding - // to a stale physical transducer. After migrating to - // coreaudio-rs + kAudioUnitSubType_VoiceProcessingIO - // (see crates/chanora_audio/src/ios_voice_unit.rs) the - // route binding is correct under either mode because - // VPIO is the canonical voice unit and re-binds on - // overrideOutputAudioPort. So route switching is no - // longer a deciding factor. + // .voiceChat advantages over .default: + // * Tells iOS this is a VoIP session — other apps' audio + // is properly ducked/paused instead of competing. + // * Enables correct Bluetooth HFP negotiation without + // manual workarounds. + // * iOS treats the audio session as a "call" for priority + // purposes (won't be interrupted by notification sounds). + // * System-level CallKit integration (lock-screen controls). // - // The "broken playback quality" bug under either - // .voiceChat or .default (with VPIO) was actually NOT - // a VPIO problem at all. iOS has TWO independent audio - // channels: the in-call channel (used by .voiceChat / - // .videoChat modes) and the media channel (used by - // .default). The in-call channel: - // * Routes through the phone-call audio path - // * Aggressively ducks non-voice content to the - // earpiece (Apple's "speakerphone vs ear" UX) - // * Volume controlled by separate in-call volume - // hardware, not the side buttons (when not in a - // phone call) - // The media channel: - // * Routes through the standard media playback path - // * No automatic ducking - // * Volume controlled by the side volume buttons - // - // Even with VPIO + .voiceChat producing a perfectly - // good signal, iOS's in-call channel routing made it - // play at "earpiece" loudness on the speaker too \u2014 - // user-perceived as "broken and poor" because the - // signal is technically there but barely audible against - // the loud iPhone speaker's noise floor. - // - // Twilio's video-quickstart-ios and Daily.co's patched - // WebRTC both document the same workaround: use .default - // mode with .defaultToSpeaker option even when using - // VPIO for AEC. The VPIO unit itself still does its job - // (echo cancellation, noise suppression, AGC on the mic - // path) \u2014 only the playback routing changes. + // .defaultToSpeaker ensures output goes to the main speaker + // (not the earpiece) by default when no headphones are + // connected, compensating for the in-call channel's tendency + // to route to the earpiece. // // References: // * https://github.com/twilio/video-quickstart-ios/issues/522 @@ -91,9 +69,6 @@ import AVFoundation // .defaultToSpeaker : route output to the main speaker // (not the earpiece) by default // when no headphones are connected. - // This is what makes the audio - // actually audible at normal - // loudness. // .allowBluetoothHFP : permit Bluetooth Hands-Free // Profile headsets as both input // and output. @@ -245,7 +220,7 @@ import AVFoundation let session = AVAudioSession.sharedInstance() try session.setCategory( .playAndRecord, - mode: .default, + mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP] ) try session.setPreferredIOBufferDuration(0.02) diff --git a/apps/chanora_flutter/lib/design/platform_capabilities.dart b/apps/chanora_flutter/lib/design/platform_capabilities.dart index adfe376..5182578 100644 --- a/apps/chanora_flutter/lib/design/platform_capabilities.dart +++ b/apps/chanora_flutter/lib/design/platform_capabilities.dart @@ -171,7 +171,7 @@ List _windowsCapabilities() => [ List _linuxCapabilities() => [ const PlatformCapability( feature: 'Voice capture', - description: 'PipeWire voice I/O with PulseAudio fallback.', + description: 'PulseAudio/ALSA via cpal.', tier: CapabilityTier.supported, ), const PlatformCapability( diff --git a/apps/chanora_flutter/lib/l10n/app_en.arb b/apps/chanora_flutter/lib/l10n/app_en.arb index 804a5dc..4d40a52 100644 --- a/apps/chanora_flutter/lib/l10n/app_en.arb +++ b/apps/chanora_flutter/lib/l10n/app_en.arb @@ -22,6 +22,8 @@ "refreshAction": "Refresh", "diagnosticsAction": "Diagnostics", "diagnosticsSaveAction": "Save export", + "diagnosticsLiveUpdating": "Live updating", + "shareAction": "Share", "diagnosticsSaved": "Diagnostic export saved to {path}", "@diagnosticsSaved": { "placeholders": { "path": { "type": "String" } } @@ -39,6 +41,16 @@ "aboutThirdPartyBody": "Chanora is built on tsclientlib, flutter_rust_bridge, platform-native audio backends, rusqlite, and the Flutter framework, among others. See the NOTICE file at the repository root for the current attribution list.", "copyAction": "Copy", "closeAction": "Close", + "openAction": "Open", + "cancelAction": "Cancel", + "retryAction": "Retry", + "chatAction": "Chat", + "chatCloseAction": "Close chat", + "chatNewPrivateAction": "New private chat", + "chatSearchClientsHint": "Search clients...", + "chatDirectMessageAction": "Private message", + "chatPokeAction": "Poke", + "clientInfoAction": "Info", "startAudioAction": "Start audio", "pttHoldToTalk": "Hold to talk", "pttTransmitting": "Transmitting…", @@ -119,6 +131,7 @@ "channelsHeading": "Channels", "clientsHeading": "Online clients", + "serverWelcomeHeading": "Server welcome message", "countChannelsAndClients": "{channels} channels • {clients} online", "@countChannelsAndClients": { "placeholders": { @@ -172,6 +185,10 @@ "microphonePermissionBody": "Chanora needs permission to access the microphone. On macOS, go to System Settings → Privacy & Security → Microphone and enable Chanora.", "microphonePermissionRequiredForVoice": "Microphone permission is required for voice transmission.", "permissionGrantAction": "Grant", + "startupPermissionsTitle": "Permissions", + "startupPermissionsBody": "Chanora requests microphone, Bluetooth headset, and notification permissions at startup so voice, headset routing, and the foreground session work correctly.", + "startupPermissionsNotNow": "Not now", + "startupPermissionsAllow": "Allow", "audioRouteSystemDefault": "System default", "audioRouteEarpiece": "Earpiece", "audioRouteUsbHeadset": "USB headset", @@ -181,6 +198,47 @@ "audioRouteChangeFailed": "Could not change audio output.", "iosAudioInterrupted": "Audio interrupted by system (phone call)", "iosAudioResuming": "Audio resuming", + "linkTrustTitle": "Open external link?", + "linkTrustBody": "You are about to open a link to:\n\n{domain}", + "@linkTrustBody": { + "placeholders": { + "domain": { "type": "String" } + } + }, + "linkTrustRememberDomain": "Trust all links from this domain", + "clientInfoFetchingProfile": "Fetching TeamSpeak profile", + "clientInfoLoadingProfile": "Loading profile...", + "clientInfoProfileUnavailable": "Profile unavailable", + "clientInfoProfileUnavailableBody": "The server did not return profile details for this client.", + "clientInfoIdentitySection": "Identity", + "clientInfoMembershipSection": "Membership", + "clientInfoConnectionSection": "Connection", + "clientInfoHistorySection": "History", + "clientInfoTransferSection": "Transfer", + "clientInfoClientId": "Client ID", + "clientInfoDatabaseId": "Database ID", + "clientInfoUniqueId": "Unique ID", + "clientInfoDescription": "Description", + "clientInfoAvatar": "Avatar", + "clientInfoServerGroups": "Server groups", + "clientInfoChannelGroup": "Channel group", + "clientInfoChannelId": "Channel ID", + "clientInfoOnline": "Online", + "clientInfoIdle": "Idle", + "clientInfoPing": "Ping", + "clientInfoAddress": "Address", + "clientInfoPacketLossClientToServer": "Packet loss C->S", + "clientInfoPacketLossServerToClient": "Packet loss S->C", + "clientInfoFirstConnected": "First connected", + "clientInfoLastConnected": "Last connected", + "clientInfoConnections": "Connections", + "clientInfoDownloadedMonth": "Downloaded this month", + "clientInfoUploadedMonth": "Uploaded this month", + "clientInfoDownloadedTotal": "Downloaded total", + "clientInfoUploadedTotal": "Uploaded total", + "clientInfoUnknown": "Unknown", + "clientInfoHidden": "Hidden", + "clientInfoNone": "None", "pokeSnackBarClearAction": "Clear", "pokeSnackBarMoreIndicator": "...", "pokeSnackBarIncomingNoMessage": "{sender} pokes you", diff --git a/apps/chanora_flutter/lib/l10n/app_zh.arb b/apps/chanora_flutter/lib/l10n/app_zh.arb index 1b4cf9e..01a871a 100644 --- a/apps/chanora_flutter/lib/l10n/app_zh.arb +++ b/apps/chanora_flutter/lib/l10n/app_zh.arb @@ -21,6 +21,8 @@ "refreshAction": "刷新", "diagnosticsAction": "诊断信息", "diagnosticsSaveAction": "保存导出", + "diagnosticsLiveUpdating": "实时更新中", + "shareAction": "分享", "diagnosticsSaved": "诊断导出已保存到 {path}", "aboutAction": "关于", "aboutVersion": "版本 {version}", @@ -32,6 +34,16 @@ "aboutThirdPartyBody": "Chanora 基于 tsclientlib、flutter_rust_bridge、平台原生音频后端、rusqlite、Flutter 框架等开源组件构建。完整归属信息请参阅仓库根目录的 NOTICE 文件。", "copyAction": "复制", "closeAction": "关闭", + "openAction": "打开", + "cancelAction": "取消", + "retryAction": "重试", + "chatAction": "聊天", + "chatCloseAction": "关闭聊天", + "chatNewPrivateAction": "新建私聊", + "chatSearchClientsHint": "搜索用户...", + "chatDirectMessageAction": "私聊", + "chatPokeAction": "戳一下", + "clientInfoAction": "信息", "startAudioAction": "启动语音", "pttHoldToTalk": "按住说话", "pttTransmitting": "正在发送…", @@ -79,6 +91,7 @@ "channelsHeading": "频道", "clientsHeading": "在线用户", + "serverWelcomeHeading": "服务器欢迎信息", "countChannelsAndClients": "{channels} 个频道 • {clients} 在线", "voiceModePtt": "按键说话", @@ -126,6 +139,10 @@ "microphonePermissionBody": "Chanora 需要麦克风访问权限。请前往系统设置 → 隐私与安全性 → 麦克风,启用 Chanora。", "microphonePermissionRequiredForVoice": "语音发送需要麦克风权限。", "permissionGrantAction": "授权", + "startupPermissionsTitle": "权限", + "startupPermissionsBody": "Chanora 会在启动时请求麦克风、蓝牙耳机和通知权限,以确保语音、耳机路由和前台会话正常工作。", + "startupPermissionsNotNow": "暂不", + "startupPermissionsAllow": "允许", "audioRouteSystemDefault": "系统默认", "audioRouteEarpiece": "听筒", "audioRouteUsbHeadset": "USB 耳机", @@ -135,6 +152,42 @@ "audioRouteChangeFailed": "无法切换音频输出。", "iosAudioInterrupted": "系统已中断音频(电话通话)", "iosAudioResuming": "音频正在恢复", + "linkTrustTitle": "打开外部链接?", + "linkTrustBody": "你将打开指向以下域名的链接:\n\n{domain}", + "linkTrustRememberDomain": "信任来自此域名的所有链接", + "clientInfoFetchingProfile": "正在获取 TeamSpeak 资料", + "clientInfoLoadingProfile": "正在加载资料…", + "clientInfoProfileUnavailable": "资料不可用", + "clientInfoProfileUnavailableBody": "服务器没有返回此用户的资料详情。", + "clientInfoIdentitySection": "身份", + "clientInfoMembershipSection": "成员关系", + "clientInfoConnectionSection": "连接", + "clientInfoHistorySection": "历史", + "clientInfoTransferSection": "传输", + "clientInfoClientId": "用户 ID", + "clientInfoDatabaseId": "数据库 ID", + "clientInfoUniqueId": "唯一 ID", + "clientInfoDescription": "描述", + "clientInfoAvatar": "头像", + "clientInfoServerGroups": "服务器组", + "clientInfoChannelGroup": "频道组", + "clientInfoChannelId": "频道 ID", + "clientInfoOnline": "在线时长", + "clientInfoIdle": "空闲", + "clientInfoPing": "延迟", + "clientInfoAddress": "地址", + "clientInfoPacketLossClientToServer": "丢包 C->S", + "clientInfoPacketLossServerToClient": "丢包 S->C", + "clientInfoFirstConnected": "首次连接", + "clientInfoLastConnected": "上次连接", + "clientInfoConnections": "连接次数", + "clientInfoDownloadedMonth": "本月下载", + "clientInfoUploadedMonth": "本月上传", + "clientInfoDownloadedTotal": "总下载", + "clientInfoUploadedTotal": "总上传", + "clientInfoUnknown": "未知", + "clientInfoHidden": "隐藏", + "clientInfoNone": "无", "pokeSnackBarClearAction": "清除", "pokeSnackBarMoreIndicator": "...", "pokeSnackBarIncomingNoMessage": "{sender} 戳了你一下", diff --git a/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart b/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart index 38eca27..c9c5a0b 100644 --- a/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart +++ b/apps/chanora_flutter/lib/l10n/generated/app_localizations.dart @@ -199,6 +199,18 @@ abstract class AppL10n { /// **'Save export'** String get diagnosticsSaveAction; + /// No description provided for @diagnosticsLiveUpdating. + /// + /// In en, this message translates to: + /// **'Live updating'** + String get diagnosticsLiveUpdating; + + /// No description provided for @shareAction. + /// + /// In en, this message translates to: + /// **'Share'** + String get shareAction; + /// No description provided for @diagnosticsSaved. /// /// In en, this message translates to: @@ -265,6 +277,66 @@ abstract class AppL10n { /// **'Close'** String get closeAction; + /// No description provided for @openAction. + /// + /// In en, this message translates to: + /// **'Open'** + String get openAction; + + /// No description provided for @cancelAction. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancelAction; + + /// No description provided for @retryAction. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retryAction; + + /// No description provided for @chatAction. + /// + /// In en, this message translates to: + /// **'Chat'** + String get chatAction; + + /// No description provided for @chatCloseAction. + /// + /// In en, this message translates to: + /// **'Close chat'** + String get chatCloseAction; + + /// No description provided for @chatNewPrivateAction. + /// + /// In en, this message translates to: + /// **'New private chat'** + String get chatNewPrivateAction; + + /// No description provided for @chatSearchClientsHint. + /// + /// In en, this message translates to: + /// **'Search clients...'** + String get chatSearchClientsHint; + + /// No description provided for @chatDirectMessageAction. + /// + /// In en, this message translates to: + /// **'Private message'** + String get chatDirectMessageAction; + + /// No description provided for @chatPokeAction. + /// + /// In en, this message translates to: + /// **'Poke'** + String get chatPokeAction; + + /// No description provided for @clientInfoAction. + /// + /// In en, this message translates to: + /// **'Info'** + String get clientInfoAction; + /// No description provided for @startAudioAction. /// /// In en, this message translates to: @@ -523,6 +595,12 @@ abstract class AppL10n { /// **'Online clients'** String get clientsHeading; + /// No description provided for @serverWelcomeHeading. + /// + /// In en, this message translates to: + /// **'Server welcome message'** + String get serverWelcomeHeading; + /// No description provided for @countChannelsAndClients. /// /// In en, this message translates to: @@ -769,6 +847,30 @@ abstract class AppL10n { /// **'Grant'** String get permissionGrantAction; + /// No description provided for @startupPermissionsTitle. + /// + /// In en, this message translates to: + /// **'Permissions'** + String get startupPermissionsTitle; + + /// No description provided for @startupPermissionsBody. + /// + /// In en, this message translates to: + /// **'Chanora requests microphone, Bluetooth headset, and notification permissions at startup so voice, headset routing, and the foreground session work correctly.'** + String get startupPermissionsBody; + + /// No description provided for @startupPermissionsNotNow. + /// + /// In en, this message translates to: + /// **'Not now'** + String get startupPermissionsNotNow; + + /// No description provided for @startupPermissionsAllow. + /// + /// In en, this message translates to: + /// **'Allow'** + String get startupPermissionsAllow; + /// No description provided for @audioRouteSystemDefault. /// /// In en, this message translates to: @@ -823,6 +925,222 @@ abstract class AppL10n { /// **'Audio resuming'** String get iosAudioResuming; + /// No description provided for @linkTrustTitle. + /// + /// In en, this message translates to: + /// **'Open external link?'** + String get linkTrustTitle; + + /// No description provided for @linkTrustBody. + /// + /// In en, this message translates to: + /// **'You are about to open a link to:\n\n{domain}'** + String linkTrustBody(String domain); + + /// No description provided for @linkTrustRememberDomain. + /// + /// In en, this message translates to: + /// **'Trust all links from this domain'** + String get linkTrustRememberDomain; + + /// No description provided for @clientInfoFetchingProfile. + /// + /// In en, this message translates to: + /// **'Fetching TeamSpeak profile'** + String get clientInfoFetchingProfile; + + /// No description provided for @clientInfoLoadingProfile. + /// + /// In en, this message translates to: + /// **'Loading profile...'** + String get clientInfoLoadingProfile; + + /// No description provided for @clientInfoProfileUnavailable. + /// + /// In en, this message translates to: + /// **'Profile unavailable'** + String get clientInfoProfileUnavailable; + + /// No description provided for @clientInfoProfileUnavailableBody. + /// + /// In en, this message translates to: + /// **'The server did not return profile details for this client.'** + String get clientInfoProfileUnavailableBody; + + /// No description provided for @clientInfoIdentitySection. + /// + /// In en, this message translates to: + /// **'Identity'** + String get clientInfoIdentitySection; + + /// No description provided for @clientInfoMembershipSection. + /// + /// In en, this message translates to: + /// **'Membership'** + String get clientInfoMembershipSection; + + /// No description provided for @clientInfoConnectionSection. + /// + /// In en, this message translates to: + /// **'Connection'** + String get clientInfoConnectionSection; + + /// No description provided for @clientInfoHistorySection. + /// + /// In en, this message translates to: + /// **'History'** + String get clientInfoHistorySection; + + /// No description provided for @clientInfoTransferSection. + /// + /// In en, this message translates to: + /// **'Transfer'** + String get clientInfoTransferSection; + + /// No description provided for @clientInfoClientId. + /// + /// In en, this message translates to: + /// **'Client ID'** + String get clientInfoClientId; + + /// No description provided for @clientInfoDatabaseId. + /// + /// In en, this message translates to: + /// **'Database ID'** + String get clientInfoDatabaseId; + + /// No description provided for @clientInfoUniqueId. + /// + /// In en, this message translates to: + /// **'Unique ID'** + String get clientInfoUniqueId; + + /// No description provided for @clientInfoDescription. + /// + /// In en, this message translates to: + /// **'Description'** + String get clientInfoDescription; + + /// No description provided for @clientInfoAvatar. + /// + /// In en, this message translates to: + /// **'Avatar'** + String get clientInfoAvatar; + + /// No description provided for @clientInfoServerGroups. + /// + /// In en, this message translates to: + /// **'Server groups'** + String get clientInfoServerGroups; + + /// No description provided for @clientInfoChannelGroup. + /// + /// In en, this message translates to: + /// **'Channel group'** + String get clientInfoChannelGroup; + + /// No description provided for @clientInfoChannelId. + /// + /// In en, this message translates to: + /// **'Channel ID'** + String get clientInfoChannelId; + + /// No description provided for @clientInfoOnline. + /// + /// In en, this message translates to: + /// **'Online'** + String get clientInfoOnline; + + /// No description provided for @clientInfoIdle. + /// + /// In en, this message translates to: + /// **'Idle'** + String get clientInfoIdle; + + /// No description provided for @clientInfoPing. + /// + /// In en, this message translates to: + /// **'Ping'** + String get clientInfoPing; + + /// No description provided for @clientInfoAddress. + /// + /// In en, this message translates to: + /// **'Address'** + String get clientInfoAddress; + + /// No description provided for @clientInfoPacketLossClientToServer. + /// + /// In en, this message translates to: + /// **'Packet loss C->S'** + String get clientInfoPacketLossClientToServer; + + /// No description provided for @clientInfoPacketLossServerToClient. + /// + /// In en, this message translates to: + /// **'Packet loss S->C'** + String get clientInfoPacketLossServerToClient; + + /// No description provided for @clientInfoFirstConnected. + /// + /// In en, this message translates to: + /// **'First connected'** + String get clientInfoFirstConnected; + + /// No description provided for @clientInfoLastConnected. + /// + /// In en, this message translates to: + /// **'Last connected'** + String get clientInfoLastConnected; + + /// No description provided for @clientInfoConnections. + /// + /// In en, this message translates to: + /// **'Connections'** + String get clientInfoConnections; + + /// No description provided for @clientInfoDownloadedMonth. + /// + /// In en, this message translates to: + /// **'Downloaded this month'** + String get clientInfoDownloadedMonth; + + /// No description provided for @clientInfoUploadedMonth. + /// + /// In en, this message translates to: + /// **'Uploaded this month'** + String get clientInfoUploadedMonth; + + /// No description provided for @clientInfoDownloadedTotal. + /// + /// In en, this message translates to: + /// **'Downloaded total'** + String get clientInfoDownloadedTotal; + + /// No description provided for @clientInfoUploadedTotal. + /// + /// In en, this message translates to: + /// **'Uploaded total'** + String get clientInfoUploadedTotal; + + /// No description provided for @clientInfoUnknown. + /// + /// In en, this message translates to: + /// **'Unknown'** + String get clientInfoUnknown; + + /// No description provided for @clientInfoHidden. + /// + /// In en, this message translates to: + /// **'Hidden'** + String get clientInfoHidden; + + /// No description provided for @clientInfoNone. + /// + /// In en, this message translates to: + /// **'None'** + String get clientInfoNone; + /// No description provided for @pokeSnackBarClearAction. /// /// In en, this message translates to: diff --git a/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart b/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart index 89fec7c..9d210c0 100644 --- a/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart +++ b/apps/chanora_flutter/lib/l10n/generated/app_localizations_en.dart @@ -62,6 +62,12 @@ class AppL10nEn extends AppL10n { @override String get diagnosticsSaveAction => 'Save export'; + @override + String get diagnosticsLiveUpdating => 'Live updating'; + + @override + String get shareAction => 'Share'; + @override String diagnosticsSaved(String path) { return 'Diagnostic export saved to $path'; @@ -102,6 +108,36 @@ class AppL10nEn extends AppL10n { @override String get closeAction => 'Close'; + @override + String get openAction => 'Open'; + + @override + String get cancelAction => 'Cancel'; + + @override + String get retryAction => 'Retry'; + + @override + String get chatAction => 'Chat'; + + @override + String get chatCloseAction => 'Close chat'; + + @override + String get chatNewPrivateAction => 'New private chat'; + + @override + String get chatSearchClientsHint => 'Search clients...'; + + @override + String get chatDirectMessageAction => 'Private message'; + + @override + String get chatPokeAction => 'Poke'; + + @override + String get clientInfoAction => 'Info'; + @override String get startAudioAction => 'Start audio'; @@ -256,6 +292,9 @@ class AppL10nEn extends AppL10n { @override String get clientsHeading => 'Online clients'; + @override + String get serverWelcomeHeading => 'Server welcome message'; + @override String countChannelsAndClients(int channels, int clients) { return '$channels channels • $clients online'; @@ -388,6 +427,19 @@ class AppL10nEn extends AppL10n { @override String get permissionGrantAction => 'Grant'; + @override + String get startupPermissionsTitle => 'Permissions'; + + @override + String get startupPermissionsBody => + 'Chanora requests microphone, Bluetooth headset, and notification permissions at startup so voice, headset routing, and the foreground session work correctly.'; + + @override + String get startupPermissionsNotNow => 'Not now'; + + @override + String get startupPermissionsAllow => 'Allow'; + @override String get audioRouteSystemDefault => 'System default'; @@ -415,6 +467,117 @@ class AppL10nEn extends AppL10n { @override String get iosAudioResuming => 'Audio resuming'; + @override + String get linkTrustTitle => 'Open external link?'; + + @override + String linkTrustBody(String domain) { + return 'You are about to open a link to:\n\n$domain'; + } + + @override + String get linkTrustRememberDomain => 'Trust all links from this domain'; + + @override + String get clientInfoFetchingProfile => 'Fetching TeamSpeak profile'; + + @override + String get clientInfoLoadingProfile => 'Loading profile...'; + + @override + String get clientInfoProfileUnavailable => 'Profile unavailable'; + + @override + String get clientInfoProfileUnavailableBody => + 'The server did not return profile details for this client.'; + + @override + String get clientInfoIdentitySection => 'Identity'; + + @override + String get clientInfoMembershipSection => 'Membership'; + + @override + String get clientInfoConnectionSection => 'Connection'; + + @override + String get clientInfoHistorySection => 'History'; + + @override + String get clientInfoTransferSection => 'Transfer'; + + @override + String get clientInfoClientId => 'Client ID'; + + @override + String get clientInfoDatabaseId => 'Database ID'; + + @override + String get clientInfoUniqueId => 'Unique ID'; + + @override + String get clientInfoDescription => 'Description'; + + @override + String get clientInfoAvatar => 'Avatar'; + + @override + String get clientInfoServerGroups => 'Server groups'; + + @override + String get clientInfoChannelGroup => 'Channel group'; + + @override + String get clientInfoChannelId => 'Channel ID'; + + @override + String get clientInfoOnline => 'Online'; + + @override + String get clientInfoIdle => 'Idle'; + + @override + String get clientInfoPing => 'Ping'; + + @override + String get clientInfoAddress => 'Address'; + + @override + String get clientInfoPacketLossClientToServer => 'Packet loss C->S'; + + @override + String get clientInfoPacketLossServerToClient => 'Packet loss S->C'; + + @override + String get clientInfoFirstConnected => 'First connected'; + + @override + String get clientInfoLastConnected => 'Last connected'; + + @override + String get clientInfoConnections => 'Connections'; + + @override + String get clientInfoDownloadedMonth => 'Downloaded this month'; + + @override + String get clientInfoUploadedMonth => 'Uploaded this month'; + + @override + String get clientInfoDownloadedTotal => 'Downloaded total'; + + @override + String get clientInfoUploadedTotal => 'Uploaded total'; + + @override + String get clientInfoUnknown => 'Unknown'; + + @override + String get clientInfoHidden => 'Hidden'; + + @override + String get clientInfoNone => 'None'; + @override String get pokeSnackBarClearAction => 'Clear'; diff --git a/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart b/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart index debbc4a..17c4b07 100644 --- a/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart +++ b/apps/chanora_flutter/lib/l10n/generated/app_localizations_zh.dart @@ -59,6 +59,12 @@ class AppL10nZh extends AppL10n { @override String get diagnosticsSaveAction => '保存导出'; + @override + String get diagnosticsLiveUpdating => '实时更新中'; + + @override + String get shareAction => '分享'; + @override String diagnosticsSaved(String path) { return '诊断导出已保存到 $path'; @@ -99,6 +105,36 @@ class AppL10nZh extends AppL10n { @override String get closeAction => '关闭'; + @override + String get openAction => '打开'; + + @override + String get cancelAction => '取消'; + + @override + String get retryAction => '重试'; + + @override + String get chatAction => '聊天'; + + @override + String get chatCloseAction => '关闭聊天'; + + @override + String get chatNewPrivateAction => '新建私聊'; + + @override + String get chatSearchClientsHint => '搜索用户...'; + + @override + String get chatDirectMessageAction => '私聊'; + + @override + String get chatPokeAction => '戳一下'; + + @override + String get clientInfoAction => '信息'; + @override String get startAudioAction => '启动语音'; @@ -249,6 +285,9 @@ class AppL10nZh extends AppL10n { @override String get clientsHeading => '在线用户'; + @override + String get serverWelcomeHeading => '服务器欢迎信息'; + @override String countChannelsAndClients(int channels, int clients) { return '$channels 个频道 • $clients 在线'; @@ -378,6 +417,19 @@ class AppL10nZh extends AppL10n { @override String get permissionGrantAction => '授权'; + @override + String get startupPermissionsTitle => '权限'; + + @override + String get startupPermissionsBody => + 'Chanora 会在启动时请求麦克风、蓝牙耳机和通知权限,以确保语音、耳机路由和前台会话正常工作。'; + + @override + String get startupPermissionsNotNow => '暂不'; + + @override + String get startupPermissionsAllow => '允许'; + @override String get audioRouteSystemDefault => '系统默认'; @@ -405,6 +457,116 @@ class AppL10nZh extends AppL10n { @override String get iosAudioResuming => '音频正在恢复'; + @override + String get linkTrustTitle => '打开外部链接?'; + + @override + String linkTrustBody(String domain) { + return '你将打开指向以下域名的链接:\n\n$domain'; + } + + @override + String get linkTrustRememberDomain => '信任来自此域名的所有链接'; + + @override + String get clientInfoFetchingProfile => '正在获取 TeamSpeak 资料'; + + @override + String get clientInfoLoadingProfile => '正在加载资料…'; + + @override + String get clientInfoProfileUnavailable => '资料不可用'; + + @override + String get clientInfoProfileUnavailableBody => '服务器没有返回此用户的资料详情。'; + + @override + String get clientInfoIdentitySection => '身份'; + + @override + String get clientInfoMembershipSection => '成员关系'; + + @override + String get clientInfoConnectionSection => '连接'; + + @override + String get clientInfoHistorySection => '历史'; + + @override + String get clientInfoTransferSection => '传输'; + + @override + String get clientInfoClientId => '用户 ID'; + + @override + String get clientInfoDatabaseId => '数据库 ID'; + + @override + String get clientInfoUniqueId => '唯一 ID'; + + @override + String get clientInfoDescription => '描述'; + + @override + String get clientInfoAvatar => '头像'; + + @override + String get clientInfoServerGroups => '服务器组'; + + @override + String get clientInfoChannelGroup => '频道组'; + + @override + String get clientInfoChannelId => '频道 ID'; + + @override + String get clientInfoOnline => '在线时长'; + + @override + String get clientInfoIdle => '空闲'; + + @override + String get clientInfoPing => '延迟'; + + @override + String get clientInfoAddress => '地址'; + + @override + String get clientInfoPacketLossClientToServer => '丢包 C->S'; + + @override + String get clientInfoPacketLossServerToClient => '丢包 S->C'; + + @override + String get clientInfoFirstConnected => '首次连接'; + + @override + String get clientInfoLastConnected => '上次连接'; + + @override + String get clientInfoConnections => '连接次数'; + + @override + String get clientInfoDownloadedMonth => '本月下载'; + + @override + String get clientInfoUploadedMonth => '本月上传'; + + @override + String get clientInfoDownloadedTotal => '总下载'; + + @override + String get clientInfoUploadedTotal => '总上传'; + + @override + String get clientInfoUnknown => '未知'; + + @override + String get clientInfoHidden => '隐藏'; + + @override + String get clientInfoNone => '无'; + @override String get pokeSnackBarClearAction => '清除'; diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 40eee16..48e56fc 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -20,10 +20,10 @@ import 'l10n/generated/app_localizations.dart'; import 'services/android_permissions_service.dart'; import 'services/app_bootstrap.dart'; import 'services/audio_lifecycle_service.dart'; -import 'services/back_intent_service.dart'; import 'services/channel_join_error_mapper.dart'; import 'services/connection_phase_state.dart'; import 'services/ios_permissions_service.dart'; +import 'services/prefetch_debouncer.dart'; import 'services/snapshot_state_mapper.dart'; import 'services/ts3_server_link.dart'; import 'services/ui_preferences_service.dart'; @@ -32,10 +32,10 @@ import 'src/rust/frb_generated.dart'; import 'widgets/permission_state_banner.dart'; import 'widgets/audio_processing_config_state.dart'; import 'widgets/chat_views.dart'; +import 'widgets/client_info_sheet.dart'; import 'widgets/connect_widgets.dart'; import 'widgets/input_dialogs.dart'; import 'widgets/snapshot_view.dart'; -import 'widgets/startup_dependency_screen.dart'; import 'widgets/voice_platform.dart'; import 'widgets/voice_bar.dart'; import 'widgets/voice_compact.dart'; @@ -92,11 +92,32 @@ Future _finishDeferredStartup() async { } catch (_) {} } -class ChanoraApp extends StatelessWidget { +class ChanoraApp extends StatefulWidget { const ChanoraApp({super.key}); - static final GlobalKey navigatorKey = - GlobalKey(); + @override + State createState() => _ChanoraAppState(); +} + +class _ChanoraAppState extends State { + final UiPreferencesService _uiPreferences = const UiPreferencesService(); + ThemeMode _themeMode = ThemeMode.system; + + @override + void initState() { + super.initState(); + unawaited(_loadThemeMode()); + } + + Future _loadThemeMode() async { + try { + final settings = await _uiPreferences.loadSettings(); + if (!mounted) return; + setState(() { + _themeMode = settings.themeMode.toFlutterThemeMode(); + }); + } catch (_) {} + } @override Widget build(BuildContext context) { @@ -119,17 +140,147 @@ class ChanoraApp extends StatelessWidget { colorSchemeSeed: const Color(0xFF3F51B5), scaffoldBackgroundColor: _appSurfaceColor, ), - navigatorKey: navigatorKey, + darkTheme: ThemeData( + useMaterial3: true, + colorSchemeSeed: const Color(0xFF3F51B5), + brightness: Brightness.dark, + ), + themeMode: _themeMode, localizationsDelegates: AppL10n.localizationsDelegates, supportedLocales: AppL10n.supportedLocales, - home: const StartupDependencyGate(child: _BetaHome()), + home: _BetaHome( + themeMode: _themeMode, + onThemeModeChanged: _setThemeMode, + ), ), ); } + + Future _setThemeMode(ThemeMode themeMode) async { + setState(() { + _themeMode = themeMode; + }); + try { + await _uiPreferences.saveThemeMode(themeMode.toUiThemeMode()); + } catch (_) {} + } +} + +extension on UiThemeMode { + ThemeMode toFlutterThemeMode() { + return switch (this) { + UiThemeMode.system => ThemeMode.system, + UiThemeMode.light => ThemeMode.light, + UiThemeMode.dark => ThemeMode.dark, + }; + } +} + +extension on ThemeMode { + UiThemeMode toUiThemeMode() { + return switch (this) { + ThemeMode.system => UiThemeMode.system, + ThemeMode.light => UiThemeMode.light, + ThemeMode.dark => UiThemeMode.dark, + }; + } +} + +class ChanoraThemeModeMenu extends StatelessWidget { + const ChanoraThemeModeMenu({ + super.key, + required this.themeMode, + required this.onThemeModeChanged, + }); + + final ThemeMode themeMode; + final Future Function(ThemeMode mode) onThemeModeChanged; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + tooltip: 'Theme', + icon: const Icon(Icons.palette_outlined), + initialValue: themeMode, + onSelected: (mode) { + unawaited(onThemeModeChanged(mode)); + }, + itemBuilder: (context) => const [ + PopupMenuItem(value: ThemeMode.system, child: Text('System')), + PopupMenuItem(value: ThemeMode.light, child: Text('Light')), + PopupMenuItem(value: ThemeMode.dark, child: Text('Dark')), + ], + ); + } +} + +class ChanoraMobileScaffold extends StatelessWidget { + const ChanoraMobileScaffold({ + super.key, + required this.compactIdleChrome, + required this.canDisconnect, + required this.title, + required this.actions, + required this.onDisconnect, + required this.compactHeader, + required this.body, + this.disconnectTooltip = 'Disconnect', + }); + + final bool compactIdleChrome; + final bool canDisconnect; + final Widget? title; + final List actions; + final VoidCallback onDisconnect; + final Widget compactHeader; + final Widget body; + final String disconnectTooltip; + + @override + Widget build(BuildContext context) { + final paddedBody = SafeArea( + top: compactIdleChrome, + child: Padding( + padding: const EdgeInsets.all(16), + child: compactIdleChrome + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + compactHeader, + const SizedBox(height: 8), + Expanded(child: body), + ], + ) + : body, + ), + ); + + return Scaffold( + appBar: compactIdleChrome + ? null + : AppBar( + leading: canDisconnect + ? IconButton( + tooltip: disconnectTooltip, + icon: const Icon(Icons.arrow_back), + onPressed: onDisconnect, + ) + : null, + leadingWidth: canDisconnect ? 44 : null, + titleSpacing: canDisconnect ? 4 : null, + title: title, + actions: actions, + ), + body: paddedBody, + ); + } } class _BetaHome extends StatefulWidget { - const _BetaHome(); + const _BetaHome({required this.themeMode, required this.onThemeModeChanged}); + + final ThemeMode themeMode; + final Future Function(ThemeMode mode) onThemeModeChanged; @override State<_BetaHome> createState() => _BetaHomeState(); @@ -165,7 +316,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { bool _snapshotRefreshQueued = false; bool _snapshotRefreshQueuedRecordActivity = false; bool _snapshotRefreshQueuedReportErrors = false; + int _connectionEpoch = 0; StreamSubscription? _eventsSub; + late final PrefetchDebouncer _prefetch; // v1 voice subsystem state (SDD-094/095/096/097). Driven by // BridgeEvent::VoiceState. @@ -174,7 +327,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { bool _hardMute = false; bool _hardMuteByPermission = false; bool _hardMuteByTalkPower = false; - bool _hardMuteByOutputMute = false; bool _permissionHardMuteClearInFlight = false; int _releaseTailMs = 200; BigInt? _currentVoiceChannelId; @@ -206,14 +358,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { // only the platform-neutral label that already crossed into // the Rust side. String _pttBoundKeyLabel = ''; - final Set _focusedPttHeldInputs = {}; + final Set _focusedPttHeldKeys = {}; List _bookmarks = const []; final List _chatMessages = []; final ValueNotifier _chatFeedRevision = ValueNotifier(0); int _chatUnread = 0; bool _chatOpen = false; - bool _chatPreviewSnackBarVisible = false; final ValueNotifier> _pokeSnackBarPokes = ValueNotifier( const [], ); @@ -228,27 +379,14 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { AndroidPermissionsService(); final IosPermissionsService _iosPermissions = IosPermissionsService(); final UiPreferencesService _uiPreferences = const UiPreferencesService(); - Map _clientPlaybackPrefs = const {}; - final Map _appliedClientPlaybackVolumes = {}; - String _clientPlaybackPrefsHost = ''; - late final BackIntentService _backIntentService; - int _modalRouteDepth = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); HardwareKeyboard.instance.addHandler(_handleFocusedPttKey); - _backIntentService = !kIsWeb && Platform.isAndroid - ? BackIntentService( - pttActiveProbe: () => _audioStats?.pttActive ?? false, - modalOpenProbe: () => _modalRouteDepth > 0, - atRootProbe: () => !_chatOpen, - closeTopModal: _closeTopModalRoute, - popRoute: _popTopRoute, - ) - : BackIntentService.noOp(); - _backIntentService.start(); + _prefetch = PrefetchDebouncer(onPrefetch: _onPrefetchServer); + _hostCtl.addListener(_onHostEdited); _eventsSub = rust.eventsStream().listen(_onEvent); // SDD-106 §5: subscribe to Kotlin -> Dart permissionStateChanged // events as early as possible so the listen-only banner reflects @@ -269,38 +407,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { unawaited(_loadUiSettings()); } - Future _showTrackedDialog({ - required WidgetBuilder builder, - bool barrierDismissible = true, - }) async { - _modalRouteDepth += 1; - try { - return await showDialog( - context: context, - barrierDismissible: barrierDismissible, - builder: builder, - ); - } finally { - if (_modalRouteDepth > 0) { - _modalRouteDepth -= 1; - } - } - } - - void _closeTopModalRoute() { - final navigator = ChanoraApp.navigatorKey.currentState; - if (navigator == null || _modalRouteDepth == 0 || !navigator.canPop()) { - return; - } - navigator.pop(); - } - - void _popTopRoute() { - final navigator = ChanoraApp.navigatorKey.currentState; - if (navigator == null) return; - unawaited(navigator.maybePop()); - } - Future _loadUiSettings() async { try { final settings = await _uiPreferences.loadSettings(); @@ -310,139 +416,52 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { if (settings.nickname.isNotEmpty) { _nickCtl.text = settings.nickname; } + if (settings.host.isNotEmpty) { + _prefetch.schedule(settings.host); + } } catch (_) {} } + void _onHostEdited() { + _prefetch.schedule(_hostCtl.text); + } + + Future _onPrefetchServer(String host) async { + try { + await rust.prefetchServer(host: host); + } catch (e) { + _recordUiDiagnostic('prefetch server', e); + } + } + Future _saveUiSettings({String? host, String? nickname}) async { try { await _uiPreferences.saveSettings(host: host, nickname: nickname); } catch (_) {} } - Future _ensureClientPlaybackPreferencesLoadedForCurrentHost() async { - final host = _hostCtl.text.trim().toLowerCase(); - if (host.isEmpty) { - _clientPlaybackPrefsHost = ''; - _clientPlaybackPrefs = const {}; - _appliedClientPlaybackVolumes.clear(); - return; - } - if (_clientPlaybackPrefsHost == host) return; - _appliedClientPlaybackVolumes.clear(); - try { - _clientPlaybackPrefs = await _uiPreferences - .loadClientPlaybackPreferencesForServer(host); - _clientPlaybackPrefsHost = host; - } catch (_) { - _clientPlaybackPrefsHost = host; - _clientPlaybackPrefs = const {}; - } - } - - Future _applyClientPlaybackPreferencesToSnapshot( - rust.BridgeSnapshot snap, - ) async { - await _ensureClientPlaybackPreferencesLoadedForCurrentHost(); - if (_clientPlaybackPrefs.isEmpty) return; - for (final client in snap.clients) { - if (client.id == snap.ownClientId || - client.isServerQuery || - client.uid.isEmpty) { - continue; - } - final pref = _clientPlaybackPrefs[client.uid]; - if (pref == null) continue; - if (_appliedClientPlaybackVolumes[client.id] == pref.appliedVolume) { - continue; - } - try { - await rust.setClientVolume( - clientId: client.id, - volume: pref.appliedVolume, - ); - _appliedClientPlaybackVolumes[client.id] = pref.appliedVolume; - } catch (_) { - // Best-effort: queues may not exist yet for silent users. - } - } - } - - Future _setClientPlaybackPreference( - rust.BridgeClient client, - ClientPlaybackPreference preference, - ) async { - if (client.id == _snapshot?.ownClientId || - client.isServerQuery || - client.uid.isEmpty) { - return; - } - - final host = _hostCtl.text.trim().toLowerCase(); - if (host.isEmpty) return; - - await _ensureClientPlaybackPreferencesLoadedForCurrentHost(); - final nextPrefs = Map.from( - _clientPlaybackPrefs, - ); - if (preference.volume == 1.0 && !preference.muted) { - nextPrefs.remove(client.uid); - } else { - nextPrefs[client.uid] = preference; - } - - if (mounted) { - setState(() => _clientPlaybackPrefs = nextPrefs); - } else { - _clientPlaybackPrefs = nextPrefs; - } - - try { - await _uiPreferences.saveClientPlaybackPreference( - serverHost: host, - userUid: client.uid, - volume: preference.volume, - muted: preference.muted, - ); - } catch (error) { - _recordUiDiagnostic('save client playback preference', error); - } - - try { - await rust.setClientVolume( - clientId: client.id, - volume: preference.appliedVolume, - ); - _appliedClientPlaybackVolumes[client.id] = preference.appliedVolume; - } catch (error) { - _recordUiDiagnostic('set client volume', error); - } - } - Future _requestRecordAudioOnStartup() async { try { if (Platform.isAndroid) { final permsExplained = await _uiPreferences.hasExplainedPermissions(); if (!permsExplained) { if (!mounted) return; - await _showTrackedDialog( + await showDialog( + context: context, builder: (ctx) => AlertDialog( - title: const Text('Permissions'), - content: const Text( - 'Chanora requests microphone, Bluetooth headset, and ' - 'notification permissions at startup so voice, headset ' - 'routing, and the foreground session work correctly.', - ), + title: Text(AppL10n.of(ctx).startupPermissionsTitle), + content: Text(AppL10n.of(ctx).startupPermissionsBody), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: const Text('Not now'), + child: Text(AppL10n.of(ctx).startupPermissionsNotNow), ), FilledButton( onPressed: () { Navigator.pop(ctx); _androidPermissions.ensureStartupPermissions(); }, - child: const Text('Allow'), + child: Text(AppL10n.of(ctx).startupPermissionsAllow), ), ], ), @@ -486,10 +505,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { await rust.setHardMute(muted: false); if (!mounted || !_hardMuteByPermission) return; setState(() { + _hardMute = false; _hardMuteByPermission = false; - if (!_inputMuted && !_hardMuteByTalkPower && !_hardMuteByOutputMute) { - _hardMute = false; - } }); } catch (e) { if (!mounted) return; @@ -533,23 +550,24 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } bool _handleFocusedPttKey(KeyEvent event) { - final platformKey = - pttMouseSideButtonPlatformKeyForLogicalKey(event.logicalKey) ?? - pttDisplayLabelForKey(event.logicalKey); - if (platformKey == null) { - return false; - } - if (event is KeyUpEvent && _focusedPttHeldInputs.contains(platformKey)) { - if (_focusedPttHeldInputs.remove(platformKey)) { - unawaited(_setPtt(false, reportError: false)); + final label = pttDisplayLabelForKey(event.logicalKey); + final isBoundKey = + _pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel; + if (event is KeyUpEvent && _focusedPttHeldKeys.contains(event.logicalKey)) { + if (_focusedPttHeldKeys.remove(event.logicalKey)) { + unawaited(_setPtt(false)); } return true; } - if (!_canHandleFocusedPttPlatformKey(platformKey)) { + if (_pttBackendId != 'focused' || + !_serverReachable || + !_inChannel || + _transmitMode != rust.BridgeTransmitMode.ptt || + !isBoundKey) { return false; } if (event is KeyDownEvent) { - if (_focusedPttHeldInputs.add(platformKey)) { + if (_focusedPttHeldKeys.add(event.logicalKey)) { unawaited(_setPtt(true)); } return true; @@ -557,38 +575,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { return false; } - bool _canHandleFocusedPttPlatformKey(String platformKey) { - return _pttBackendId == 'focused' && - _serverReachable && - _inChannel && - _transmitMode == rust.BridgeTransmitMode.ptt && - _pttBoundKeyLabel.isNotEmpty && - platformKey == _pttBoundKeyLabel; - } - - void _handleFocusedPttPointerDown(PointerDownEvent event) { - final platformKey = pttMouseSideButtonPlatformKeyForButtons(event.buttons); - if (platformKey == null || !_canHandleFocusedPttPlatformKey(platformKey)) { - return; - } - if (_focusedPttHeldInputs.add(platformKey)) { - unawaited(_setPtt(true)); - } - } - - void _handleFocusedPttPointerRelease(PointerEvent event) { - final boundKey = _pttBoundKeyLabel; - if (!boundKey.startsWith('mouse-side-button:')) { - return; - } - if (_focusedPttHeldInputs.remove(boundKey)) { - unawaited(_setPtt(false, reportError: false)); - } - } - void _releaseFocusedPttIfHeld() { - if (_focusedPttHeldInputs.isEmpty) return; - _focusedPttHeldInputs.clear(); + if (_focusedPttHeldKeys.isEmpty) return; + _focusedPttHeldKeys.clear(); unawaited(_setPtt(false, reportError: false)); } @@ -627,12 +616,14 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { if (!mounted) return; switch (evt) { case rust.BridgeEvent_Connected(): + final hadSnapshot = _snapshot != null; setState(() { - _phase = ConnectionPhase.synchronizing; + _phase = phaseAfterConnectedEvent(_phase, hasSnapshot: hadSnapshot); _lostReason = null; _reconnectAttempt = null; _reconnectDelay = null; }); + unawaited(_refreshSnapshot(recordActivity: false, reportErrors: true)); case rust.BridgeEvent_Lost(:final reason): setState(() { _phase = ConnectionPhase.reconnecting; @@ -758,16 +749,12 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { // Skip echo of self-sent messages (already added locally). if (senderId == _snapshot?.ownClientId) return; final isPoke = target is rust.BridgeMessageTarget_Poke; - final resolvedSenderName = _resolvedChatSenderName( - senderId, - senderName, - ); final receivedAt = DateTime.now(); setState(() { _appendChatEntryUnlocked( ChatEntry( senderId: senderId, - senderName: resolvedSenderName, + senderName: senderName, message: message, target: target, isSelf: senderId == _snapshot?.ownClientId, @@ -777,7 +764,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { }); if (isPoke) { _showPokeSnackBar( - senderName: resolvedSenderName, + senderName: senderName, message: message, receivedAt: receivedAt, ); @@ -788,7 +775,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { unawaited(() async { if (!mounted) return; _showChatMessageSnackBar( - senderName: resolvedSenderName, + senderName: senderName, message: message, target: target, ); @@ -862,13 +849,14 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey); - _backIntentService.stop(); _eventsSub?.cancel(); _statsTimer?.cancel(); _snapshotRefreshInFlight = false; _snapshotRefreshQueued = false; _snapshotRefreshQueuedRecordActivity = false; _snapshotRefreshQueuedReportErrors = false; + _hostCtl.removeListener(_onHostEdited); + _prefetch.dispose(); _hostCtl.dispose(); _nickCtl.dispose(); _passwordCtl.dispose(); @@ -893,6 +881,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { String? nickname, String? password, }) async { + final connectEpoch = ++_connectionEpoch; setState(() { _phase = ConnectionPhase.connecting; _snapshot = null; @@ -906,25 +895,27 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { password: password ?? _passwordCtl.text, ); if (!mounted) return; + final connectedHost = host ?? _hostCtl.text.trim(); + final connectedNickname = nickname ?? _nickCtl.text.trim(); setState(() { _phase = ConnectionPhase.synchronizing; _applySnapshot(snap); }); - unawaited( - _saveUiSettings( - host: host ?? _hostCtl.text.trim(), - nickname: nickname ?? _nickCtl.text.trim(), - ), - ); - try { - await FlutterForegroundTask.startService( - notificationTitle: 'Chanora', - notificationText: 'Connected to ${snap.serverName}', - notificationButtons: const [ - NotificationButton(id: 'disconnect', text: 'Disconnect'), - ], + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || + _connectionEpoch != connectEpoch || + !_serverReachable || + _snapshot?.serverName != snap.serverName) { + return; + } + unawaited( + _runPostConnectSideEffects( + snap: snap, + host: connectedHost, + nickname: connectedNickname, + ), ); - } catch (_) {} + }); } catch (e) { if (!mounted) return; final errorStr = e.toString(); @@ -939,47 +930,63 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } } + Future _runPostConnectSideEffects({ + required rust.BridgeSnapshot snap, + required String host, + required String nickname, + }) async { + unawaited(_saveUiSettings(host: host, nickname: nickname)); + try { + await FlutterForegroundTask.startService( + notificationTitle: 'Chanora', + notificationText: 'Connected to ${snap.serverName}', + notificationButtons: const [ + NotificationButton(id: 'disconnect', text: 'Disconnect'), + ], + ); + } catch (_) {} + } + void _showPermissionDeniedDialog(String rawError) { final l10n = AppL10n.of(context); final isNetwork = rawError.contains('9987') || rawError.contains('connect'); - unawaited( - _showTrackedDialog( - builder: (ctx) => AlertDialog( - title: Text( - isNetwork ? l10n.networkPermissionTitle : l10n.permissionDenied, - ), - content: Text( - isNetwork - ? l10n.networkPermissionBody - : l10n.microphonePermissionBody, - ), - actions: [ - if (Platform.isIOS && !isNetwork) - TextButton( - onPressed: () { - Navigator.pop(ctx); - unawaited(_openIosAppSettings()); - }, - child: Text(l10n.networkPermissionOpenSettings), - ), - if (Platform.isMacOS) - TextButton( - onPressed: () { - Navigator.pop(ctx); - try { - Process.run('open', [ - 'x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork', - ]); - } catch (_) {} - }, - child: Text(l10n.networkPermissionOpenSettings), - ), - TextButton( - onPressed: () => Navigator.pop(ctx), - child: Text(MaterialLocalizations.of(context).okButtonLabel), - ), - ], + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + isNetwork ? l10n.networkPermissionTitle : l10n.permissionDenied, ), + content: Text( + isNetwork + ? l10n.networkPermissionBody + : l10n.microphonePermissionBody, + ), + actions: [ + if (Platform.isIOS && !isNetwork) + TextButton( + onPressed: () { + Navigator.pop(ctx); + unawaited(_openIosAppSettings()); + }, + child: Text(l10n.networkPermissionOpenSettings), + ), + if (Platform.isMacOS) + TextButton( + onPressed: () { + Navigator.pop(ctx); + try { + Process.run('open', [ + 'x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork', + ]); + } catch (_) {} + }, + child: Text(l10n.networkPermissionOpenSettings), + ), + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(MaterialLocalizations.of(context).okButtonLabel), + ), + ], ), ); } @@ -1014,14 +1021,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { if (!mounted) return; setState(() { _outputMuted = next; - _hardMuteByOutputMute = next; - if (next) { - _hardMute = true; - } else if (!_inputMuted && - !_hardMuteByPermission && - !_hardMuteByTalkPower) { - _hardMute = false; - } }); } catch (e) { if (!mounted) return; @@ -1116,9 +1115,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } Future _onToggleHardMute() async { - // Don't allow manual unmute while another policy owns the local - // transmit clamp. - if ((_hardMuteByTalkPower || _hardMuteByOutputMute) && _hardMute) { + // Don't allow manual unmute when talk-power-muted. + if (_hardMuteByTalkPower && _hardMute) { return; } final next = !_hardMute; @@ -1213,8 +1211,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { pttBoundInputClass: _pttBoundInputClass, isTouchOnly: isTouchOnlyPttHost, initialAudioConfig: audioConfig, - onnxRuntimeAvailable: - StartupDependencyScope.maybeOf(context)?.hasOnnxRuntime ?? true, onModeChanged: (mode) async { try { await rust.setTransmitMode(mode: mode); @@ -1256,8 +1252,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { initialMode: _transmitMode, initialReleaseTailMs: _releaseTailMs, initialAudioConfig: audioConfig, - onnxRuntimeAvailable: - StartupDependencyScope.maybeOf(ctx)?.hasOnnxRuntime ?? true, pttLevel: _pttLevel, pttBackendId: _pttBackendId, pttBoundInputClass: _pttBoundInputClass, @@ -1359,6 +1353,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } Future _onDisconnect() async { + _connectionEpoch++; _releaseFocusedPttIfHeld(); if (mounted) { setState(() { @@ -1370,7 +1365,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { Future _onConfirmDisconnect() async { final l10n = AppL10n.of(context); - final confirmed = await _showTrackedDialog( + final confirmed = await showDialog( + context: context, builder: (ctx) => AlertDialog( title: Text(l10n.disconnectConfirmTitle), content: Text(l10n.disconnectConfirmBody), @@ -1419,7 +1415,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { _hardMute = false; _hardMuteByPermission = false; _hardMuteByTalkPower = false; - _hardMuteByOutputMute = false; _inChannel = false; _currentVoiceChannelId = null; _pendingVoiceChannelId = null; @@ -1439,7 +1434,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { String clientName = '', }) async { final initialSnapshot = _snapshot!; - _dismissChatPreviewSnackBar(); setState(() { _chatUnread = 0; _chatOpen = true; @@ -1466,6 +1460,22 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { if (mounted) setState(() => _chatOpen = false); } + Future _onOpenClientInfo(rust.BridgeClient client) async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + showDragHandle: false, + builder: (context) => FractionallySizedBox( + heightFactor: 0.86, + child: ClientInfoSheet( + clientName: client.name, + loadProfile: () => rust.clientProfile(clientId: client.id), + ), + ), + ); + } + void _showPokeSnackBar({ required String senderName, required String message, @@ -1522,50 +1532,36 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { if (_pokeSnackBarPokes.value.isNotEmpty) return; final messenger = ScaffoldMessenger.of(context); messenger.hideCurrentSnackBar(); - _chatPreviewSnackBarVisible = true; - void openChatFromPreview() { - _dismissChatPreviewSnackBar(); - unawaited( - _onOpenChat( - target: target, - clientName: _chatClientNameForTarget(target, senderName), - ), - ); - } - - final controller = messenger.showSnackBar( + messenger.showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, margin: _chatSnackBarMargin(), - duration: const Duration(seconds: 4), - content: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: openChatFromPreview, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '$senderName (${_chatTargetLabel(target)})', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 4), - Text(message, maxLines: 3, overflow: TextOverflow.ellipsis), - ], + duration: const Duration(seconds: 7), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$senderName (${_chatTargetLabel(target)})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Text(message, maxLines: 3, overflow: TextOverflow.ellipsis), + ], + ), + action: SnackBarAction( + label: AppL10n.of(context).openAction, + onPressed: () => unawaited( + _onOpenChat( + target: target, + clientName: _chatClientNameForTarget(target, senderName), + ), ), ), - action: SnackBarAction(label: 'Open', onPressed: openChatFromPreview), ), ); - controller.closed.then((_) => _chatPreviewSnackBarVisible = false); - } - - void _dismissChatPreviewSnackBar() { - if (!_chatPreviewSnackBarVisible || !mounted) return; - _chatPreviewSnackBarVisible = false; - ScaffoldMessenger.of(context).removeCurrentSnackBar(); } EdgeInsetsGeometry _chatSnackBarMargin() { @@ -1606,33 +1602,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { }; } - String _resolvedChatSenderName(BigInt senderId, String fallback) { - final snapshot = _snapshot; - if (snapshot != null) { - for (final client in snapshot.clients) { - if (client.id == senderId && client.name.isNotEmpty) { - return client.name; - } - } - } - return fallback; - } - void _applySnapshot(rust.BridgeSnapshot snap) { _snapshot = snap; - unawaited(_applyClientPlaybackPreferencesToSnapshot(snap)); + _phase = phaseAfterSnapshotApplied(_phase); final own = ownClientSnapshotState(snap); if (own == null) return; _inputMuted = own.inputMuted; _outputMuted = own.outputMuted; - _hardMuteByOutputMute = own.outputMuted; - if (own.outputMuted) { - _hardMute = true; - } else if (!_inputMuted && - !_hardMuteByPermission && - !_hardMuteByTalkPower) { - _hardMute = false; - } if (!_voiceStateInitialized || _currentVoiceChannelId == null) { _currentVoiceChannelId = own.channelId; _pendingVoiceChannelId = null; @@ -1645,16 +1621,35 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { _notifyChatFeedChanged(); if (!own.talkPowerOk && !_hardMuteByTalkPower) { + final talkPowerEpoch = _connectionEpoch; _hardMuteByTalkPower = true; _hardMute = true; - rust.setHardMute(muted: true); - rust.setInputMuted(muted: true); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || + _connectionEpoch != talkPowerEpoch || + !_serverReachable || + !_hardMuteByTalkPower) { + return; + } + unawaited(rust.setHardMute(muted: true)); + unawaited(rust.setInputMuted(muted: true)); + }); } else if (own.talkPowerOk && _hardMuteByTalkPower) { + final talkPowerEpoch = _connectionEpoch; _hardMuteByTalkPower = false; - if (!_hardMuteByPermission && !_hardMuteByOutputMute && !_inputMuted) { + if (!_hardMuteByPermission) { _hardMute = false; - rust.setHardMute(muted: false); - rust.setInputMuted(muted: false); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || + _connectionEpoch != talkPowerEpoch || + !_serverReachable || + _hardMuteByTalkPower || + _hardMuteByPermission) { + return; + } + unawaited(rust.setHardMute(muted: false)); + unawaited(rust.setInputMuted(muted: false)); + }); } } } @@ -1664,45 +1659,20 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { return snap == null ? null : ownClientSnapshotState(snap); } - Future _onShowDiagnostics(BuildContext context) async { - final l10n = AppL10n.of(context); - final rustText = await rust.exportDiagnostics(); - if (!context.mounted) return; + String _diagnosticsText() { + final rustText = rust.exportDiagnostics(); final uiText = _uiDiagnostics.isEmpty ? 'UI diagnostics: none' : ['UI diagnostics:', ..._uiDiagnostics].join('\n'); - final text = '$uiText\n\nRust diagnostics:\n$rustText'; + return '$uiText\n\nRust diagnostics:\n$rustText'; + } + + Future _onShowDiagnostics() async { + if (!mounted) return; await showDialog( context: context, - builder: (ctx) => AlertDialog( - title: Text(l10n.diagnosticsAction), - content: SingleChildScrollView( - child: SelectableText( - text, - style: const TextStyle(fontFamily: 'monospace', fontSize: 11), - ), - ), - actions: [ - TextButton( - onPressed: () async { - await SharePlus.instance.share(ShareParams(text: text)); - }, - child: const Text('Share'), - ), - TextButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: text)); - if (!ctx.mounted) return; - Navigator.of(ctx).pop(); - }, - child: Text(l10n.copyAction), - ), - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: Text(l10n.closeAction), - ), - ], - ), + builder: (ctx) => + _LiveDiagnosticsDialog(diagnosticsTextBuilder: _diagnosticsText), ); } @@ -1808,7 +1778,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { TextButton( onPressed: () async { Navigator.of(ctx).pop(); - await _onShowDiagnostics(context); + await Future.delayed(Duration.zero); + if (!mounted) return; + await _onShowDiagnostics(); }, child: Text(l10n.diagnosticsAction), ), @@ -1959,14 +1931,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { icon: const Icon(Icons.info_outline), onPressed: () => _onShowAbout(context), ), - if (_phase.canOpenChat) ...[ + ChanoraThemeModeMenu( + themeMode: widget.themeMode, + onThemeModeChanged: widget.onThemeModeChanged, + ), + if (_phase.canOpenChatWithSnapshot(hasSnapshot: _snapshot != null)) ...[ Padding( padding: const EdgeInsetsDirectional.only(end: 12), child: Badge( isLabelVisible: _chatUnread > 0, label: Text(_chatUnread.toString()), child: IconButton( - tooltip: 'Chat', + tooltip: l10n.chatAction, icon: const Icon(Icons.chat_bubble_outline), onPressed: _onOpenChat, ), @@ -1987,6 +1963,23 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { ), ) : headerTitle; + final compactIdleChrome = + !_serverReachable && MediaQuery.sizeOf(context).width < _wideBreakpoint; + final compactIdleHeader = Row( + children: [ + Expanded( + child: Text( + l10n.appTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + ...headerActions, + ], + ); final connTokens = _phase.tokens(theme.colorScheme); final statusText = connectionStatusText( @@ -1997,234 +1990,256 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { reconnectAttempt: _reconnectAttempt, reconnectDelay: _reconnectDelay, ); + final awaitingServerSnapshot = _phase.shouldShowSnapshotLoading( + hasSnapshot: _snapshot != null, + ); - final bodyContent = Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: _handleFocusedPttPointerDown, - onPointerUp: _handleFocusedPttPointerRelease, - onPointerCancel: _handleFocusedPttPointerRelease, - child: LayoutBuilder( - builder: (ctx, bodyConstraints) { - final isWideSnapshot = - bodyConstraints.maxWidth >= _wideBreakpoint && - _serverReachable && - _snapshot != null; - final banner = Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: theme.colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - l10n.homeNotProductionReadyBanner, - style: TextStyle(color: theme.colorScheme.onTertiaryContainer), - ), - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)], - if (!_serverReachable) - Row( + final bodyContent = LayoutBuilder( + builder: (ctx, bodyConstraints) { + final isWideSnapshot = + bodyConstraints.maxWidth >= _wideBreakpoint && + _serverReachable && + _snapshot != null; + final banner = Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + l10n.homeNotProductionReadyBanner, + style: TextStyle(color: theme.colorScheme.onTertiaryContainer), + ), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)], + if (!_serverReachable) + Row( + children: [ + Icon(connTokens.icon, size: 18, color: connTokens.color), + const SizedBox(width: 6), + Expanded( + child: Text( + statusText, + softWrap: true, + style: theme.textTheme.titleMedium, + ), + ), + ], + ), + if (_lostReason != null || _reconnectAttempt != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: + connTokens.background ?? theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( children: [ - Icon(connTokens.icon, size: 18, color: connTokens.color), - const SizedBox(width: 6), + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.onErrorContainer, + ), + ), + const SizedBox(width: 10), Expanded( child: Text( - statusText, - softWrap: true, - style: theme.textTheme.titleMedium, + _reconnectAttempt != null + ? l10n.statusReconnecting( + _reconnectAttempt!, + _reconnectDelay ?? 0, + ) + : l10n.statusConnectionLost(_lostReason ?? ''), + style: TextStyle( + color: theme.colorScheme.onErrorContainer, + ), ), ), ], ), - if (_lostReason != null || _reconnectAttempt != null) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: - connTokens.background ?? - theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: theme.colorScheme.onErrorContainer, - ), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - _reconnectAttempt != null - ? l10n.statusReconnecting( - _reconnectAttempt!, - _reconnectDelay ?? 0, - ) - : l10n.statusConnectionLost(_lostReason ?? ''), - style: TextStyle( - color: theme.colorScheme.onErrorContainer, - ), - ), - ), - ], - ), - ), - ], - const SizedBox(height: 12), - if (_phase == ConnectionPhase.idle || - _phase == ConnectionPhase.disconnected) ...[ - Expanded( - child: AnimatedPadding( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOut, - padding: EdgeInsets.only( - bottom: MediaQuery.viewInsetsOf(ctx).bottom, - ), - child: SingleChildScrollView( - keyboardDismissBehavior: - ScrollViewKeyboardDismissBehavior.onDrag, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ConnectForm( - hostCtl: _hostCtl, - nickCtl: _nickCtl, - passwordCtl: _passwordCtl, - onConnect: () => _onConnect(), - onAddBookmark: _onAddCurrentBookmark, - ), - const SizedBox(height: 16), - BookmarkList( - bookmarks: _bookmarks, - onConnect: _onUseBookmark, - onDelete: _onDeleteBookmark, - ), - ], - ), - ), - ), - ), - ] else if (_phase == ConnectionPhase.connecting) ...[ - const Center( - child: Padding( - padding: EdgeInsets.all(32), - child: CircularProgressIndicator(), - ), - ), - ] else if (_serverReachable && _snapshot != null) ...[ - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) { - final voiceBar = VoiceBar( - inChannel: _inChannel, - transmitMode: _transmitMode, - hardMute: _hardMute, - outputMuted: _outputMuted, - talkPowerBlocked: _hardMuteByTalkPower, - releaseTailMs: _releaseTailMs, - channelName: snapshotChannelName( - _snapshot, - _currentVoiceChannelId, - ), - audioStats: _audioStats, - pttLevel: _pttLevel, - pttBackendId: _pttBackendId, - pttBoundInputClass: _pttBoundInputClass, - pttBoundKeyLabel: _pttBoundKeyLabel, - onConfigure: _onOpenVoiceSettings, - onPttHeldChanged: _onOnscreenPttHeldChanged, - ); - // SDD-106 §2/§3 + SRS-209 + SRS-164: listen-only - // banner. Self-hides on granted / unknown. - final permissionBanner = - PermissionStateBanner.fromCallbacks( - recordAudioState: _activeRecordAudioState, - ensureRecordAudio: _ensureActiveRecordAudio, - openAppSettings: _openActivePermissionSettings, - ); - final snapshotView = SnapshotView( - snapshot: _snapshot!, - audioStats: _audioStats, - currentVoiceChannelId: _currentVoiceChannelId, - pendingVoiceChannelId: _pendingVoiceChannelId, - localInputMuted: _inputMuted || _hardMute, - localOutputMuted: _outputMuted, - hasJoinPending: _pendingVoiceChannelId != null, - canJoinVoiceChannel: _canJoinVoiceChannel, - onJoinChannel: (ch) => _onJoinChannel(ch), - onJoinChannelWithPassword: (ch) => - _onJoinChannel(ch, askForPassword: true), - clientPlaybackPreferences: _clientPlaybackPrefs, - onClientPlaybackPreferenceChanged: - _setClientPlaybackPreference, - onTs3ServerLink: _onTs3ServerLink, - ); - final ownClientState = _ownClientState; - const voiceBarWidthWide = 320.0; - if (constraints.maxWidth >= _wideBreakpoint) { - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - width: voiceBarWidthWide, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - banner, - const SizedBox(height: 12), - permissionBanner, - voiceBar, - ], - ), - ), - const SizedBox(width: 12), - Expanded(child: snapshotView), - ], - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded(child: snapshotView), - const SizedBox(height: 8), - permissionBanner, - VoiceStatusChip( - transmitMode: _transmitMode, - releaseTailMs: _releaseTailMs, - pttBoundKeyLabel: _pttBoundKeyLabel, - audioStats: _audioStats, - isTouchOnly: isTouchOnlyPttHost, - inputMuted: _hardMute, - outputMuted: _outputMuted, - talkPower: ownClientState?.talkPower, - neededTalkPower: ownClientState?.neededTalkPower, - talkPowerGranted: ownClientState?.talkPowerGranted, - onTap: () => _onOpenVoiceDetailsSheet(), - ), - if (_inChannel && - _transmitMode == rust.BridgeTransmitMode.ptt) ...[ - const SizedBox(height: 8), - VoicePttButton( - active: _audioStats?.pttActive ?? false, - onHeldChanged: _onOnscreenPttHeldChanged, - ), - ], - ], - ); - }, - ), - ), - ], + ), ], - ); - }, - ), + const SizedBox(height: 12), + if (_phase == ConnectionPhase.idle || + _phase == ConnectionPhase.disconnected) ...[ + Expanded( + child: AnimatedPadding( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(ctx).bottom, + ), + child: SingleChildScrollView( + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConnectForm( + hostCtl: _hostCtl, + nickCtl: _nickCtl, + passwordCtl: _passwordCtl, + onConnect: () => _onConnect(), + onAddBookmark: _onAddCurrentBookmark, + ), + const SizedBox(height: 16), + BookmarkList( + bookmarks: _bookmarks, + onConnect: _onUseBookmark, + onDelete: _onDeleteBookmark, + ), + ], + ), + ), + ), + ), + ] else if (_phase == ConnectionPhase.connecting || + awaitingServerSnapshot) ...[ + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + statusText, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium, + ), + ], + ), + ), + ), + ), + ] else if (_serverReachable && _snapshot != null) ...[ + Expanded( + child: LayoutBuilder( + builder: (ctx, constraints) { + final voiceBar = VoiceBar( + inChannel: _inChannel, + transmitMode: _transmitMode, + hardMute: _hardMute, + outputMuted: _outputMuted, + talkPowerBlocked: _hardMuteByTalkPower, + releaseTailMs: _releaseTailMs, + channelName: snapshotChannelName( + _snapshot, + _currentVoiceChannelId, + ), + audioStats: _audioStats, + pttLevel: _pttLevel, + pttBackendId: _pttBackendId, + pttBoundInputClass: _pttBoundInputClass, + pttBoundKeyLabel: _pttBoundKeyLabel, + onConfigure: _onOpenVoiceSettings, + onPttHeldChanged: _onOnscreenPttHeldChanged, + ); + // SDD-106 §2/§3 + SRS-209 + SRS-164: listen-only + // banner. Self-hides on granted / unknown. + final permissionBanner = + PermissionStateBanner.fromCallbacks( + recordAudioState: _activeRecordAudioState, + ensureRecordAudio: _ensureActiveRecordAudio, + openAppSettings: _openActivePermissionSettings, + ); + final snapshotView = SnapshotView( + snapshot: _snapshot!, + audioStats: _audioStats, + currentVoiceChannelId: _currentVoiceChannelId, + pendingVoiceChannelId: _pendingVoiceChannelId, + localInputMuted: _inputMuted || _hardMute, + localOutputMuted: _outputMuted, + hasJoinPending: _pendingVoiceChannelId != null, + canJoinVoiceChannel: _canJoinVoiceChannel, + onJoinChannel: (ch) => _onJoinChannel(ch), + onJoinChannelWithPassword: (ch) => + _onJoinChannel(ch, askForPassword: true), + enableClientLongPressMenu: isTouchOnlyPttHost, + onOpenClientInfo: (client) => + unawaited(_onOpenClientInfo(client)), + onOpenClientChat: (client) => unawaited( + _onOpenChat( + target: rust.BridgeMessageTarget.client(client.id), + clientName: client.name, + ), + ), + onOpenClientPoke: (client) => unawaited( + _onOpenChat( + target: rust.BridgeMessageTarget.poke(client.id), + clientName: client.name, + ), + ), + onTs3ServerLink: _onTs3ServerLink, + ); + final ownClientState = _ownClientState; + const voiceBarWidthWide = 320.0; + if (constraints.maxWidth >= _wideBreakpoint) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: voiceBarWidthWide, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + banner, + const SizedBox(height: 12), + permissionBanner, + voiceBar, + ], + ), + ), + const SizedBox(width: 12), + Expanded(child: snapshotView), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: snapshotView), + const SizedBox(height: 8), + permissionBanner, + VoiceStatusChip( + transmitMode: _transmitMode, + releaseTailMs: _releaseTailMs, + pttBoundKeyLabel: _pttBoundKeyLabel, + audioStats: _audioStats, + isTouchOnly: isTouchOnlyPttHost, + inputMuted: _hardMute, + outputMuted: _outputMuted, + talkPower: ownClientState?.talkPower, + neededTalkPower: ownClientState?.neededTalkPower, + talkPowerGranted: ownClientState?.talkPowerGranted, + onTap: () => _onOpenVoiceDetailsSheet(), + ), + if (_inChannel && + _transmitMode == rust.BridgeTransmitMode.ptt) ...[ + const SizedBox(height: 8), + VoicePttButton( + active: _audioStats?.pttActive ?? false, + onHeldChanged: _onOnscreenPttHeldChanged, + ), + ], + ], + ); + }, + ), + ), + ], + ], + ); + }, ); if (_isMacOS) { @@ -2254,24 +2269,105 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { ); } - return Scaffold( - appBar: AppBar( - leading: _phase.canDisconnect - ? IconButton( - tooltip: l10n.disconnectAction, - icon: const Icon(Icons.arrow_back), - onPressed: _onConfirmDisconnect, - ) - : null, - leadingWidth: _phase.canDisconnect ? 44 : null, - titleSpacing: _phase.canDisconnect ? 4 : null, - title: appBarTitle, - actions: headerActions, + return ChanoraMobileScaffold( + compactIdleChrome: compactIdleChrome, + canDisconnect: _phase.canDisconnect, + title: appBarTitle, + actions: headerActions, + onDisconnect: _onConfirmDisconnect, + compactHeader: compactIdleHeader, + disconnectTooltip: l10n.disconnectAction, + body: bodyContent, + ); + } +} + +class _LiveDiagnosticsDialog extends StatefulWidget { + const _LiveDiagnosticsDialog({required this.diagnosticsTextBuilder}); + + final String Function() diagnosticsTextBuilder; + + @override + State<_LiveDiagnosticsDialog> createState() => _LiveDiagnosticsDialogState(); +} + +class _LiveDiagnosticsDialogState extends State<_LiveDiagnosticsDialog> { + static const _refreshInterval = Duration(seconds: 1); + + Timer? _refreshTimer; + String _text = ''; + + @override + void initState() { + super.initState(); + _refresh(); + _refreshTimer = Timer.periodic(_refreshInterval, (_) => _refresh()); + } + + @override + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); + } + + void _refresh() { + final next = widget.diagnosticsTextBuilder(); + if (!mounted || next == _text) return; + setState(() => _text = next); + } + + @override + Widget build(BuildContext context) { + final l10n = AppL10n.of(context); + final size = MediaQuery.sizeOf(context); + + return AlertDialog( + title: Row( + children: [ + Expanded(child: Text(l10n.diagnosticsAction)), + const SizedBox(width: 12), + Tooltip( + message: l10n.diagnosticsLiveUpdating, + child: Icon( + Icons.sync, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], ), - body: SafeArea( - top: false, - child: Padding(padding: const EdgeInsets.all(16), child: bodyContent), + content: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 720, + maxHeight: size.height * 0.65, + ), + child: SingleChildScrollView( + child: SelectableText( + _text, + style: const TextStyle(fontFamily: 'monospace', fontSize: 11), + ), + ), ), + actions: [ + TextButton( + onPressed: () async { + await SharePlus.instance.share(ShareParams(text: _text)); + }, + child: Text(l10n.shareAction), + ), + TextButton( + onPressed: () async { + await Clipboard.setData(ClipboardData(text: _text)); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + child: Text(l10n.copyAction), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.closeAction), + ), + ], ); } } diff --git a/apps/chanora_flutter/lib/services/app_bootstrap.dart b/apps/chanora_flutter/lib/services/app_bootstrap.dart index 1c135b7..ef14815 100644 --- a/apps/chanora_flutter/lib/services/app_bootstrap.dart +++ b/apps/chanora_flutter/lib/services/app_bootstrap.dart @@ -18,19 +18,17 @@ Future? _storageInitFuture; Future? _vadBootstrapFuture; StorageDirectoryProvider _storageDirectoryProvider = getApplicationSupportDirectory; -StorageDirectoryProvider _vadModelDirectoryProvider = - getApplicationSupportDirectory; StorageInitializer _storageInitializer = _defaultStorageInitializer; Future _defaultStorageInitializer(String dir) { return rust.initStorage(dir: dir); } -Future _copyBundledAssetToManagedDirectory({ +Future _copyBundledAssetToDocuments({ required String assetPath, required String fileName, }) async { - final dir = await _vadModelDirectoryProvider(); + final dir = await getApplicationDocumentsDirectory(); final file = File('${dir.path}/$fileName'); final data = await rootBundle.load(assetPath); final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); @@ -61,7 +59,7 @@ Future configureBundledVadModels() async { } Future _configureBundledVadModelsImpl() async { - final silero = await _copyBundledAssetToManagedDirectory( + final silero = await _copyBundledAssetToDocuments( assetPath: _sileroVadAsset, fileName: 'silero_vad.onnx', ); @@ -126,15 +124,12 @@ Future _wireStorageImpl() async { @visibleForTesting void debugResetStorageBootstrap({ StorageDirectoryProvider? storageDirectoryProvider, - StorageDirectoryProvider? vadModelDirectoryProvider, StorageInitializer? storageInitializer, }) { _storageInitFuture = null; _vadBootstrapFuture = null; _storageDirectoryProvider = storageDirectoryProvider ?? getApplicationSupportDirectory; - _vadModelDirectoryProvider = - vadModelDirectoryProvider ?? getApplicationSupportDirectory; _storageInitializer = storageInitializer ?? _defaultStorageInitializer; } diff --git a/apps/chanora_flutter/lib/services/connection_phase_state.dart b/apps/chanora_flutter/lib/services/connection_phase_state.dart index 9124c08..cbfb185 100644 --- a/apps/chanora_flutter/lib/services/connection_phase_state.dart +++ b/apps/chanora_flutter/lib/services/connection_phase_state.dart @@ -35,6 +35,12 @@ extension ConnectionPhaseState on ConnectionPhase { bool get canDisconnect => canOpenChat; + bool shouldShowSnapshotLoading({required bool hasSnapshot}) => + isServerReachable && !hasSnapshot; + + bool canOpenChatWithSnapshot({required bool hasSnapshot}) => + canOpenChat && hasSnapshot; + ConnectionTokens tokens(ColorScheme colorScheme) { switch (this) { case ConnectionPhase.idle: @@ -52,6 +58,15 @@ extension ConnectionPhaseState on ConnectionPhase { } } +ConnectionPhase phaseAfterConnectedEvent( + ConnectionPhase phase, { + required bool hasSnapshot, +}) { + return phase == ConnectionPhase.connected && hasSnapshot + ? ConnectionPhase.connected + : ConnectionPhase.synchronizing; +} + String connectionStatusText({ required ConnectionPhase phase, required AppL10n l10n, @@ -77,3 +92,9 @@ String connectionStatusText({ return l10n.statusIdle; } } + +ConnectionPhase phaseAfterSnapshotApplied(ConnectionPhase phase) { + return phase == ConnectionPhase.synchronizing + ? ConnectionPhase.connected + : phase; +} diff --git a/apps/chanora_flutter/lib/services/link_trust_service.dart b/apps/chanora_flutter/lib/services/link_trust_service.dart index 2861c31..733a93a 100644 --- a/apps/chanora_flutter/lib/services/link_trust_service.dart +++ b/apps/chanora_flutter/lib/services/link_trust_service.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; + +import '../l10n/generated/app_localizations.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LinkTrustService extends ChangeNotifier { @@ -54,42 +56,45 @@ Future showLinkTrustDialog(BuildContext context, String domain) async { return showDialog( context: context, builder: (ctx) => StatefulBuilder( - builder: (ctx, setDialogState) => AlertDialog( - title: const Text('Open external link?'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('You are about to open a link to:\n\n$domain'), - const SizedBox(height: 12), - Row( - children: [ - SizedBox( - width: 24, - height: 24, - child: Checkbox( - value: remember, - onChanged: (v) => - setDialogState(() => remember = v ?? false), + builder: (ctx, setDialogState) { + final l10n = AppL10n.of(ctx); + return AlertDialog( + title: Text(l10n.linkTrustTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.linkTrustBody(domain)), + const SizedBox(height: 12), + Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: remember, + onChanged: (v) => + setDialogState(() => remember = v ?? false), + ), ), - ), - const SizedBox(width: 8), - const Flexible(child: Text('Trust all links from this domain')), - ], + const SizedBox(width: 8), + Flexible(child: Text(l10n.linkTrustRememberDomain)), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(null), + child: Text(l10n.cancelAction), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(remember), + child: Text(l10n.openAction), ), ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(null), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(ctx).pop(remember), - child: const Text('Open'), - ), - ], - ), + ); + }, ), ); } diff --git a/apps/chanora_flutter/lib/services/prefetch_debouncer.dart b/apps/chanora_flutter/lib/services/prefetch_debouncer.dart new file mode 100644 index 0000000..c920493 --- /dev/null +++ b/apps/chanora_flutter/lib/services/prefetch_debouncer.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +class PrefetchDebouncer { + PrefetchDebouncer({ + required this.onPrefetch, + this.delay = const Duration(milliseconds: 700), + }); + + final Future Function(String host) onPrefetch; + final Duration delay; + + Timer? _timer; + bool _disposed = false; + + void schedule(String rawHost) { + if (_disposed) return; + _timer?.cancel(); + final host = rawHost.trim(); + if (host.isEmpty) return; + _timer = Timer(delay, () { + if (_disposed) return; + unawaited(onPrefetch(host)); + }); + } + + void dispose() { + _disposed = true; + _timer?.cancel(); + _timer = null; + } +} diff --git a/apps/chanora_flutter/lib/services/startup_dependency_check.dart b/apps/chanora_flutter/lib/services/startup_dependency_check.dart deleted file mode 100644 index 28b16e3..0000000 --- a/apps/chanora_flutter/lib/services/startup_dependency_check.dart +++ /dev/null @@ -1,444 +0,0 @@ -import 'dart:ffi'; -import 'dart:io'; - -import 'package:flutter/foundation.dart' show visibleForTesting; - -import '../src/rust/api.dart' as rust; - -class StartupDependencyCheckResult { - const StartupDependencyCheckResult({ - required this.issues, - required this.platformLabel, - }); - - final List issues; - final String platformLabel; - - bool get hasIssues => issues.isNotEmpty; - bool get hasBlockingIssues => issues.any((issue) => issue.isRequired); - bool get hasOnnxRuntime => !issues.any((issue) => issue.id == 'linux-onnxruntime'); -} - -class StartupDependencyIssue { - const StartupDependencyIssue({ - required this.id, - required this.title, - required this.summary, - required this.details, - required this.severity, - this.installHints = const [], - }); - - final String id; - final String title; - final String summary; - final List details; - final StartupDependencySeverity severity; - final List installHints; - - bool get isRequired => severity == StartupDependencySeverity.required; -} - -class StartupInstallHint { - const StartupInstallHint({required this.label, required this.command}); - - final String label; - final String command; -} - -enum StartupDependencySeverity { required, recommended } - -enum _LinuxDistro { debian, fedora, arch, other } - -enum _LinuxArch { x64, arm64, other } - -typedef _LibraryProbe = bool Function(String candidate); -typedef _FileExists = Future Function(String path); -typedef _ResolvedExecutableProvider = String Function(); -typedef _CurrentDirectoryProvider = String Function(); -typedef _OsReleaseProvider = Future Function(); -typedef _PlatformIsLinux = bool Function(); -typedef _LogFilePathProvider = String Function(); -typedef _CurrentAbiProvider = Abi Function(); - -_LibraryProbe _libraryProbe = _defaultLibraryProbe; -_FileExists _fileExists = _defaultFileExists; -_ResolvedExecutableProvider _resolvedExecutableProvider = () => - Platform.resolvedExecutable; -_CurrentDirectoryProvider _currentDirectoryProvider = () => - Directory.current.path; -_OsReleaseProvider _osReleaseProvider = _defaultOsReleaseProvider; -_PlatformIsLinux _platformIsLinux = () => Platform.isLinux; -_LogFilePathProvider _logFilePathProvider = rust.logFilePathStr; -_CurrentAbiProvider _currentAbiProvider = Abi.current; - -Future logStartupDependencyIssues( - StartupDependencyCheckResult result, -) async { - if (!result.hasIssues) return; - - final path = _logFilePathProvider(); - if (path.isEmpty) return; - - final issues = result.issues - .map( - (issue) => - '${issue.id}:${issue.isRequired ? 'required' : 'recommended'}:' - '${_sanitizeLogValue(issue.title)}', - ) - .join(', '); - final line = - '${DateTime.now().toUtc().toIso8601String()} ' - '[startup_dependency_check] ' - 'platform="${result.platformLabel}" ' - 'blocking=${result.hasBlockingIssues} ' - 'issues=[$issues]'; - - try { - await File( - path, - ).writeAsString('$line\n', mode: FileMode.append, flush: true); - } catch (_) { - // Best-effort only: a missing/unwritable log file must not block app startup. - } -} - -String _sanitizeLogValue(String value) => value.replaceAll('"', "'"); - -Future checkStartupDependencies() async { - if (!_platformIsLinux()) { - return const StartupDependencyCheckResult( - issues: [], - platformLabel: 'default', - ); - } - - final distro = await _detectLinuxDistro(); - final arch = _detectLinuxArch(); - final executableDir = File(_resolvedExecutableProvider()).parent.path; - final issues = []; - - if (!_hasAnyLoadableLibrary(const [ - 'libpipewire-0.3.so.0', - 'libpipewire-0.3.so', - ])) { - issues.add(_buildPipeWireIssue(distro)); - } - - if (!_hasAnyLoadableLibrary(const [ - 'libpulse.so.0', - 'libpulse.so', - 'libpulse-simple.so.0', - 'libpulse-simple.so', - ])) { - issues.add(_buildPulseAudioIssue(distro)); - } - - if (!await _hasOnnxRuntime(executableDir: executableDir)) { - issues.add(_buildOnnxIssue(executableDir: executableDir, arch: arch)); - } - - return StartupDependencyCheckResult( - issues: issues, - platformLabel: switch (distro) { - _LinuxDistro.debian => 'Debian / Ubuntu', - _LinuxDistro.fedora => 'Fedora', - _LinuxDistro.arch => 'Arch Linux', - _LinuxDistro.other => 'Linux', - }, - ); -} - -bool _defaultLibraryProbe(String candidate) { - try { - DynamicLibrary.open(candidate); - return true; - } catch (_) { - return false; - } -} - -Future _defaultFileExists(String path) => File(path).exists(); - -Future _defaultOsReleaseProvider() async { - const path = '/etc/os-release'; - final file = File(path); - if (!await file.exists()) { - return null; - } - return file.readAsString(); -} - -bool _hasAnyLoadableLibrary(List candidates) { - for (final candidate in candidates) { - if (_libraryProbe(candidate)) { - return true; - } - } - return false; -} - -Future _hasOnnxRuntime({required String executableDir}) async { - final envPath = Platform.environment['ORT_DYLIB_PATH']; - if (envPath != null && envPath.isNotEmpty && await _fileExists(envPath)) { - return true; - } - - final candidates = { - '$executableDir/lib/libonnxruntime.so', - '$executableDir/libonnxruntime.so', - '${_currentDirectoryProvider()}/libonnxruntime.so', - '/usr/lib/libonnxruntime.so', - '/usr/lib64/libonnxruntime.so', - '/usr/local/lib/libonnxruntime.so', - '/lib/x86_64-linux-gnu/libonnxruntime.so', - '/usr/lib/x86_64-linux-gnu/libonnxruntime.so', - '/lib/aarch64-linux-gnu/libonnxruntime.so', - '/usr/lib/aarch64-linux-gnu/libonnxruntime.so', - }; - - for (final candidate in candidates) { - if (await _fileExists(candidate)) { - return true; - } - } - - for (final dir in const [ - '/usr/lib', - '/usr/lib64', - '/usr/local/lib', - '/usr/lib/x86_64-linux-gnu', - '/usr/lib/aarch64-linux-gnu', - ]) { - final match = await _firstMatchingDirEntry(dir, 'libonnxruntime.so'); - if (match != null) { - return true; - } - } - - return _hasAnyLoadableLibrary(const ['libonnxruntime.so']); -} - -Future _firstMatchingDirEntry(String dir, String prefix) async { - final directory = Directory(dir); - if (!await directory.exists()) { - return null; - } - - await for (final entity in directory.list(followLinks: false)) { - if (entity is! File) { - continue; - } - final name = entity.uri.pathSegments.isEmpty - ? '' - : entity.uri.pathSegments.last; - if (name == prefix || name.startsWith('$prefix.')) { - return entity.path; - } - } - - return null; -} - -Future<_LinuxDistro> _detectLinuxDistro() async { - final content = await _osReleaseProvider(); - if (content == null || content.isEmpty) { - return _LinuxDistro.other; - } - - final fields = _parseOsRelease(content); - final id = fields['id'] ?? ''; - final idLike = (fields['id_like'] ?? '').split(RegExp(r'\s+')); - final ids = {id, ...idLike}; - if (ids.contains('fedora')) { - return _LinuxDistro.fedora; - } - if (ids.contains('ubuntu') || ids.contains('debian')) { - return _LinuxDistro.debian; - } - if (ids.contains('arch')) { - return _LinuxDistro.arch; - } - return _LinuxDistro.other; -} - -Map _parseOsRelease(String content) { - final fields = {}; - for (final line in content.split('\n')) { - final separator = line.indexOf('='); - if (separator <= 0) continue; - final key = line.substring(0, separator).trim().toLowerCase(); - var value = line.substring(separator + 1).trim().toLowerCase(); - if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) { - value = value.substring(1, value.length - 1); - } - fields[key] = value; - } - return fields; -} - -_LinuxArch _detectLinuxArch() { - return switch (_currentAbiProvider()) { - Abi.linuxX64 => _LinuxArch.x64, - Abi.linuxArm64 => _LinuxArch.arm64, - _ => _LinuxArch.other, - }; -} - -StartupDependencyIssue _buildPipeWireIssue(_LinuxDistro distro) { - final List hints = switch (distro) { - _LinuxDistro.debian => const [ - StartupInstallHint( - label: 'Debian / Ubuntu', - command: 'sudo apt install libpipewire-0.3-0', - ), - ], - _LinuxDistro.fedora => const [ - StartupInstallHint( - label: 'Fedora', - command: 'sudo dnf install pipewire-libs', - ), - ], - _LinuxDistro.arch => const [ - StartupInstallHint( - label: 'Arch Linux', - command: 'sudo pacman -S pipewire', - ), - ], - _LinuxDistro.other => const [], - }; - - return StartupDependencyIssue( - id: 'linux-pipewire-runtime', - title: 'PipeWire runtime is missing', - summary: - 'Chanora uses PipeWire as the primary Linux voice backend. Without it, Chanora will try the PulseAudio fallback.', - details: const [ - 'Install the PipeWire runtime package for your distribution.', - 'After installing it, restart Chanora and tap Recheck.', - ], - severity: StartupDependencySeverity.recommended, - installHints: hints, - ); -} - -StartupDependencyIssue _buildPulseAudioIssue(_LinuxDistro distro) { - final List hints = switch (distro) { - _LinuxDistro.debian => const [ - StartupInstallHint( - label: 'Debian / Ubuntu', - command: 'sudo apt install libpulse0', - ), - ], - _LinuxDistro.fedora => const [ - StartupInstallHint( - label: 'Fedora', - command: 'sudo dnf install pulseaudio-libs', - ), - ], - _LinuxDistro.arch => const [ - StartupInstallHint( - label: 'Arch Linux', - command: 'sudo pacman -S libpulse', - ), - ], - _LinuxDistro.other => const [], - }; - - return StartupDependencyIssue( - id: 'linux-pulseaudio-runtime', - title: 'PulseAudio runtime is missing', - summary: - 'Chanora uses PulseAudio as the Linux fallback voice backend when PipeWire is unavailable.', - details: const [ - 'Install the PulseAudio client library package for your distribution.', - 'After installing it, restart Chanora and tap Recheck.', - ], - severity: StartupDependencySeverity.recommended, - installHints: hints, - ); -} - -StartupDependencyIssue _buildOnnxIssue({ - required String executableDir, - required _LinuxArch arch, -}) { - final bundlePath = '$executableDir/lib/libonnxruntime.so'; - final archivePrefix = switch (arch) { - _LinuxArch.x64 => 'onnxruntime-linux-x64-.tgz', - _LinuxArch.arm64 => 'onnxruntime-linux-aarch64-.tgz', - _LinuxArch.other => 'onnxruntime-linux--.tgz', - }; - final archiveDescription = switch (arch) { - _LinuxArch.x64 => - 'This machine needs the Linux x64 CPU archive ($archivePrefix).', - _LinuxArch.arm64 => - 'This machine needs the Linux ARM64 / aarch64 CPU archive ($archivePrefix).', - _LinuxArch.other => - 'Download the Linux CPU archive that matches this machine ($archivePrefix).', - }; - return StartupDependencyIssue( - id: 'linux-onnxruntime', - title: 'ONNX Runtime is not available', - summary: - 'Silero voice activity detection needs libonnxruntime.so. Chanora can continue, but it will fall back to simpler voice detection.', - details: [ - archiveDescription, - 'Open the official ONNX Runtime releases page, download the matching Linux CPU archive, then extract it.', - 'Inside the extracted archive, use the file in lib/ named libonnxruntime.so (or the versioned libonnxruntime.so.* file it points to).', - 'Use a Chanora build that already bundles ONNX Runtime, or place libonnxruntime.so in the app bundle lib/ directory.', - 'You can also point Chanora at an existing ONNX Runtime shared library with ORT_DYLIB_PATH.', - ], - severity: StartupDependencySeverity.recommended, - installHints: [ - StartupInstallHint( - label: switch (arch) { - _LinuxArch.x64 => 'Download Linux x64 archive', - _LinuxArch.arm64 => 'Download Linux ARM64 archive', - _LinuxArch.other => 'Open release downloads', - }, - command: 'https://github.com/microsoft/onnxruntime/releases', - ), - StartupInstallHint( - label: 'Archive name to look for', - command: archivePrefix, - ), - const StartupInstallHint( - label: 'Official install reference', - command: 'https://onnxruntime.ai/docs/install/', - ), - const StartupInstallHint( - label: 'Temporary shell setup', - command: 'export ORT_DYLIB_PATH=/absolute/path/to/libonnxruntime.so', - ), - StartupInstallHint( - label: 'Bundle into this app', - command: 'cp /absolute/path/to/libonnxruntime.so $bundlePath', - ), - StartupInstallHint(label: 'Bundled file location', command: bundlePath), - ], - ); -} - -@visibleForTesting -void debugResetStartupDependencyCheck({ - bool Function(String)? libraryProbe, - Future Function(String path)? fileExists, - String Function()? resolvedExecutableProvider, - String Function()? currentDirectoryProvider, - Future Function()? osReleaseProvider, - bool Function()? platformIsLinux, - String Function()? logFilePathProvider, - Abi Function()? currentAbiProvider, -}) { - _libraryProbe = libraryProbe ?? _defaultLibraryProbe; - _fileExists = fileExists ?? _defaultFileExists; - _resolvedExecutableProvider = - resolvedExecutableProvider ?? (() => Platform.resolvedExecutable); - _currentDirectoryProvider = - currentDirectoryProvider ?? (() => Directory.current.path); - _osReleaseProvider = osReleaseProvider ?? _defaultOsReleaseProvider; - _platformIsLinux = platformIsLinux ?? (() => Platform.isLinux); - _logFilePathProvider = logFilePathProvider ?? rust.logFilePathStr; - _currentAbiProvider = currentAbiProvider ?? Abi.current; -} diff --git a/apps/chanora_flutter/lib/services/ui_preferences_service.dart b/apps/chanora_flutter/lib/services/ui_preferences_service.dart index 5834d96..c0b99a9 100644 --- a/apps/chanora_flutter/lib/services/ui_preferences_service.dart +++ b/apps/chanora_flutter/lib/services/ui_preferences_service.dart @@ -1,53 +1,35 @@ -import 'dart:convert'; - import 'package:shared_preferences/shared_preferences.dart'; +enum UiThemeMode { + system, + light, + dark; + + static UiThemeMode fromStorage(String? value) { + return UiThemeMode.values.firstWhere( + (mode) => mode.name == value, + orElse: () => UiThemeMode.system, + ); + } +} + class UiSettings { - const UiSettings({this.host = '', this.nickname = ''}); + const UiSettings({ + this.host = '', + this.nickname = '', + this.themeMode = UiThemeMode.system, + }); final String host; final String nickname; -} - -class ClientPlaybackPreference { - const ClientPlaybackPreference({this.volume = 1.0, this.muted = false}); - - final double volume; - final bool muted; - - double get appliedVolume => muted ? 0.0 : volume; - - ClientPlaybackPreference copyWith({double? volume, bool? muted}) { - return ClientPlaybackPreference( - volume: _clampVolume(volume ?? this.volume), - muted: muted ?? this.muted, - ); - } - - Map toJson() => { - 'volume': _clampVolume(volume), - 'muted': muted, - }; - - static ClientPlaybackPreference fromJson(Map json) { - final volume = json['volume']; - return ClientPlaybackPreference( - volume: _clampVolume(volume is num ? volume.toDouble() : 1.0), - muted: json['muted'] as bool? ?? false, - ); - } - - static double _clampVolume(double volume) { - if (!volume.isFinite) return 1.0; - return volume.clamp(0.0, 4.0); - } + final UiThemeMode themeMode; } class UiPreferencesService { static const _hostKey = 'ui.host'; static const _nicknameKey = 'ui.nickname'; + static const _themeModeKey = 'ui.theme_mode'; static const _permissionsExplainedKey = 'perms_explained'; - static const _clientPlaybackPrefsKey = 'audio.client_playback_prefs'; const UiPreferencesService(); @@ -56,6 +38,7 @@ class UiPreferencesService { return UiSettings( host: prefs.getString(_hostKey) ?? '', nickname: prefs.getString(_nicknameKey) ?? '', + themeMode: UiThemeMode.fromStorage(prefs.getString(_themeModeKey)), ); } @@ -65,6 +48,11 @@ class UiPreferencesService { if (nickname != null) await prefs.setString(_nicknameKey, nickname); } + Future saveThemeMode(UiThemeMode themeMode) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_themeModeKey, themeMode.name); + } + Future hasExplainedPermissions() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool(_permissionsExplainedKey) ?? false; @@ -74,80 +62,4 @@ class UiPreferencesService { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_permissionsExplainedKey, true); } - - Future> - loadClientPlaybackPreferencesForServer(String serverHost) async { - final normalizedHost = _normalizeServerHost(serverHost); - if (normalizedHost.isEmpty) return const {}; - - final prefs = await SharedPreferences.getInstance(); - final raw = prefs.getString(_clientPlaybackPrefsKey); - if (raw == null || raw.isEmpty) return const {}; - - final decoded = _decodeClientPlaybackPreferences(raw); - - final result = {}; - for (final entry in decoded.entries) { - final key = entry.key; - final value = entry.value; - final (host, uid) = _splitCompositeKey(key); - if (host != normalizedHost || - uid.isEmpty || - value is! Map) { - continue; - } - result[uid] = ClientPlaybackPreference.fromJson(value); - } - return result; - } - - Future saveClientPlaybackPreference({ - required String serverHost, - required String userUid, - double? volume, - bool? muted, - }) async { - final normalizedHost = _normalizeServerHost(serverHost); - final normalizedUid = userUid.trim(); - if (normalizedHost.isEmpty || normalizedUid.isEmpty) return; - - final prefs = await SharedPreferences.getInstance(); - final current = await loadClientPlaybackPreferencesForServer(serverHost); - final previous = current[normalizedUid] ?? const ClientPlaybackPreference(); - final updated = previous.copyWith(volume: volume, muted: muted); - - final raw = prefs.getString(_clientPlaybackPrefsKey); - final decoded = _decodeClientPlaybackPreferences(raw); - final compositeKey = _compositeKey(normalizedHost, normalizedUid); - - if (updated.volume == 1.0 && !updated.muted) { - decoded.remove(compositeKey); - } else { - decoded[compositeKey] = updated.toJson(); - } - - await prefs.setString(_clientPlaybackPrefsKey, jsonEncode(decoded)); - } - - String _compositeKey(String normalizedHost, String normalizedUid) { - return '$normalizedHost|$normalizedUid'; - } - - (String, String) _splitCompositeKey(String key) { - final index = key.indexOf('|'); - if (index == -1) return ('', ''); - return (key.substring(0, index), key.substring(index + 1)); - } - - static String _normalizeServerHost(String host) => host.trim().toLowerCase(); - - Map _decodeClientPlaybackPreferences(String? raw) { - if (raw == null || raw.isEmpty) return {}; - try { - final decoded = jsonDecode(raw); - return decoded is Map ? decoded : {}; - } catch (_) { - return {}; - } - } } diff --git a/apps/chanora_flutter/lib/src/rust/api.dart b/apps/chanora_flutter/lib/src/rust/api.dart index fe9ece5..600aab2 100644 --- a/apps/chanora_flutter/lib/src/rust/api.dart +++ b/apps/chanora_flutter/lib/src/rust/api.dart @@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart' hide protected; part 'api.freezed.dart'; // These functions are ignored because they are not marked as `pub`: `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from` // These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate` /// Return the platform-conventional log-file path as a string, or @@ -33,9 +33,19 @@ Future connect({ password: password, ); +/// Warm server address resolution for the active host field. This is +/// intentionally fire-and-forget from the UI perspective: it schedules +/// Rust-side prefetch work and never opens a TS3 session. +Future prefetchServer({required String host}) => + RustLib.instance.api.crateApiPrefetchServer(host: host); + /// Re-fetch a fresh snapshot from the active connection. Future snapshot() => RustLib.instance.api.crateApiSnapshot(); +/// Fetch richer profile and live connection details for one online client. +Future clientProfile({required BigInt clientId}) => + RustLib.instance.api.crateApiClientProfile(clientId: clientId); + /// Disconnect from the server. No-op if not connected. Future disconnect() => RustLib.instance.api.crateApiDisconnect(); @@ -198,8 +208,7 @@ Future sendChatMessage({ /// On non-Android targets or before a voice session opens the /// section is omitted. Per SDD-090 every field in that section is /// a device-side technical scalar — no PII admitted. -Future exportDiagnostics() => - RustLib.instance.api.crateApiExportDiagnostics(); +String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics(); /// Wire the identity persistence store to a platform-private /// directory. Should be called once on app start after Flutter has @@ -796,9 +805,6 @@ class BridgeClient { /// Stable client id. final BigInt id; - /// Stable TeamSpeak unique identifier. - final String uid; - /// Channel id the client is currently in. final BigInt channel; @@ -825,7 +831,6 @@ class BridgeClient { const BridgeClient({ required this.id, - required this.uid, required this.channel, required this.name, required this.inputMuted, @@ -839,7 +844,6 @@ class BridgeClient { @override int get hashCode => id.hashCode ^ - uid.hashCode ^ channel.hashCode ^ name.hashCode ^ inputMuted.hashCode ^ @@ -855,7 +859,6 @@ class BridgeClient { other is BridgeClient && runtimeType == other.runtimeType && id == other.id && - uid == other.uid && channel == other.channel && name == other.name && inputMuted == other.inputMuted && @@ -866,6 +869,172 @@ class BridgeClient { talkPowerGranted == other.talkPowerGranted; } +/// Rich profile and live connection details for one online client. +class BridgeClientProfile { + /// Stable online client id. + final BigInt id; + + /// Channel the client is currently in. + final BigInt channel; + + /// Nickname. + final String name; + + /// TeamSpeak unique id. + final String uniqueId; + + /// Stable TeamSpeak database id, when visible. + final BigInt? databaseId; + + /// ISO country code, when visible. + final String countryCode; + + /// User description, when visible. + final String description; + + /// Client version string. + final String version; + + /// Client platform string. + final String platform; + + /// Account creation time as Unix seconds. + final PlatformInt64? createdUnixSeconds; + + /// Last connection time as Unix seconds. + final PlatformInt64? lastConnectedUnixSeconds; + + /// Total historical connections. + final BigInt? connectionsTotal; + + /// Current online duration in seconds. + final PlatformInt64? onlineSeconds; + + /// Current idle time in milliseconds. + final PlatformInt64? idleMilliseconds; + + /// Current ping in milliseconds. + final PlatformInt64? pingMilliseconds; + + /// Client address. Empty when permission-gated. + final String clientAddress; + + /// Resolved server group names. + final List serverGroups; + + /// Resolved channel group name. + final String channelGroup; + + /// TeamSpeak avatar file path suffix. + final String avatarPath; + + /// Downloaded bytes this month. + final BigInt? bytesDownloadedMonth; + + /// Uploaded bytes this month. + final BigInt? bytesUploadedMonth; + + /// Downloaded bytes across all time. + final BigInt? bytesDownloadedTotal; + + /// Uploaded bytes across all time. + final BigInt? bytesUploadedTotal; + + /// Client-to-server total packet loss ratio. + final double? packetLossClientToServerTotal; + + /// Server-to-client total packet loss ratio. + final double? packetLossServerToClientTotal; + + const BridgeClientProfile({ + required this.id, + required this.channel, + required this.name, + required this.uniqueId, + this.databaseId, + required this.countryCode, + required this.description, + required this.version, + required this.platform, + this.createdUnixSeconds, + this.lastConnectedUnixSeconds, + this.connectionsTotal, + this.onlineSeconds, + this.idleMilliseconds, + this.pingMilliseconds, + required this.clientAddress, + required this.serverGroups, + required this.channelGroup, + required this.avatarPath, + this.bytesDownloadedMonth, + this.bytesUploadedMonth, + this.bytesDownloadedTotal, + this.bytesUploadedTotal, + this.packetLossClientToServerTotal, + this.packetLossServerToClientTotal, + }); + + @override + int get hashCode => + id.hashCode ^ + channel.hashCode ^ + name.hashCode ^ + uniqueId.hashCode ^ + databaseId.hashCode ^ + countryCode.hashCode ^ + description.hashCode ^ + version.hashCode ^ + platform.hashCode ^ + createdUnixSeconds.hashCode ^ + lastConnectedUnixSeconds.hashCode ^ + connectionsTotal.hashCode ^ + onlineSeconds.hashCode ^ + idleMilliseconds.hashCode ^ + pingMilliseconds.hashCode ^ + clientAddress.hashCode ^ + serverGroups.hashCode ^ + channelGroup.hashCode ^ + avatarPath.hashCode ^ + bytesDownloadedMonth.hashCode ^ + bytesUploadedMonth.hashCode ^ + bytesDownloadedTotal.hashCode ^ + bytesUploadedTotal.hashCode ^ + packetLossClientToServerTotal.hashCode ^ + packetLossServerToClientTotal.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BridgeClientProfile && + runtimeType == other.runtimeType && + id == other.id && + channel == other.channel && + name == other.name && + uniqueId == other.uniqueId && + databaseId == other.databaseId && + countryCode == other.countryCode && + description == other.description && + version == other.version && + platform == other.platform && + createdUnixSeconds == other.createdUnixSeconds && + lastConnectedUnixSeconds == other.lastConnectedUnixSeconds && + connectionsTotal == other.connectionsTotal && + onlineSeconds == other.onlineSeconds && + idleMilliseconds == other.idleMilliseconds && + pingMilliseconds == other.pingMilliseconds && + clientAddress == other.clientAddress && + serverGroups == other.serverGroups && + channelGroup == other.channelGroup && + avatarPath == other.avatarPath && + bytesDownloadedMonth == other.bytesDownloadedMonth && + bytesUploadedMonth == other.bytesUploadedMonth && + bytesDownloadedTotal == other.bytesDownloadedTotal && + bytesUploadedTotal == other.bytesUploadedTotal && + packetLossClientToServerTotal == + other.packetLossClientToServerTotal && + packetLossServerToClientTotal == other.packetLossServerToClientTotal; +} + /// Bridge effect owner for AEC/NS/AGC. enum BridgeEffectOwner { /// Platform-owned effect. diff --git a/apps/chanora_flutter/lib/src/rust/api.freezed.dart b/apps/chanora_flutter/lib/src/rust/api.freezed.dart index 0735ae0..724128d 100644 --- a/apps/chanora_flutter/lib/src/rust/api.freezed.dart +++ b/apps/chanora_flutter/lib/src/rust/api.freezed.dart @@ -248,7 +248,7 @@ return audioRouteChanged(_that.route);case _: class BridgeEvent_Connected extends BridgeEvent { const BridgeEvent_Connected({required this.serverName}): super._(); - + /// Server name reported by the server snapshot. final String serverName; @@ -315,7 +315,7 @@ as String, class BridgeEvent_Lost extends BridgeEvent { const BridgeEvent_Lost({required this.reason}): super._(); - + /// Reason classification from the protocol layer. final String reason; @@ -382,7 +382,7 @@ as String, class BridgeEvent_Reconnecting extends BridgeEvent { const BridgeEvent_Reconnecting({required this.attempt, required this.delaySecs}): super._(); - + /// 1-based attempt counter for the current outage. final int attempt; @@ -452,7 +452,7 @@ as int, class BridgeEvent_Disconnected extends BridgeEvent { const BridgeEvent_Disconnected({required this.reason}): super._(); - + /// Reason classification. final String reason; @@ -519,7 +519,7 @@ as String, class BridgeEvent_AudioStarted extends BridgeEvent { const BridgeEvent_AudioStarted(): super._(); - + @@ -551,7 +551,7 @@ String toString() { class BridgeEvent_AudioStopped extends BridgeEvent { const BridgeEvent_AudioStopped(): super._(); - + @@ -583,7 +583,7 @@ String toString() { class BridgeEvent_SnapshotChanged extends BridgeEvent { const BridgeEvent_SnapshotChanged({required this.channels, required this.clients}): super._(); - + /// Latest channel count. final int channels; @@ -653,7 +653,7 @@ as int, class BridgeEvent_PttCapability extends BridgeEvent { const BridgeEvent_PttCapability({required this.level, required this.backendId, required this.boundInputClass}): super._(); - + /// Stable capability identifier (`"L0Focused"`, /// `"L1GlobalShortcut"`, `"L2GlobalHoldToTalk"`, @@ -729,7 +729,7 @@ as String, class BridgeEvent_VoiceState extends BridgeEvent { const BridgeEvent_VoiceState({required this.inChannel, required this.transmitMode, required this.mute, required this.releaseTailMs, this.currentChannelId, this.pendingTargetChannelId, required this.canJoin, required this.canLeave, required this.joinSyncState, this.joinErrorCode}): super._(); - + /// True when the session is currently joined to a voice /// channel and the audio engine is running. @@ -824,7 +824,7 @@ as BridgeVoiceJoinErrorCode?, class BridgeEvent_InterruptionState extends BridgeEvent { const BridgeEvent_InterruptionState({required this.began, required this.shouldResume}): super._(); - + /// True when interruption began, false when it ended. final bool began; @@ -894,7 +894,7 @@ as bool, class BridgeEvent_PermissionState extends BridgeEvent { const BridgeEvent_PermissionState({required this.permission, required this.state}): super._(); - + /// Canonical Android permission identifier. final String permission; @@ -964,7 +964,7 @@ as PermissionStateKind, class BridgeEvent_ChatMessage extends BridgeEvent { const BridgeEvent_ChatMessage({required this.senderId, required this.senderName, required this.message, required this.target}): super._(); - + /// Client id of the sender. final BigInt senderId; @@ -1037,7 +1037,7 @@ as BridgeMessageTarget, @override @pragma('vm:prefer-inline') $BridgeMessageTargetCopyWith<$Res> get target { - + return $BridgeMessageTargetCopyWith<$Res>(_self.target, (value) { return _then(_self.copyWith(target: value)); }); @@ -1049,7 +1049,7 @@ $BridgeMessageTargetCopyWith<$Res> get target { class BridgeEvent_ServerActivity extends BridgeEvent { const BridgeEvent_ServerActivity({required this.message}): super._(); - + /// TeamSpeak-style activity line. final String message; @@ -1116,7 +1116,7 @@ as String, class BridgeEvent_AudioRouteChanged extends BridgeEvent { const BridgeEvent_AudioRouteChanged({required this.route}): super._(); - + /// The new audio route. final BridgeAudioRoute route; @@ -1355,7 +1355,7 @@ return poke(_that.field0);case _: class BridgeMessageTarget_Server extends BridgeMessageTarget { const BridgeMessageTarget_Server(): super._(); - + @@ -1387,7 +1387,7 @@ String toString() { class BridgeMessageTarget_Channel extends BridgeMessageTarget { const BridgeMessageTarget_Channel(): super._(); - + @@ -1419,7 +1419,7 @@ String toString() { class BridgeMessageTarget_Client extends BridgeMessageTarget { const BridgeMessageTarget_Client(this.field0): super._(); - + final BigInt field0; @@ -1485,7 +1485,7 @@ as BigInt, class BridgeMessageTarget_Poke extends BridgeMessageTarget { const BridgeMessageTarget_Poke(this.field0): super._(); - + final BigInt field0; diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.dart index df5ef0e..82bbb34 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.dart @@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -560177922; + int get rustContentHash => 281698435; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -87,6 +87,8 @@ abstract class RustLibApi extends BaseApi { Future crateApiBridgeInit(); + Future crateApiClientProfile({required BigInt clientId}); + Future crateApiConnect({ required String host, required String nickname, @@ -101,7 +103,7 @@ abstract class RustLibApi extends BaseApi { Stream crateApiEventsStream(); - Future crateApiExportDiagnostics(); + String crateApiExportDiagnostics(); Future crateApiGetAudioProcessingConfig(); @@ -134,6 +136,8 @@ abstract class RustLibApi extends BaseApi { required String password, }); + Future crateApiPrefetchServer({required String host}); + Future crateApiPttDescriptor(); void crateApiRecordLifecycleEvent({required String state}); @@ -314,6 +318,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiBridgeInitConstMeta => const TaskConstMeta(debugName: "bridge_init", argNames: []); + @override + Future crateApiClientProfile({ + required BigInt clientId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_u_64(clientId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_client_profile, + decodeErrorData: sse_decode_bridge_error, + ), + constMeta: kCrateApiClientProfileConstMeta, + argValues: [clientId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiClientProfileConstMeta => + const TaskConstMeta(debugName: "client_profile", argNames: ["clientId"]); + @override Future crateApiConnect({ required String host, @@ -330,7 +364,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 6, port: port_, ); }, @@ -360,7 +394,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 7, port: port_, ); }, @@ -387,7 +421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, @@ -415,7 +449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 8, + funcId: 9, port: port_, ); }, @@ -448,7 +482,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 9, + funcId: 10, port: port_, ); }, @@ -469,21 +503,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: "events_stream", argNames: ["sink"]); @override - Future crateApiExportDiagnostics() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { + String crateApiExportDiagnostics() { + return handler.executeSync( + SyncTask( + callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 10, - port: port_, - ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_bridge_error, + decodeErrorData: null, ), constMeta: kCrateApiExportDiagnosticsConstMeta, argValues: [], @@ -504,7 +533,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 11, + funcId: 12, port: port_, ); }, @@ -534,7 +563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 13, port: port_, ); }, @@ -561,7 +590,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 14, port: port_, ); }, @@ -588,7 +617,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 15, port: port_, ); }, @@ -612,7 +641,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -635,7 +664,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_bool(shouldResume, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -661,7 +690,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(routeClass, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -687,7 +716,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_bridge_audio_route(route, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -715,7 +744,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 20, port: port_, ); }, @@ -742,7 +771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 21, port: port_, ); }, @@ -769,7 +798,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 21, + funcId: 22, port: port_, ); }, @@ -796,7 +825,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 23, port: port_, ); }, @@ -820,7 +849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -850,7 +879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 25, port: port_, ); }, @@ -870,6 +899,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["channelId", "password"], ); + @override + Future crateApiPrefetchServer({required String host}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(host, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 26, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_bridge_error, + ), + constMeta: kCrateApiPrefetchServerConstMeta, + argValues: [host], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiPrefetchServerConstMeta => + const TaskConstMeta(debugName: "prefetch_server", argNames: ["host"]); + @override Future crateApiPttDescriptor() { return handler.executeNormal( @@ -879,7 +936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 27, port: port_, ); }, @@ -904,7 +961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(state, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -937,7 +994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 29, port: port_, ); }, @@ -964,7 +1021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_bridge_audio_route(route, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -998,7 +1055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 31, port: port_, ); }, @@ -1033,7 +1090,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 30, + funcId: 32, port: port_, ); }, @@ -1063,7 +1120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 33, port: port_, ); }, @@ -1091,7 +1148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 34, port: port_, ); }, @@ -1119,7 +1176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 33, + funcId: 35, port: port_, ); }, @@ -1149,7 +1206,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 36, port: port_, ); }, @@ -1177,7 +1234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_bridge_network_state(state, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1203,7 +1260,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 38, port: port_, ); }, @@ -1231,7 +1288,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 39, port: port_, ); }, @@ -1259,7 +1316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 40, port: port_, ); }, @@ -1287,7 +1344,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 41, port: port_, ); }, @@ -1319,7 +1376,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 42, port: port_, ); }, @@ -1349,7 +1406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 43, port: port_, ); }, @@ -1377,7 +1434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 44, port: port_, ); }, @@ -1405,7 +1462,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 45, port: port_, ); }, @@ -1432,7 +1489,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 46, port: port_, ); }, @@ -1460,7 +1517,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 47, port: port_, ); }, @@ -1492,7 +1549,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 48, port: port_, ); }, @@ -1521,7 +1578,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 49, port: port_, ); }, @@ -1594,12 +1651,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_bridge_voice_join_error_code(raw); } + @protected + double dco_decode_box_autoadd_f_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + @protected int dco_decode_box_autoadd_i_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw as int; } + @protected + PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_i_64(raw); + } + @protected BigInt dco_decode_box_autoadd_u_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1756,19 +1825,53 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { BridgeClient dco_decode_bridge_client(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 10) - throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); + if (arr.length != 9) + throw Exception('unexpected arr length: expect 9 but see ${arr.length}'); return BridgeClient( id: dco_decode_u_64(arr[0]), - uid: dco_decode_String(arr[1]), - channel: dco_decode_u_64(arr[2]), - name: dco_decode_String(arr[3]), - inputMuted: dco_decode_bool(arr[4]), - outputMuted: dco_decode_bool(arr[5]), - isSpeaking: dco_decode_bool(arr[6]), - isServerQuery: dco_decode_bool(arr[7]), - talkPower: dco_decode_i_32(arr[8]), - talkPowerGranted: dco_decode_bool(arr[9]), + channel: dco_decode_u_64(arr[1]), + name: dco_decode_String(arr[2]), + inputMuted: dco_decode_bool(arr[3]), + outputMuted: dco_decode_bool(arr[4]), + isSpeaking: dco_decode_bool(arr[5]), + isServerQuery: dco_decode_bool(arr[6]), + talkPower: dco_decode_i_32(arr[7]), + talkPowerGranted: dco_decode_bool(arr[8]), + ); + } + + @protected + BridgeClientProfile dco_decode_bridge_client_profile(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 25) + throw Exception('unexpected arr length: expect 25 but see ${arr.length}'); + return BridgeClientProfile( + id: dco_decode_u_64(arr[0]), + channel: dco_decode_u_64(arr[1]), + name: dco_decode_String(arr[2]), + uniqueId: dco_decode_String(arr[3]), + databaseId: dco_decode_opt_box_autoadd_u_64(arr[4]), + countryCode: dco_decode_String(arr[5]), + description: dco_decode_String(arr[6]), + version: dco_decode_String(arr[7]), + platform: dco_decode_String(arr[8]), + createdUnixSeconds: dco_decode_opt_box_autoadd_i_64(arr[9]), + lastConnectedUnixSeconds: dco_decode_opt_box_autoadd_i_64(arr[10]), + connectionsTotal: dco_decode_opt_box_autoadd_u_64(arr[11]), + onlineSeconds: dco_decode_opt_box_autoadd_i_64(arr[12]), + idleMilliseconds: dco_decode_opt_box_autoadd_i_64(arr[13]), + pingMilliseconds: dco_decode_opt_box_autoadd_i_64(arr[14]), + clientAddress: dco_decode_String(arr[15]), + serverGroups: dco_decode_list_String(arr[16]), + channelGroup: dco_decode_String(arr[17]), + avatarPath: dco_decode_String(arr[18]), + bytesDownloadedMonth: dco_decode_opt_box_autoadd_u_64(arr[19]), + bytesUploadedMonth: dco_decode_opt_box_autoadd_u_64(arr[20]), + bytesDownloadedTotal: dco_decode_opt_box_autoadd_u_64(arr[21]), + bytesUploadedTotal: dco_decode_opt_box_autoadd_u_64(arr[22]), + packetLossClientToServerTotal: dco_decode_opt_box_autoadd_f_32(arr[23]), + packetLossServerToClientTotal: dco_decode_opt_box_autoadd_f_32(arr[24]), ); } @@ -2004,6 +2107,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeI64(raw); } + @protected + List dco_decode_list_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_String).toList(); + } + @protected List dco_decode_list_bridge_audio_device(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2049,12 +2158,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { : dco_decode_box_autoadd_bridge_voice_join_error_code(raw); } + @protected + double? dco_decode_opt_box_autoadd_f_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_f_32(raw); + } + @protected int? dco_decode_opt_box_autoadd_i_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw == null ? null : dco_decode_box_autoadd_i_32(raw); } + @protected + PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_i_64(raw); + } + @protected BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2152,12 +2273,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_bridge_voice_join_error_code(deserializer)); } + @protected + double sse_decode_box_autoadd_f_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_f_32(deserializer)); + } + @protected int sse_decode_box_autoadd_i_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs return (sse_decode_i_32(deserializer)); } + @protected + PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_i_64(deserializer)); + } + @protected BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2358,7 +2491,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { BridgeClient sse_decode_bridge_client(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_id = sse_decode_u_64(deserializer); - var var_uid = sse_decode_String(deserializer); var var_channel = sse_decode_u_64(deserializer); var var_name = sse_decode_String(deserializer); var var_inputMuted = sse_decode_bool(deserializer); @@ -2369,7 +2501,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_talkPowerGranted = sse_decode_bool(deserializer); return BridgeClient( id: var_id, - uid: var_uid, channel: var_channel, name: var_name, inputMuted: var_inputMuted, @@ -2381,6 +2512,75 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BridgeClientProfile sse_decode_bridge_client_profile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_u_64(deserializer); + var var_channel = sse_decode_u_64(deserializer); + var var_name = sse_decode_String(deserializer); + var var_uniqueId = sse_decode_String(deserializer); + var var_databaseId = sse_decode_opt_box_autoadd_u_64(deserializer); + var var_countryCode = sse_decode_String(deserializer); + var var_description = sse_decode_String(deserializer); + var var_version = sse_decode_String(deserializer); + var var_platform = sse_decode_String(deserializer); + var var_createdUnixSeconds = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_lastConnectedUnixSeconds = sse_decode_opt_box_autoadd_i_64( + deserializer, + ); + var var_connectionsTotal = sse_decode_opt_box_autoadd_u_64(deserializer); + var var_onlineSeconds = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_idleMilliseconds = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_pingMilliseconds = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_clientAddress = sse_decode_String(deserializer); + var var_serverGroups = sse_decode_list_String(deserializer); + var var_channelGroup = sse_decode_String(deserializer); + var var_avatarPath = sse_decode_String(deserializer); + var var_bytesDownloadedMonth = sse_decode_opt_box_autoadd_u_64( + deserializer, + ); + var var_bytesUploadedMonth = sse_decode_opt_box_autoadd_u_64(deserializer); + var var_bytesDownloadedTotal = sse_decode_opt_box_autoadd_u_64( + deserializer, + ); + var var_bytesUploadedTotal = sse_decode_opt_box_autoadd_u_64(deserializer); + var var_packetLossClientToServerTotal = sse_decode_opt_box_autoadd_f_32( + deserializer, + ); + var var_packetLossServerToClientTotal = sse_decode_opt_box_autoadd_f_32( + deserializer, + ); + return BridgeClientProfile( + id: var_id, + channel: var_channel, + name: var_name, + uniqueId: var_uniqueId, + databaseId: var_databaseId, + countryCode: var_countryCode, + description: var_description, + version: var_version, + platform: var_platform, + createdUnixSeconds: var_createdUnixSeconds, + lastConnectedUnixSeconds: var_lastConnectedUnixSeconds, + connectionsTotal: var_connectionsTotal, + onlineSeconds: var_onlineSeconds, + idleMilliseconds: var_idleMilliseconds, + pingMilliseconds: var_pingMilliseconds, + clientAddress: var_clientAddress, + serverGroups: var_serverGroups, + channelGroup: var_channelGroup, + avatarPath: var_avatarPath, + bytesDownloadedMonth: var_bytesDownloadedMonth, + bytesUploadedMonth: var_bytesUploadedMonth, + bytesDownloadedTotal: var_bytesDownloadedTotal, + bytesUploadedTotal: var_bytesUploadedTotal, + packetLossClientToServerTotal: var_packetLossClientToServerTotal, + packetLossServerToClientTotal: var_packetLossServerToClientTotal, + ); + } + @protected BridgeEffectOwner sse_decode_bridge_effect_owner( SseDeserializer deserializer, @@ -2680,6 +2880,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getPlatformInt64(); } + @protected + List sse_decode_list_String(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_String(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_bridge_audio_device( SseDeserializer deserializer, @@ -2770,6 +2982,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_f_32(deserializer)); + } else { + return null; + } + } + @protected int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2781,6 +3004,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_i_64(deserializer)); + } else { + return null; + } + } + @protected BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2898,12 +3132,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bridge_voice_join_error_code(self, serializer); } + @protected + void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_f_32(self, serializer); + } + @protected void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_i_32(self, serializer); } + @protected + void sse_encode_box_autoadd_i_64( + PlatformInt64 self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_64(self, serializer); + } + @protected void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3056,6 +3305,45 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bool(self.talkPowerGranted, serializer); } + @protected + void sse_encode_bridge_client_profile( + BridgeClientProfile self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self.id, serializer); + sse_encode_u_64(self.channel, serializer); + sse_encode_String(self.name, serializer); + sse_encode_String(self.uniqueId, serializer); + sse_encode_opt_box_autoadd_u_64(self.databaseId, serializer); + sse_encode_String(self.countryCode, serializer); + sse_encode_String(self.description, serializer); + sse_encode_String(self.version, serializer); + sse_encode_String(self.platform, serializer); + sse_encode_opt_box_autoadd_i_64(self.createdUnixSeconds, serializer); + sse_encode_opt_box_autoadd_i_64(self.lastConnectedUnixSeconds, serializer); + sse_encode_opt_box_autoadd_u_64(self.connectionsTotal, serializer); + sse_encode_opt_box_autoadd_i_64(self.onlineSeconds, serializer); + sse_encode_opt_box_autoadd_i_64(self.idleMilliseconds, serializer); + sse_encode_opt_box_autoadd_i_64(self.pingMilliseconds, serializer); + sse_encode_String(self.clientAddress, serializer); + sse_encode_list_String(self.serverGroups, serializer); + sse_encode_String(self.channelGroup, serializer); + sse_encode_String(self.avatarPath, serializer); + sse_encode_opt_box_autoadd_u_64(self.bytesDownloadedMonth, serializer); + sse_encode_opt_box_autoadd_u_64(self.bytesUploadedMonth, serializer); + sse_encode_opt_box_autoadd_u_64(self.bytesDownloadedTotal, serializer); + sse_encode_opt_box_autoadd_u_64(self.bytesUploadedTotal, serializer); + sse_encode_opt_box_autoadd_f_32( + self.packetLossClientToServerTotal, + serializer, + ); + sse_encode_opt_box_autoadd_f_32( + self.packetLossServerToClientTotal, + serializer, + ); + } + @protected void sse_encode_bridge_effect_owner( BridgeEffectOwner self, @@ -3330,6 +3618,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putPlatformInt64(self); } + @protected + void sse_encode_list_String(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_String(item, serializer); + } + } + @protected void sse_encode_list_bridge_audio_device( List self, @@ -3411,6 +3708,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_f_32(double? 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_f_32(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3421,6 +3728,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_i_64( + PlatformInt64? 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_i_64(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart index cdcd3a7..dbde565 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.io.dart @@ -48,9 +48,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + double dco_decode_box_autoadd_f_32(dynamic raw); + @protected int dco_decode_box_autoadd_i_32(dynamic raw); + @protected + PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); + @protected BigInt dco_decode_box_autoadd_u_64(dynamic raw); @@ -88,6 +94,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BridgeClient dco_decode_bridge_client(dynamic raw); + @protected + BridgeClientProfile dco_decode_bridge_client_profile(dynamic raw); + @protected BridgeEffectOwner dco_decode_bridge_effect_owner(dynamic raw); @@ -141,6 +150,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 dco_decode_i_64(dynamic raw); + @protected + List dco_decode_list_String(dynamic raw); + @protected List dco_decode_list_bridge_audio_device(dynamic raw); @@ -163,9 +175,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { BridgeVoiceJoinErrorCode? dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw); + @protected + double? dco_decode_opt_box_autoadd_f_32(dynamic raw); + @protected int? dco_decode_opt_box_autoadd_i_32(dynamic raw); + @protected + PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); + @protected BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); @@ -219,9 +237,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + double sse_decode_box_autoadd_f_32(SseDeserializer deserializer); + @protected int sse_decode_box_autoadd_i_32(SseDeserializer deserializer); + @protected + PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); + @protected BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer); @@ -265,6 +289,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BridgeClient sse_decode_bridge_client(SseDeserializer deserializer); + @protected + BridgeClientProfile sse_decode_bridge_client_profile( + SseDeserializer deserializer, + ); + @protected BridgeEffectOwner sse_decode_bridge_effect_owner( SseDeserializer deserializer, @@ -334,6 +363,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + @protected + List sse_decode_list_String(SseDeserializer deserializer); + @protected List sse_decode_list_bridge_audio_device( SseDeserializer deserializer, @@ -366,9 +398,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer); + @protected int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer); + @protected + PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); + @protected BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); @@ -431,9 +469,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_i_64( + PlatformInt64 self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer); @@ -491,6 +538,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_bridge_client(BridgeClient self, SseSerializer serializer); + @protected + void sse_encode_bridge_client_profile( + BridgeClientProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_bridge_effect_owner( BridgeEffectOwner self, @@ -578,6 +631,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + @protected void sse_encode_list_bridge_audio_device( List self, @@ -617,9 +673,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_i_64( + PlatformInt64? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart index 20bd007..f345a9b 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.web.dart @@ -50,9 +50,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + double dco_decode_box_autoadd_f_32(dynamic raw); + @protected int dco_decode_box_autoadd_i_32(dynamic raw); + @protected + PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); + @protected BigInt dco_decode_box_autoadd_u_64(dynamic raw); @@ -90,6 +96,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BridgeClient dco_decode_bridge_client(dynamic raw); + @protected + BridgeClientProfile dco_decode_bridge_client_profile(dynamic raw); + @protected BridgeEffectOwner dco_decode_bridge_effect_owner(dynamic raw); @@ -143,6 +152,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 dco_decode_i_64(dynamic raw); + @protected + List dco_decode_list_String(dynamic raw); + @protected List dco_decode_list_bridge_audio_device(dynamic raw); @@ -165,9 +177,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { BridgeVoiceJoinErrorCode? dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw); + @protected + double? dco_decode_opt_box_autoadd_f_32(dynamic raw); + @protected int? dco_decode_opt_box_autoadd_i_32(dynamic raw); + @protected + PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); + @protected BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); @@ -221,9 +239,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + double sse_decode_box_autoadd_f_32(SseDeserializer deserializer); + @protected int sse_decode_box_autoadd_i_32(SseDeserializer deserializer); + @protected + PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); + @protected BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer); @@ -267,6 +291,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BridgeClient sse_decode_bridge_client(SseDeserializer deserializer); + @protected + BridgeClientProfile sse_decode_bridge_client_profile( + SseDeserializer deserializer, + ); + @protected BridgeEffectOwner sse_decode_bridge_effect_owner( SseDeserializer deserializer, @@ -336,6 +365,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + @protected + List sse_decode_list_String(SseDeserializer deserializer); + @protected List sse_decode_list_bridge_audio_device( SseDeserializer deserializer, @@ -368,9 +400,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer); + @protected int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer); + @protected + PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); + @protected BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); @@ -433,9 +471,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_i_64( + PlatformInt64 self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer); @@ -493,6 +540,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_bridge_client(BridgeClient self, SseSerializer serializer); + @protected + void sse_encode_bridge_client_profile( + BridgeClientProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_bridge_effect_owner( BridgeEffectOwner self, @@ -580,6 +633,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + @protected void sse_encode_list_bridge_audio_device( List self, @@ -619,9 +675,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_i_64( + PlatformInt64? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); diff --git a/apps/chanora_flutter/lib/widgets/audio_output_tile.dart b/apps/chanora_flutter/lib/widgets/audio_output_tile.dart index aad16cd..76133bb 100644 --- a/apps/chanora_flutter/lib/widgets/audio_output_tile.dart +++ b/apps/chanora_flutter/lib/widgets/audio_output_tile.dart @@ -313,47 +313,51 @@ class _AndroidAudioOutputPickerSheet extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppL10n.of(context); final theme = Theme.of(context); + final maxHeight = MediaQuery.sizeOf(context).height * 0.72; return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(l10n.audioOutputLabel, style: theme.textTheme.titleLarge), - const SizedBox(height: 16), - _PickerRow( - icon: Icons.speaker, - label: l10n.audioRouteSystemDefault, - selected: !devices.any((d) => d.isSelected), - onTap: () => Navigator.of(context).pop('auto'), - ), - if (loading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 24), - child: Center(child: CircularProgressIndicator()), - ) - else if (devices.isEmpty) - TextButton.icon( - onPressed: onRefresh, - icon: const Icon(Icons.refresh), - label: Text(l10n.audioRouteRefreshDevices), - ) - else - for (final device in devices) - _PickerRow( - icon: AudioOutputTileState._androidDeviceIcon(device.type), - label: AudioOutputTileState._androidDeviceLabel( - device.type, - device.name, - l10n, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(l10n.audioOutputLabel, style: theme.textTheme.titleLarge), + const SizedBox(height: 16), + _PickerRow( + icon: Icons.speaker, + label: l10n.audioRouteSystemDefault, + selected: !devices.any((d) => d.isSelected), + onTap: () => Navigator.of(context).pop('auto'), + ), + if (loading) + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Center(child: CircularProgressIndicator()), + ) + else if (devices.isEmpty) + TextButton.icon( + onPressed: onRefresh, + icon: const Icon(Icons.refresh), + label: Text(l10n.audioRouteRefreshDevices), + ) + else + for (final device in devices) + _PickerRow( + icon: AudioOutputTileState._androidDeviceIcon(device.type), + label: AudioOutputTileState._androidDeviceLabel( + device.type, + device.name, + l10n, + ), + selected: device.isSelected, + onTap: device.isAvailableForCommunication + ? () => Navigator.of(context).pop(device.id) + : null, ), - selected: device.isSelected, - onTap: device.isAvailableForCommunication - ? () => Navigator.of(context).pop(device.id) - : null, - ), - ], + ], + ), ), ), ); diff --git a/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart b/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart index 6a030ca..1ed4a77 100644 --- a/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart +++ b/apps/chanora_flutter/lib/widgets/audio_processing_config_state.dart @@ -201,32 +201,19 @@ bool androidShowsLimiterControl(AudioProcessingConfigState state) { /// Never leave the UI on the hidden disabled backend. /// -/// Desktop keeps Silero as the default, but still allows a user-chosen -/// WebRTC fallback when ONNX Runtime is unavailable. +/// Windows and Linux use Silero as the primary VAD; WebRTC is still +/// available internally as a runtime fallback. rust.BridgeVadBackend normalizedVadBackend( rust.BridgeVadBackend backend, { bool? isWindows, bool? isLinux, - bool onnxRuntimeAvailable = true, }) { - final normalized = backend == rust.BridgeVadBackend.disabled - ? rust.BridgeVadBackend.sileroOnnx - : backend; final desktop = (isWindows ?? Platform.isWindows) || (isLinux ?? Platform.isLinux); - if (desktop && - !onnxRuntimeAvailable && - normalized == rust.BridgeVadBackend.sileroOnnx) { - return rust.BridgeVadBackend.webrtcVad; - } - if (!desktop) { - return normalized; - } - return switch (normalized) { - rust.BridgeVadBackend.webrtcVad || - rust.BridgeVadBackend.sileroOnnx => normalized, - _ => rust.BridgeVadBackend.sileroOnnx, - }; + if (desktop) return rust.BridgeVadBackend.sileroOnnx; + return backend == rust.BridgeVadBackend.disabled + ? rust.BridgeVadBackend.sileroOnnx + : backend; } rust.BridgeIosVoiceProcessingMode normalizedIosProcessingMode( diff --git a/apps/chanora_flutter/lib/widgets/chat_views.dart b/apps/chanora_flutter/lib/widgets/chat_views.dart index 2b91dbf..ac3b29e 100644 --- a/apps/chanora_flutter/lib/widgets/chat_views.dart +++ b/apps/chanora_flutter/lib/widgets/chat_views.dart @@ -1,3 +1,5 @@ +import 'dart:async' show unawaited; + import 'package:flutter/material.dart'; import '../l10n/generated/app_localizations.dart'; @@ -9,12 +11,9 @@ import '../src/rust/api.dart' as rust; import 'bbcode_text.dart'; const double _chatSidebarTileExtent = 92; -const double _chatSidebarIndicatorExtent = 76; -const double _chatSidebarIconExtent = 24; -const double _chatSidebarIconSize = 20; -const BorderRadius _chatSidebarIndicatorRadius = BorderRadius.all( - Radius.circular(20), -); +const double _chatSidebarCompactTileExtent = 76; +const double _chatSidebarCompactHeight = 84; +const double _chatMobileBreakpoint = 600; /// One chat/activity message shown in the chat hub. class ChatEntry { @@ -616,12 +615,22 @@ class _ChatPageState extends State { final messages = _messages; final currentChannelId = _currentChannelId; final channelName = snapshotChannelName(snapshot, currentChannelId); + final l10n = AppL10n.of(context); + final detail = _ChatDetailView( + target: _selectedTarget, + clientName: _selectedClientName, + snapshot: snapshot, + messages: messages, + currentChannelId: currentChannelId, + channelName: channelName, + onTs3ServerLink: widget.onTs3ServerLink, + ); return Scaffold( appBar: AppBar( - title: Text('Chat — ${snapshot.serverName}'), + title: Text('${l10n.chatAction} - ${snapshot.serverName}'), actions: [ IconButton( - tooltip: 'Close chat', + tooltip: l10n.chatCloseAction, icon: const Icon(Icons.close), onPressed: _selectedPrivateClientId == null ? null @@ -629,9 +638,10 @@ class _ChatPageState extends State { ), ], ), - body: Row( - children: [ - _ChatSidebar( + body: LayoutBuilder( + builder: (context, constraints) { + final sidebar = _ChatSidebar( + compact: constraints.maxWidth < _chatMobileBreakpoint, selectedTarget: _selectedTarget, privateChats: _privateChats, onSelect: _selectTarget, @@ -639,20 +649,24 @@ class _ChatPageState extends State { _closedPrivateChats.remove(id); _selectTarget(rust.BridgeMessageTarget.client(id), name: name); }), - ), - const VerticalDivider(width: 1), - Expanded( - child: _ChatDetailView( - target: _selectedTarget, - clientName: _selectedClientName, - snapshot: snapshot, - messages: messages, - currentChannelId: currentChannelId, - channelName: channelName, - onTs3ServerLink: widget.onTs3ServerLink, - ), - ), - ], + ); + if (constraints.maxWidth < _chatMobileBreakpoint) { + return Column( + children: [ + sidebar, + const Divider(height: 1), + Expanded(child: detail), + ], + ); + } + return Row( + children: [ + sidebar, + const VerticalDivider(width: 1), + Expanded(child: detail), + ], + ); + }, ), ); } @@ -740,12 +754,14 @@ class _PrivateChatItem { class _ChatSidebar extends StatelessWidget { const _ChatSidebar({ + required this.compact, required this.selectedTarget, required this.privateChats, required this.onSelect, required this.onNewPrivateChat, }); + final bool compact; final rust.BridgeMessageTarget selectedTarget; final List<_PrivateChatItem> privateChats; final void Function(rust.BridgeMessageTarget target, {String name}) onSelect; @@ -753,64 +769,85 @@ class _ChatSidebar extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final dividerColor = theme.colorScheme.outlineVariant.withValues( - alpha: 0.45, + final l10n = AppL10n.of(context); + final fixedItems = [ + _ChatSidebarItem( + compact: compact, + icon: Icons.dns_outlined, + label: 'Server', + selected: selectedTarget is rust.BridgeMessageTarget_Server, + onTap: () => onSelect(const rust.BridgeMessageTarget.server()), + ), + _ChatSidebarItem( + compact: compact, + icon: Icons.tag, + label: 'Channel', + selected: selectedTarget is rust.BridgeMessageTarget_Channel, + onTap: () => onSelect(const rust.BridgeMessageTarget.channel()), + ), + ]; + final privateItems = [ + for (final chat in privateChats) + _ChatSidebarItem( + compact: compact, + icon: Icons.person_outline, + label: chat.name.isNotEmpty ? chat.name : 'Direct', + selected: switch (selectedTarget) { + rust.BridgeMessageTarget_Client(:final field0) => field0 == chat.id, + _ => false, + }, + onTap: () => onSelect( + rust.BridgeMessageTarget.client(chat.id), + name: chat.name, + ), + ), + ]; + final addButton = Padding( + padding: EdgeInsets.all(compact ? 4 : 8), + child: IconButton.filledTonal( + tooltip: l10n.chatNewPrivateAction, + icon: const Icon(Icons.add), + onPressed: onNewPrivateChat, + ), ); - return SizedBox( - width: _chatSidebarTileExtent, - child: Material( - color: theme.colorScheme.surfaceContainerLow, - child: Column( + final compactDivider = ColoredBox( + color: Theme.of(context).dividerColor, + child: const SizedBox(width: 1, height: double.infinity), + ); + + if (compact) { + return SizedBox( + height: _chatSidebarCompactHeight, + child: Row( children: [ - const SizedBox(height: 8), - _ChatSidebarItem( - icon: Icons.dns_outlined, - label: 'Server', - selected: selectedTarget is rust.BridgeMessageTarget_Server, - onTap: () => onSelect(const rust.BridgeMessageTarget.server()), - ), - _ChatSidebarItem( - icon: Icons.tag, - label: 'Channel', - selected: selectedTarget is rust.BridgeMessageTarget_Channel, - onTap: () => onSelect(const rust.BridgeMessageTarget.channel()), - ), - Divider(height: 1, indent: 12, endIndent: 12, color: dividerColor), + ...fixedItems, + compactDivider, Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 6), - itemCount: privateChats.length, - itemBuilder: (context, index) { - final chat = privateChats[index]; - final selected = switch (selectedTarget) { - rust.BridgeMessageTarget_Client(:final field0) => - field0 == chat.id, - _ => false, - }; - return _ChatSidebarItem( - icon: Icons.person_outline, - label: chat.name.isNotEmpty ? chat.name : 'Direct', - selected: selected, - onTap: () => onSelect( - rust.BridgeMessageTarget.client(chat.id), - name: chat.name, - ), - ); - }, - ), - ), - Divider(height: 1, indent: 12, endIndent: 12, color: dividerColor), - Padding( - padding: const EdgeInsets.fromLTRB(8, 10, 8, 12), - child: IconButton.filledTonal( - tooltip: 'New private chat', - icon: const Icon(Icons.add), - onPressed: onNewPrivateChat, + child: ListView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + children: privateItems, ), ), + compactDivider, + addButton, ], ), + ); + } + + return SizedBox( + width: _chatSidebarTileExtent, + child: Column( + children: [ + ...fixedItems, + const Divider(height: 1), + Expanded( + child: ListView(padding: EdgeInsets.zero, children: privateItems), + ), + const Divider(height: 1), + addButton, + ], ), ); } @@ -818,12 +855,14 @@ class _ChatSidebar extends StatelessWidget { class _ChatSidebarItem extends StatelessWidget { const _ChatSidebarItem({ + required this.compact, required this.icon, required this.label, required this.selected, required this.onTap, }); + final bool compact; final IconData icon; final String label; final bool selected; @@ -832,63 +871,57 @@ class _ChatSidebarItem extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final scheme = theme.colorScheme; - final indicatorColor = selected - ? scheme.primaryContainer - : Colors.transparent; - final contentColor = selected - ? scheme.onPrimaryContainer - : scheme.onSurfaceVariant; - final labelStyle = theme.textTheme.labelSmall?.copyWith( - color: contentColor, - height: 1.15, - fontWeight: selected ? FontWeight.w600 : FontWeight.w500, - ); - - return SizedBox( - width: _chatSidebarTileExtent, - height: _chatSidebarTileExtent, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: _chatSidebarIndicatorRadius, - child: Center( - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - width: _chatSidebarIndicatorExtent, - height: _chatSidebarIndicatorExtent, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - color: indicatorColor, - borderRadius: _chatSidebarIndicatorRadius, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: _chatSidebarIconExtent, - height: _chatSidebarIconExtent, - child: Icon( - icon, - size: _chatSidebarIconSize, - color: contentColor, - ), + final colorScheme = theme.colorScheme; + final fg = selected + ? colorScheme.onSecondaryContainer + : colorScheme.onSurface; + final tileExtent = compact + ? _chatSidebarCompactTileExtent + : _chatSidebarTileExtent; + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Ink( + width: tileExtent, + height: compact ? 72 : tileExtent, + decoration: BoxDecoration( + color: selected + ? colorScheme.secondaryContainer + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: compact ? 6 : 8, + vertical: compact ? 8 : 10, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 24, + height: 24, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [Icon(icon, size: 18, color: fg)], ), - const SizedBox(height: 8), - SizedBox( - width: _chatSidebarIndicatorExtent - 16, - child: Text( - label, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: labelStyle, - ), + ), + const SizedBox(height: 8), + Text( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: theme.textTheme.labelSmall?.copyWith( + color: fg, + height: 1.1, + fontWeight: selected ? FontWeight.w700 : FontWeight.w500, ), - ], - ), + ), + ], ), ), ), @@ -926,6 +959,7 @@ class _ClientPickerDialogState extends State<_ClientPickerDialog> { @override Widget build(BuildContext context) { + final l10n = AppL10n.of(context); final groups = filterChatClientPickerGroups( channels: widget.channels, clientsByChannel: widget.byChannel, @@ -943,10 +977,10 @@ class _ClientPickerDialogState extends State<_ClientPickerDialog> { padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), child: TextField( controller: _searchCtl, - decoration: const InputDecoration( - hintText: 'Search clients...', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), + decoration: InputDecoration( + hintText: l10n.chatSearchClientsHint, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), isDense: true, ), onChanged: (v) => setState(() => _query = v), @@ -1044,7 +1078,6 @@ class _ChatDetailViewState extends State<_ChatDetailView> { final _scrollCtl = ScrollController(); int _lastRenderedMessageCount = -1; rust.BridgeMessageTarget? _lastRenderedTarget; - bool _sending = false; Iterable get _filtered { if (widget.target is rust.BridgeMessageTarget_Channel) { @@ -1074,51 +1107,30 @@ class _ChatDetailViewState extends State<_ChatDetailView> { super.dispose(); } - Future _send() async { + void _send() { final text = _textCtl.text.trim(); - if (text.isEmpty || !_canSend || _sending) return; + if (text.isEmpty || !_canSend) return; _textCtl.clear(); - setState(() => _sending = true); - try { - await rust.sendChatMessage(message: text, target: widget.target); - if (!mounted) return; - final ownId = widget.snapshot.ownClientId; - setState(() { - widget.messages.add( - ChatEntry( - senderId: ownId, - senderName: widget.target is rust.BridgeMessageTarget_Poke - ? widget.clientName - : 'You', - message: text, - target: widget.target, - isSelf: true, - timestamp: DateTime.now(), - ), - ); - if (widget.messages.length > 200) { - widget.messages.removeRange(0, widget.messages.length - 200); - } - }); - _scrollToBottom(); - } catch (error) { - if (!mounted) return; - _textCtl.text = text; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - content: Text( - 'Could not send message: $error', - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), + unawaited(rust.sendChatMessage(message: text, target: widget.target)); + final ownId = widget.snapshot.ownClientId; + setState(() { + widget.messages.add( + ChatEntry( + senderId: ownId, + senderName: widget.target is rust.BridgeMessageTarget_Poke + ? widget.clientName + : 'You', + message: text, + target: widget.target, + isSelf: true, + timestamp: DateTime.now(), ), ); - } finally { - if (mounted) { - setState(() => _sending = false); + if (widget.messages.length > 200) { + widget.messages.removeRange(0, widget.messages.length - 200); } - } + }); + _scrollToBottom(); } void _scrollToBottom() { diff --git a/apps/chanora_flutter/lib/widgets/client_info_sheet.dart b/apps/chanora_flutter/lib/widgets/client_info_sheet.dart new file mode 100644 index 0000000..061edba --- /dev/null +++ b/apps/chanora_flutter/lib/widgets/client_info_sheet.dart @@ -0,0 +1,491 @@ +import 'package:flutter/material.dart'; + +import '../l10n/generated/app_localizations.dart'; +import '../src/rust/api.dart' as rust; + +/// Bottom sheet that presents richer TeamSpeak client profile data. +class ClientInfoSheet extends StatefulWidget { + /// Construct a client info sheet. + const ClientInfoSheet({ + super.key, + required this.clientName, + required this.loadProfile, + }); + + /// Display name used while the profile request is loading. + final String clientName; + + /// Fetch richer profile data from the active protocol connection. + final Future Function() loadProfile; + + @override + State createState() => _ClientInfoSheetState(); +} + +class _ClientInfoSheetState extends State { + late Future _profileFuture; + + @override + void initState() { + super.initState(); + _profileFuture = widget.loadProfile(); + } + + void _retry() { + setState(() { + _profileFuture = widget.loadProfile(); + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: theme.colorScheme.onSurfaceVariant.withValues( + alpha: 0.34, + ), + borderRadius: BorderRadius.circular(999), + ), + ), + const SizedBox(height: 16), + Expanded( + child: FutureBuilder( + future: _profileFuture, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return _ClientInfoLoading(name: widget.clientName); + } + if (snapshot.hasError || !snapshot.hasData) { + return _ClientInfoError( + name: widget.clientName, + onRetry: _retry, + ); + } + return _ClientInfoContent(profile: snapshot.data!); + }, + ), + ), + ], + ), + ), + ); + } +} + +class _ClientInfoLoading extends StatelessWidget { + const _ClientInfoLoading({required this.name}); + + final String name; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppL10n.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ClientInfoHeader(name: name, subtitle: l10n.clientInfoFetchingProfile), + const SizedBox(height: 24), + LinearProgressIndicator( + minHeight: 3, + borderRadius: BorderRadius.circular(999), + ), + const SizedBox(height: 16), + Text(l10n.clientInfoLoadingProfile, style: theme.textTheme.bodyMedium), + ], + ); + } +} + +class _ClientInfoError extends StatelessWidget { + const _ClientInfoError({required this.name, required this.onRetry}); + + final String name; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppL10n.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ClientInfoHeader( + name: name, + subtitle: l10n.clientInfoProfileUnavailable, + ), + const SizedBox(height: 24), + Icon(Icons.info_outline, color: theme.colorScheme.error, size: 32), + const SizedBox(height: 12), + Text( + l10n.clientInfoProfileUnavailableBody, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh), + label: Text(l10n.retryAction), + ), + ], + ); + } +} + +class _ClientInfoContent extends StatelessWidget { + const _ClientInfoContent({required this.profile}); + + final rust.BridgeClientProfile profile; + + @override + Widget build(BuildContext context) { + final l10n = AppL10n.of(context); + return ListView( + children: [ + _ClientInfoHeader( + name: profile.name, + subtitle: _joinNonEmpty([ + profile.platform, + profile.version, + profile.countryCode, + ]), + ), + const SizedBox(height: 20), + _InfoSection( + title: l10n.clientInfoIdentitySection, + rows: [ + _InfoRowData(l10n.clientInfoClientId, profile.id.toString()), + _InfoRowData( + l10n.clientInfoDatabaseId, + _formatBigInt(profile.databaseId, l10n), + ), + _InfoRowData( + l10n.clientInfoUniqueId, + _emptyAsHidden(profile.uniqueId, l10n), + ), + _InfoRowData( + l10n.clientInfoDescription, + _emptyAsHidden(profile.description, l10n), + ), + _InfoRowData( + l10n.clientInfoAvatar, + _emptyAsNone(profile.avatarPath, l10n), + ), + ], + ), + _InfoSection( + title: l10n.clientInfoMembershipSection, + rows: [ + _InfoRowData( + l10n.clientInfoServerGroups, + profile.serverGroups.isEmpty + ? l10n.clientInfoUnknown + : profile.serverGroups.join(', '), + ), + _InfoRowData( + l10n.clientInfoChannelGroup, + _emptyAsHidden(profile.channelGroup, l10n), + ), + _InfoRowData(l10n.clientInfoChannelId, profile.channel.toString()), + ], + ), + _InfoSection( + title: l10n.clientInfoConnectionSection, + rows: [ + _InfoRowData( + l10n.clientInfoOnline, + _formatSeconds(profile.onlineSeconds, l10n), + ), + _InfoRowData( + l10n.clientInfoIdle, + _formatMilliseconds(profile.idleMilliseconds, l10n), + ), + _InfoRowData( + l10n.clientInfoPing, + _formatMilliseconds(profile.pingMilliseconds, l10n), + ), + _InfoRowData( + l10n.clientInfoAddress, + _emptyAsHidden(profile.clientAddress, l10n), + ), + _InfoRowData( + l10n.clientInfoPacketLossClientToServer, + _formatLoss(profile.packetLossClientToServerTotal), + ), + _InfoRowData( + l10n.clientInfoPacketLossServerToClient, + _formatLoss(profile.packetLossServerToClientTotal), + ), + ], + ), + _InfoSection( + title: l10n.clientInfoHistorySection, + rows: [ + _InfoRowData( + l10n.clientInfoFirstConnected, + _formatUnix(profile.createdUnixSeconds, l10n), + ), + _InfoRowData( + l10n.clientInfoLastConnected, + _formatUnix(profile.lastConnectedUnixSeconds, l10n), + ), + _InfoRowData( + l10n.clientInfoConnections, + _formatBigInt(profile.connectionsTotal, l10n), + ), + ], + ), + _InfoSection( + title: l10n.clientInfoTransferSection, + rows: [ + _InfoRowData( + l10n.clientInfoDownloadedMonth, + _formatBytes(profile.bytesDownloadedMonth), + ), + _InfoRowData( + l10n.clientInfoUploadedMonth, + _formatBytes(profile.bytesUploadedMonth), + ), + _InfoRowData( + l10n.clientInfoDownloadedTotal, + _formatBytes(profile.bytesDownloadedTotal), + ), + _InfoRowData( + l10n.clientInfoUploadedTotal, + _formatBytes(profile.bytesUploadedTotal), + ), + ], + ), + ], + ); + } +} + +class _ClientInfoHeader extends StatelessWidget { + const _ClientInfoHeader({required this.name, required this.subtitle}); + + final String name; + final String subtitle; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 28, + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.onPrimaryContainer, + child: Text( + _initials(name), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + if (subtitle.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + ], + ); + } +} + +class _InfoSection extends StatelessWidget { + const _InfoSection({required this.title, required this.rows}); + + final String title; + final List<_InfoRowData> rows; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: theme.dividerColor), + bottom: BorderSide(color: theme.dividerColor), + ), + ), + child: Column( + children: [ + for (var i = 0; i < rows.length; i++) ...[ + _InfoRow(row: rows[i]), + if (i != rows.length - 1) const Divider(height: 1), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _InfoRow extends StatelessWidget { + const _InfoRow({required this.row}); + + final _InfoRowData row; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 11), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 132, + child: Text( + row.label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: SelectableText( + row.value, + style: theme.textTheme.bodyMedium?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + ], + ), + ); + } +} + +class _InfoRowData { + const _InfoRowData(this.label, this.value); + + final String label; + final String value; +} + +String _initials(String value) { + final trimmed = value.trim(); + if (trimmed.isEmpty) return '?'; + final parts = trimmed.split(RegExp(r'\s+')).where((p) => p.isNotEmpty); + final chars = parts.take(2).map((p) => p.characters.first.toUpperCase()); + return chars.join(); +} + +String _joinNonEmpty(Iterable values) { + return values.where((value) => value.trim().isNotEmpty).join(' / '); +} + +String _emptyAsHidden(String value, AppL10n l10n) => + value.trim().isEmpty ? l10n.clientInfoHidden : value; + +String _emptyAsNone(String value, AppL10n l10n) => + value.trim().isEmpty ? l10n.clientInfoNone : value; + +String _formatBigInt(BigInt? value, AppL10n l10n) => + value?.toString() ?? l10n.clientInfoUnknown; + +String _formatUnix(Object? seconds, AppL10n l10n) { + final raw = _intFromPlatform(seconds); + if (raw == null || raw <= 0) return l10n.clientInfoUnknown; + final date = DateTime.fromMillisecondsSinceEpoch( + raw * 1000, + isUtc: true, + ).toLocal(); + return date.toString().split('.').first; +} + +String _formatSeconds(Object? seconds, AppL10n l10n) { + final raw = _intFromPlatform(seconds); + if (raw == null) return l10n.clientInfoUnknown; + if (raw < 60) return '$raw s'; + final minutes = raw ~/ 60; + final hours = minutes ~/ 60; + if (hours > 0) { + return '${hours}h ${minutes % 60}m ${raw % 60}s'; + } + return '${minutes}m ${raw % 60}s'; +} + +String _formatMilliseconds(Object? milliseconds, AppL10n l10n) { + final raw = _intFromPlatform(milliseconds); + if (raw == null) return l10n.clientInfoUnknown; + if (raw < 1000) return '$raw ms'; + return '${(raw / 1000).toStringAsFixed(2)} s'; +} + +String _formatBytes(BigInt? value) { + if (value == null) return 'Unknown'; + final bytes = value.toDouble(); + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + var amount = bytes; + var unitIndex = 0; + while (amount >= 1024 && unitIndex < units.length - 1) { + amount /= 1024; + unitIndex++; + } + final digits = unitIndex == 0 ? 0 : 1; + return '${amount.toStringAsFixed(digits)} ${units[unitIndex]}'; +} + +String _formatLoss(double? value) { + if (value == null) return 'Unknown'; + return '${(value * 100).toStringAsFixed(2)}%'; +} + +int? _intFromPlatform(Object? value) { + if (value == null) return null; + if (value is int) return value; + if (value is BigInt) return value.toInt(); + return int.tryParse(value.toString()); +} diff --git a/apps/chanora_flutter/lib/widgets/connect_widgets.dart b/apps/chanora_flutter/lib/widgets/connect_widgets.dart index ddc4e5a..30e289f 100644 --- a/apps/chanora_flutter/lib/widgets/connect_widgets.dart +++ b/apps/chanora_flutter/lib/widgets/connect_widgets.dart @@ -65,7 +65,6 @@ class _ConnectFormState extends State { keyboardType: TextInputType.url, textCapitalization: TextCapitalization.none, textInputAction: TextInputAction.next, - onSubmitted: (_) => _nickFocus.requestFocus(), autocorrect: false, enableSuggestions: false, inputFormatters: [ @@ -90,7 +89,6 @@ class _ConnectFormState extends State { focusNode: _nickFocus, onTapOutside: _onTapOutside, textInputAction: TextInputAction.next, - onSubmitted: (_) => _passwordFocus.requestFocus(), autocorrect: false, enableSuggestions: false, decoration: InputDecoration( @@ -105,7 +103,6 @@ class _ConnectFormState extends State { onTapOutside: _onTapOutside, obscureText: true, textInputAction: TextInputAction.done, - onSubmitted: (_) => widget.onConnect(), autocorrect: false, enableSuggestions: false, decoration: InputDecoration( @@ -115,22 +112,37 @@ class _ConnectFormState extends State { ), ), const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: FilledButton.icon( - icon: const Icon(Icons.login), - label: Text(l10n.connectAction), - onPressed: widget.onConnect, - ), - ), - const SizedBox(width: 8), - OutlinedButton.icon( + LayoutBuilder( + builder: (context, constraints) { + const stackedActionsMaxWidth = 400.0; + final connectButton = FilledButton.icon( + icon: const Icon(Icons.login), + label: Text(l10n.connectAction), + onPressed: widget.onConnect, + ); + final bookmarkButton = OutlinedButton.icon( icon: const Icon(Icons.bookmark_add_outlined), label: Text(l10n.bookmarkAddAction), onPressed: widget.onAddBookmark, - ), - ], + ); + if (constraints.maxWidth <= stackedActionsMaxWidth) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + connectButton, + const SizedBox(height: 8), + bookmarkButton, + ], + ); + } + return Row( + children: [ + Expanded(child: connectButton), + const SizedBox(width: 8), + Flexible(child: bookmarkButton), + ], + ); + }, ), ], ); diff --git a/apps/chanora_flutter/lib/widgets/input_dialogs.dart b/apps/chanora_flutter/lib/widgets/input_dialogs.dart index e37a6c9..0c6be71 100644 --- a/apps/chanora_flutter/lib/widgets/input_dialogs.dart +++ b/apps/chanora_flutter/lib/widgets/input_dialogs.dart @@ -4,9 +4,6 @@ import 'package:flutter/services.dart'; import '../l10n/generated/app_localizations.dart'; import '../src/rust/api.dart' as rust; -const int pttMouseBackButtonBitmask = 0x08; -const int pttMouseForwardButtonBitmask = 0x10; - /// Translate a [LogicalKeyboardKey] into the platform-neutral label /// stored by the PTT binding flow. String? pttDisplayLabelForKey(LogicalKeyboardKey k) { @@ -49,27 +46,6 @@ String? pttDisplayLabelForKey(LogicalKeyboardKey k) { return fallback; } -String? pttMouseSideButtonPlatformKeyForLogicalKey(LogicalKeyboardKey key) { - return switch (key) { - LogicalKeyboardKey.browserBack || - LogicalKeyboardKey.goBack => 'mouse-side-button:$pttMouseBackButtonBitmask', - LogicalKeyboardKey.browserForward => - 'mouse-side-button:$pttMouseForwardButtonBitmask', - _ => null, - }; -} - -String? pttMouseSideButtonPlatformKeyForButtons(int buttons) { - if ((buttons & pttMouseBackButtonBitmask) == pttMouseBackButtonBitmask) { - return 'mouse-side-button:$pttMouseBackButtonBitmask'; - } - if ((buttons & pttMouseForwardButtonBitmask) == - pttMouseForwardButtonBitmask) { - return 'mouse-side-button:$pttMouseForwardButtonBitmask'; - } - return null; -} - /// Result of a successful PTT binding capture. class CapturedBinding { const CapturedBinding({required this.inputClass, required this.platformKey}); @@ -205,16 +181,6 @@ class _PttBindingCaptureDialogState extends State { KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; - final mouseSideButton = pttMouseSideButtonPlatformKeyForLogicalKey( - event.logicalKey, - ); - if (mouseSideButton != null) { - setState(() { - _captured = mouseSideButton; - _capturedClass = rust.BridgePttInputClass.mouseSideButton; - }); - return KeyEventResult.handled; - } final label = pttDisplayLabelForKey(event.logicalKey); if (label == null) return KeyEventResult.ignored; setState(() { @@ -224,9 +190,9 @@ class _PttBindingCaptureDialogState extends State { return KeyEventResult.handled; } - void _captureMouseSideButton(String platformKey) { + void _captureMouseSideButton(int button) { setState(() { - _captured = platformKey; + _captured = 'mouse-side-button:$button'; _capturedClass = rust.BridgePttInputClass.mouseSideButton; }); } @@ -237,57 +203,59 @@ class _PttBindingCaptureDialogState extends State { final theme = Theme.of(context); return AlertDialog( title: Text(l10n.pttConfigureTitle), - content: SizedBox( - width: 360, - child: Focus( - focusNode: _focusNode, - onKeyEvent: _onKeyEvent, - autofocus: true, - child: Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (e) { - final platformKey = pttMouseSideButtonPlatformKeyForButtons( - e.buttons, - ); - if (platformKey != null) { - _captureMouseSideButton(platformKey); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.pttConfigurePrompt, - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: SingleChildScrollView( + child: Focus( + focusNode: _focusNode, + onKeyEvent: _onKeyEvent, + autofocus: true, + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (e) { + const int back = 0x08; + const int forward = 0x10; + if (e.buttons == back || e.buttons == forward) { + _captureMouseSideButton(e.buttons); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.pttConfigurePrompt, + style: theme.textTheme.bodyMedium, ), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(6), - ), - child: Text( - _captured == null - ? l10n.pttConfigureWaiting - : '${l10n.pttConfigureCaptured}: $_captured', - style: theme.textTheme.bodyMedium?.copyWith( - fontFamily: 'monospace', + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + _captured == null + ? l10n.pttConfigureWaiting + : '${l10n.pttConfigureCaptured}: $_captured', + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + ), ), ), - ), - const SizedBox(height: 12), - Text( - l10n.pttConfigurePrivacyNote, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + const SizedBox(height: 12), + Text( + l10n.pttConfigurePrivacyNote, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/apps/chanora_flutter/lib/widgets/ptt_capability_badge.dart b/apps/chanora_flutter/lib/widgets/ptt_capability_badge.dart index a6a801b..ac81730 100644 --- a/apps/chanora_flutter/lib/widgets/ptt_capability_badge.dart +++ b/apps/chanora_flutter/lib/widgets/ptt_capability_badge.dart @@ -54,43 +54,48 @@ class PttCapabilityBadge extends StatelessWidget { showModalBottomSheet( context: context, showDragHandle: true, + isScrollControlled: true, builder: (sheetContext) { final theme = Theme.of(sheetContext); + final maxHeight = MediaQuery.sizeOf(sheetContext).height * 0.72; return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.pttCapabilityExplainTitle, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: 12), - Text( - l10n.pttCapabilityExplainFocusedHeading, - style: theme.textTheme.titleSmall, - ), - const SizedBox(height: 4), - Text( - l10n.pttCapabilityExplainFocusedBody, - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Text( - _explainBodyForPlatform(l10n), - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Align( - alignment: AlignmentDirectional.centerEnd, - child: TextButton( - onPressed: () => Navigator.of(sheetContext).pop(), - child: Text(l10n.closeAction), + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.pttCapabilityExplainTitle, + style: theme.textTheme.titleMedium, ), - ), - ], + const SizedBox(height: 12), + Text( + l10n.pttCapabilityExplainFocusedHeading, + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: 4), + Text( + l10n.pttCapabilityExplainFocusedBody, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 16), + Text( + _explainBodyForPlatform(l10n), + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 16), + Align( + alignment: AlignmentDirectional.centerEnd, + child: TextButton( + onPressed: () => Navigator.of(sheetContext).pop(), + child: Text(l10n.closeAction), + ), + ), + ], + ), ), ), ); diff --git a/apps/chanora_flutter/lib/widgets/snapshot_view.dart b/apps/chanora_flutter/lib/widgets/snapshot_view.dart index c2ffcb8..d0836cf 100644 --- a/apps/chanora_flutter/lib/widgets/snapshot_view.dart +++ b/apps/chanora_flutter/lib/widgets/snapshot_view.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,17 +6,10 @@ import '../services/channel_spacer.dart'; import '../services/link_trust_service.dart'; import '../services/snapshot_state_mapper.dart'; import '../services/ts3_server_link.dart'; -import '../services/ui_preferences_service.dart'; import '../src/rust/api.dart' as rust; import 'bbcode_text.dart'; import 'talk_power_warning.dart'; -typedef ClientPlaybackPreferenceChanged = - Future Function( - rust.BridgeClient client, - ClientPlaybackPreference preference, - ); - /// Connected-server snapshot with welcome text, channels, and clients. class SnapshotView extends StatefulWidget { /// Construct a snapshot view. @@ -34,8 +25,10 @@ class SnapshotView extends StatefulWidget { required this.canJoinVoiceChannel, required this.onJoinChannel, required this.onJoinChannelWithPassword, - this.clientPlaybackPreferences = const {}, - this.onClientPlaybackPreferenceChanged, + this.enableClientLongPressMenu = false, + this.onOpenClientInfo, + this.onOpenClientChat, + this.onOpenClientPoke, this.onTs3ServerLink, }); @@ -69,11 +62,17 @@ class SnapshotView extends StatefulWidget { /// Join a password-protected channel. final ValueChanged onJoinChannelWithPassword; - /// Persisted per-client playback preferences keyed by TeamSpeak UID. - final Map clientPlaybackPreferences; + /// True on touch-only mobile hosts where long-press opens client actions. + final bool enableClientLongPressMenu; - /// Apply an updated per-client playback preference. - final ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged; + /// Open richer profile details for a non-self client. + final ValueChanged? onOpenClientInfo; + + /// Open a direct chat with a non-self client. + final ValueChanged? onOpenClientChat; + + /// Open a poke composer for a non-self client. + final ValueChanged? onOpenClientPoke; /// Handle TeamSpeak server links embedded in server-provided text. final Ts3ServerLinkHandler? onTs3ServerLink; @@ -88,11 +87,10 @@ class _SnapshotViewState extends State { static const _channelIconColumnWidth = 24.0; static const _channelTextGap = 8.0; static const _userRowStartIndent = 32.0; - static const _clientPlaybackVolumePresets = [0.25, 0.5, 1.0, 2.0, 4.0]; final _scrollController = ScrollController(); final Map _channelExpandedById = {}; - bool _welcomeExpanded = true; + bool _welcomeExpanded = false; double _welcomeHeight = 0; final _welcomeKey = GlobalKey(); @@ -293,6 +291,12 @@ class _SnapshotViewState extends State { fontWeight: FontWeight.w600, ) : null; + final isSelf = client.id == widget.snapshot.ownClientId; + final canOpenPeerActions = + !isSelf && + (widget.onOpenClientChat != null || widget.onOpenClientPoke != null); + final canOpenClientMenu = + widget.onOpenClientInfo != null || canOpenPeerActions; final decoration = status.isSpeaking ? BoxDecoration( @@ -311,40 +315,90 @@ class _SnapshotViewState extends State { ], ) : null; - final canAdjustPlayback = _canAdjustClientPlayback(client); - Offset? secondaryTapPosition; - return Padding( + final tile = Padding( padding: EdgeInsets.only( left: channelIndent + _userRowStartIndent, right: 8, ), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onLongPress: canAdjustPlayback - ? () => unawaited(_showClientPlaybackMenu(client, withHaptic: true)) - : null, - onSecondaryTapDown: canAdjustPlayback - ? (details) => secondaryTapPosition = details.globalPosition - : null, - onSecondaryTap: canAdjustPlayback - ? () => unawaited( - _showClientPlaybackMenu(client, anchor: secondaryTapPosition), - ) - : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 120), - curve: Curves.easeOut, - decoration: decoration, - child: ListTile( - dense: true, - visualDensity: VisualDensity.compact, - leading: status.icon, - title: Text(client.name, style: nameStyle), - ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 120), + curve: Curves.easeOut, + decoration: decoration, + child: ListTile( + dense: true, + visualDensity: VisualDensity.compact, + leading: status.icon, + title: Text(client.name, style: nameStyle), ), ), ); + if (!canOpenClientMenu) return tile; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onSecondaryTapDown: (details) => + _showClientMenu(client, details.globalPosition), + onLongPressStart: widget.enableClientLongPressMenu + ? (details) => _showClientMenu(client, details.globalPosition) + : null, + child: tile, + ); + } + + Future _showClientMenu( + rust.BridgeClient client, + Offset globalPosition, + ) async { + final overlay = Overlay.of(context).context.findRenderObject(); + if (overlay is! RenderBox) return; + final isSelf = client.id == widget.snapshot.ownClientId; + + final selected = await showMenu<_ClientMenuAction>( + context: context, + position: RelativeRect.fromRect( + Rect.fromLTWH(globalPosition.dx, globalPosition.dy, 0, 0), + Offset.zero & overlay.size, + ), + items: [ + if (widget.onOpenClientInfo != null) + PopupMenuItem( + value: _ClientMenuAction.info, + child: ListTile( + dense: true, + leading: const Icon(Icons.info_outline), + title: Text(AppL10n.of(context).clientInfoAction), + ), + ), + if (!isSelf && widget.onOpenClientChat != null) + PopupMenuItem( + value: _ClientMenuAction.directMessage, + child: ListTile( + dense: true, + leading: const Icon(Icons.chat_bubble_outline), + title: Text(AppL10n.of(context).chatDirectMessageAction), + ), + ), + if (!isSelf && widget.onOpenClientPoke != null) + PopupMenuItem( + value: _ClientMenuAction.poke, + child: ListTile( + dense: true, + leading: const Icon(Icons.notifications_active_outlined), + title: Text(AppL10n.of(context).chatPokeAction), + ), + ), + ], + ); + if (!mounted || selected == null) return; + switch (selected) { + case _ClientMenuAction.info: + widget.onOpenClientInfo?.call(client); + case _ClientMenuAction.directMessage: + widget.onOpenClientChat?.call(client); + case _ClientMenuAction.poke: + widget.onOpenClientPoke?.call(client); + } } Widget _expandButton( @@ -386,81 +440,6 @@ class _SnapshotViewState extends State { }); } - bool _canAdjustClientPlayback(rust.BridgeClient client) { - return widget.onClientPlaybackPreferenceChanged != null && - client.id != widget.snapshot.ownClientId && - !client.isServerQuery && - client.uid.isNotEmpty; - } - - ClientPlaybackPreference _clientPlaybackPreference(rust.BridgeClient client) { - return widget.clientPlaybackPreferences[client.uid] ?? - const ClientPlaybackPreference(); - } - - Future _showClientPlaybackMenu( - rust.BridgeClient client, { - Offset? anchor, - bool withHaptic = false, - }) async { - if (!_canAdjustClientPlayback(client)) return; - - final onChanged = widget.onClientPlaybackPreferenceChanged; - if (onChanged == null) return; - - if (withHaptic) { - unawaited(HapticFeedback.selectionClick().catchError((_) {})); - } - if (!mounted) return; - - final preference = _clientPlaybackPreference(client); - final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; - final position = anchor ?? overlay.size.center(Offset.zero); - final selected = await showMenu<_ClientPlaybackMenuAction>( - context: context, - position: RelativeRect.fromLTRB( - position.dx, - position.dy, - overlay.size.width - position.dx, - overlay.size.height - position.dy, - ), - items: [ - PopupMenuItem<_ClientPlaybackMenuAction>( - value: const _ToggleMuteClientPlaybackMenuAction(), - child: ListTile( - dense: true, - contentPadding: EdgeInsets.zero, - leading: Icon( - preference.muted ? Icons.volume_off : Icons.volume_up, - ), - title: Text(preference.muted ? 'Unmute playback' : 'Mute playback'), - subtitle: Text(client.name), - ), - ), - const PopupMenuDivider(), - ..._clientPlaybackVolumePresets.map( - (preset) => CheckedPopupMenuItem<_ClientPlaybackMenuAction>( - value: _VolumeClientPlaybackMenuAction(preset), - checked: - !preference.muted && (preference.volume - preset).abs() < 0.001, - child: Text('Volume ${(preset * 100).round()}%'), - ), - ), - ], - ); - if (selected == null || !mounted) return; - final next = switch (selected) { - _ToggleMuteClientPlaybackMenuAction() => preference.copyWith( - muted: !preference.muted, - ), - _VolumeClientPlaybackMenuAction(:final volume) => preference.copyWith( - volume: volume, - muted: false, - ), - }; - await onChanged(client, next); - } - ({Widget icon, bool isSpeaking}) _clientVoiceStatusIcon( ThemeData theme, rust.BridgeClient client, @@ -530,6 +509,8 @@ class _SnapshotViewState extends State { } } +enum _ClientMenuAction { info, directMessage, poke } + class _SpacerChannelContent extends StatelessWidget { const _SpacerChannelContent({required this.spacer}); @@ -673,20 +654,6 @@ class _ChannelTreeNode { final List<_ChannelTreeNode> children = []; } -sealed class _ClientPlaybackMenuAction { - const _ClientPlaybackMenuAction(); -} - -class _ToggleMuteClientPlaybackMenuAction extends _ClientPlaybackMenuAction { - const _ToggleMuteClientPlaybackMenuAction(); -} - -class _VolumeClientPlaybackMenuAction extends _ClientPlaybackMenuAction { - const _VolumeClientPlaybackMenuAction(this.volume); - - final double volume; -} - _ChannelTree _buildChannelTree(List channels) { final byParent = >{}; final knownIds = {for (final channel in channels) channel.id}; @@ -757,7 +724,7 @@ class _WelcomeMessageTile extends StatelessWidget { ), const SizedBox(width: 4), Text( - 'Server welcome message', + AppL10n.of(context).serverWelcomeHeading, style: theme.textTheme.labelMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -766,20 +733,22 @@ class _WelcomeMessageTile extends StatelessWidget { ), ), ), - AnimatedCrossFade( - firstChild: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), - child: BbCodeText( - welcomeMessage, - linkTrust: LinkTrustService.instance, - onTs3ServerLink: onTs3ServerLink, - ), - ), - secondChild: const SizedBox(width: double.infinity), - crossFadeState: expanded - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, + AnimatedSwitcher( duration: const Duration(milliseconds: 200), + child: expanded + ? Padding( + key: const ValueKey('welcome-expanded'), + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: BbCodeText( + welcomeMessage, + linkTrust: LinkTrustService.instance, + onTs3ServerLink: onTs3ServerLink, + ), + ) + : const SizedBox( + key: ValueKey('welcome-collapsed'), + width: double.infinity, + ), ), ], ), diff --git a/apps/chanora_flutter/lib/widgets/startup_dependency_screen.dart b/apps/chanora_flutter/lib/widgets/startup_dependency_screen.dart deleted file mode 100644 index 8d64d69..0000000 --- a/apps/chanora_flutter/lib/widgets/startup_dependency_screen.dart +++ /dev/null @@ -1,468 +0,0 @@ -import 'dart:async' show unawaited; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../services/startup_dependency_check.dart'; - -class StartupDependencyGate extends StatefulWidget { - const StartupDependencyGate({ - required this.child, - this.checker = checkStartupDependencies, - this.logger = logStartupDependencyIssues, - super.key, - }); - - final Widget child; - final Future Function() checker; - final Future Function(StartupDependencyCheckResult result) logger; - - @override - State createState() => _StartupDependencyGateState(); -} - -class StartupDependencyScope extends InheritedWidget { - const StartupDependencyScope({ - required this.result, - required super.child, - super.key, - }); - - final StartupDependencyCheckResult result; - - static StartupDependencyCheckResult? maybeOf(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType() - ?.result; - } - - @override - bool updateShouldNotify(StartupDependencyScope oldWidget) { - return result != oldWidget.result; - } -} - -class _StartupDependencyGateState extends State { - late Future _future; - bool _dismissedForSession = false; - String? _lastLoggedIssueSignature; - - @override - void initState() { - super.initState(); - _future = widget.checker(); - } - - void _recheck() { - setState(() { - _future = widget.checker(); - _dismissedForSession = false; - }); - } - - @override - Widget build(BuildContext context) { - if (_dismissedForSession) { - return FutureBuilder( - future: _future, - builder: (context, snapshot) { - return StartupDependencyScope( - result: - snapshot.data ?? - const StartupDependencyCheckResult( - issues: [], - platformLabel: 'default', - ), - child: widget.child, - ); - }, - ); - } - - return FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const _StartupCheckLoadingView(); - } - - final result = snapshot.data; - if (result == null || !result.hasIssues) { - _lastLoggedIssueSignature = null; - return StartupDependencyScope( - result: - result ?? - const StartupDependencyCheckResult( - issues: [], - platformLabel: 'default', - ), - child: widget.child, - ); - } - - _logIssueScreenShown(result); - return StartupDependencyScope( - result: result, - child: StartupDependencyScreen( - result: result, - onContinue: () { - setState(() { - _dismissedForSession = true; - }); - }, - onRecheck: _recheck, - ), - ); - }, - ); - } - - void _logIssueScreenShown(StartupDependencyCheckResult result) { - final signature = [ - result.platformLabel, - for (final issue in result.issues) - '${issue.id}:${issue.isRequired ? 'required' : 'recommended'}', - ].join('|'); - if (_lastLoggedIssueSignature == signature) { - return; - } - _lastLoggedIssueSignature = signature; - unawaited(widget.logger(result)); - } -} - -class StartupDependencyScreen extends StatelessWidget { - const StartupDependencyScreen({ - required this.result, - required this.onContinue, - required this.onRecheck, - super.key, - }); - - final StartupDependencyCheckResult result; - final VoidCallback onContinue; - final VoidCallback onRecheck; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final hasBlockingIssues = result.hasBlockingIssues; - final content = Column( - children: [ - Icon( - hasBlockingIssues - ? Icons.warning_amber_rounded - : Icons.info_outline_rounded, - size: 52, - color: hasBlockingIssues - ? theme.colorScheme.error - : theme.colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'Finish Linux setup', - style: theme.textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Text( - hasBlockingIssues - ? 'Chanora started, but some Linux runtime packages are still missing. Install them, then recheck.' - : 'Chanora started, but a few optional Linux runtime components are still missing. You can install them now or continue with limited functionality.', - style: theme.textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - 'Detected platform: ${result.platformLabel}', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - ...result.issues.map((issue) => _DependencyIssueCard(issue: issue)), - const SizedBox(height: 16), - Wrap( - alignment: WrapAlignment.center, - spacing: 12, - runSpacing: 12, - children: [ - FilledButton.icon( - onPressed: onRecheck, - icon: const Icon(Icons.refresh_rounded), - label: const Text('Recheck'), - ), - OutlinedButton.icon( - onPressed: onContinue, - icon: const Icon(Icons.arrow_forward_rounded), - label: Text( - hasBlockingIssues - ? 'Continue with limited mode' - : 'Continue anyway', - ), - ), - ], - ), - ], - ); - - return Scaffold( - body: SafeArea( - child: Scrollbar( - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), - children: [ - Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 880), - child: content, - ), - ), - ], - ), - ), - ), - ); - } -} - -class _DependencyIssueCard extends StatelessWidget { - const _DependencyIssueCard({required this.issue}); - - final StartupDependencyIssue issue; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scheme = theme.colorScheme; - final containerColor = issue.isRequired - ? scheme.errorContainer - : scheme.secondaryContainer; - final onContainerColor = issue.isRequired - ? scheme.onErrorContainer - : scheme.onSecondaryContainer; - - return Card( - margin: const EdgeInsets.only(bottom: 16), - color: containerColor, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - issue.isRequired - ? Icons.error_outline_rounded - : Icons.settings_suggest_rounded, - color: onContainerColor, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - issue.title, - style: theme.textTheme.titleMedium?.copyWith( - color: onContainerColor, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - Text( - issue.summary, - style: theme.textTheme.bodyMedium?.copyWith( - color: onContainerColor, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - _SeverityBadge(issue: issue), - ], - ), - if (issue.details.isNotEmpty) ...[ - const SizedBox(height: 16), - ...issue.details.map( - (detail) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - '• $detail', - style: theme.textTheme.bodyMedium?.copyWith( - color: onContainerColor, - ), - ), - ), - ), - ], - if (issue.installHints.isNotEmpty) ...[ - const SizedBox(height: 8), - Text( - 'Install help', - style: theme.textTheme.titleSmall?.copyWith( - color: onContainerColor, - ), - ), - const SizedBox(height: 12), - ...issue.installHints.map( - (hint) => _InstallHintTile( - hint: hint, - foregroundColor: onContainerColor, - ), - ), - ], - ], - ), - ), - ); - } -} - -class _InstallHintTile extends StatelessWidget { - const _InstallHintTile({required this.hint, required this.foregroundColor}); - - final StartupInstallHint hint; - final Color foregroundColor; - - @override - Widget build(BuildContext context) { - final messenger = ScaffoldMessenger.of(context); - final isUrl = _isWebUrl(hint.command); - final VoidCallback? onTap = isUrl - ? () => unawaited(_openUrl(context, Uri.parse(hint.command))) - : null; - return InkWell( - borderRadius: BorderRadius.circular(16), - onTap: onTap, - child: Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: foregroundColor.withValues(alpha: 0.08), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - hint.label, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: foregroundColor, - fontWeight: FontWeight.w700, - ), - ), - ), - TextButton.icon( - onPressed: () async { - if (isUrl) { - await _openUrl(context, Uri.parse(hint.command)); - return; - } - await Clipboard.setData(ClipboardData(text: hint.command)); - messenger.showSnackBar( - const SnackBar(content: Text('Install command copied')), - ); - }, - icon: Icon( - isUrl - ? Icons.open_in_new_rounded - : Icons.content_copy_rounded, - ), - label: Text(isUrl ? 'Open' : 'Copy'), - ), - ], - ), - const SizedBox(height: 8), - SelectableText( - hint.command, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontFamily: 'monospace', - color: foregroundColor, - ), - ), - ], - ), - ), - ); - } - - bool _isWebUrl(String value) { - final uri = Uri.tryParse(value); - return uri != null && - (uri.scheme == 'http' || uri.scheme == 'https') && - uri.hasAuthority; - } - - Future _openUrl(BuildContext context, Uri uri) async { - final messenger = ScaffoldMessenger.of(context); - final opened = await launchUrl(uri, mode: LaunchMode.externalApplication); - if (!opened && context.mounted) { - messenger.showSnackBar( - SnackBar(content: Text('Could not open ${uri.toString()}')), - ); - } - } -} - -class _SeverityBadge extends StatelessWidget { - const _SeverityBadge({required this.issue}); - - final StartupDependencyIssue issue; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scheme = theme.colorScheme; - final foregroundColor = issue.isRequired - ? scheme.onErrorContainer - : scheme.onSecondaryContainer; - - return DecoratedBox( - decoration: BoxDecoration( - color: foregroundColor.withValues(alpha: 0.10), - borderRadius: BorderRadius.circular(999), - border: Border.all(color: foregroundColor.withValues(alpha: 0.18)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - child: Text( - issue.isRequired ? 'Required' : 'Recommended', - style: theme.textTheme.labelMedium?.copyWith( - color: foregroundColor, - fontWeight: FontWeight.w600, - ), - ), - ), - ); - } -} - -class _StartupCheckLoadingView extends StatelessWidget { - const _StartupCheckLoadingView(); - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Checking Linux runtime dependencies…'), - ], - ), - ), - ); - } -} diff --git a/apps/chanora_flutter/lib/widgets/voice_compact.dart b/apps/chanora_flutter/lib/widgets/voice_compact.dart index 7213c85..601c46e 100644 --- a/apps/chanora_flutter/lib/widgets/voice_compact.dart +++ b/apps/chanora_flutter/lib/widgets/voice_compact.dart @@ -358,7 +358,6 @@ Future showVoiceDetailsSheet( required String pttBoundInputClass, required bool isTouchOnly, required rust.BridgeAudioProcessingConfig initialAudioConfig, - bool onnxRuntimeAvailable = true, required ValueChanged onModeChanged, required ValueChanged onReleaseTailChanged, required ValueChanged onAudioConfigChanged, @@ -389,7 +388,6 @@ Future showVoiceDetailsSheet( pttBoundInputClass: pttBoundInputClass, isTouchOnly: isTouchOnly, initialAudioConfig: initialAudioConfig, - onnxRuntimeAvailable: onnxRuntimeAvailable, onModeChanged: onModeChanged, onReleaseTailChanged: onReleaseTailChanged, onAudioConfigChanged: onAudioConfigChanged, @@ -413,7 +411,6 @@ class _VoiceSheetBody extends StatefulWidget { required this.pttBoundInputClass, required this.isTouchOnly, required this.initialAudioConfig, - required this.onnxRuntimeAvailable, required this.onModeChanged, required this.onReleaseTailChanged, required this.onAudioConfigChanged, @@ -431,7 +428,6 @@ class _VoiceSheetBody extends StatefulWidget { final String pttBoundInputClass; final bool isTouchOnly; final rust.BridgeAudioProcessingConfig initialAudioConfig; - final bool onnxRuntimeAvailable; final ValueChanged onModeChanged; final ValueChanged onReleaseTailChanged; final ValueChanged onAudioConfigChanged; @@ -466,10 +462,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> { _audioProcessing = AudioProcessingConfigState.fromConfig( widget.initialAudioConfig, ); - _audioProcessing.vadBackend = normalizedVadBackend( - _audioProcessing.vadBackend, - onnxRuntimeAvailable: widget.onnxRuntimeAvailable, - ); // Poll audio stats at 250 ms so TX/RX counters and the level meter // update in real time while the sheet is open, independent of the parent. @@ -785,32 +777,15 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> { const SizedBox(height: 2), SegmentedButton( style: voiceSegmentedButtonStyle(theme), - segments: vadBackendSegmentsForAvailability( - desktop: _isDesktopSileroVadHost, - onnxRuntimeAvailable: widget.onnxRuntimeAvailable, - ), + segments: _isDesktopSileroVadHost + ? desktopVadBackendSegments + : vadBackendSegments, selected: {_audioProcessing.vadBackend}, onSelectionChanged: (s) { - setState( - () => _audioProcessing.vadBackend = normalizedVadBackend( - s.first, - onnxRuntimeAvailable: widget.onnxRuntimeAvailable, - ), - ); + setState(() => _audioProcessing.vadBackend = s.first); _notifyAudioConfig(); }, ), - if (_isDesktopSileroVadHost) ...[ - const SizedBox(height: 4), - Text( - widget.onnxRuntimeAvailable - ? 'Silero needs ONNX Runtime. WebRTC works without it.' - : 'Silero is unavailable because ONNX Runtime was not found. WebRTC is selected.', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], // Debug. const SizedBox(height: 8), diff --git a/apps/chanora_flutter/lib/widgets/voice_settings.dart b/apps/chanora_flutter/lib/widgets/voice_settings.dart index 5d90967..45cd4be 100644 --- a/apps/chanora_flutter/lib/widgets/voice_settings.dart +++ b/apps/chanora_flutter/lib/widgets/voice_settings.dart @@ -67,7 +67,6 @@ class VoiceSettingsDialog extends StatefulWidget { this.pttLevel = '', this.pttBackendId = '', this.pttBoundInputClass = '', - this.onnxRuntimeAvailable = true, this.talkPower, this.neededTalkPower, this.talkPowerGranted, @@ -79,7 +78,6 @@ class VoiceSettingsDialog extends StatefulWidget { final String pttLevel; final String pttBackendId; final String pttBoundInputClass; - final bool onnxRuntimeAvailable; final int? talkPower; final int? neededTalkPower; final bool? talkPowerGranted; @@ -103,10 +101,6 @@ class _VoiceSettingsDialogState extends State { _audioProcessing = AudioProcessingConfigState.fromConfig( widget.initialAudioConfig, ); - _audioProcessing.vadBackend = normalizedVadBackend( - _audioProcessing.vadBackend, - onnxRuntimeAvailable: widget.onnxRuntimeAvailable, - ); } rust.BridgeAudioProcessingConfig _buildConfig() { @@ -297,29 +291,14 @@ class _VoiceSettingsDialogState extends State { const VoiceSubHeader('Backend'), SegmentedButton( style: voiceSegmentedButtonStyle(theme), - segments: vadBackendSegmentsForAvailability( - desktop: _isDesktopSileroVadHost, - onnxRuntimeAvailable: widget.onnxRuntimeAvailable, - ), + segments: _isDesktopSileroVadHost + ? desktopVadBackendSegments + : vadBackendSegments, selected: {_audioProcessing.vadBackend}, - onSelectionChanged: (s) => setState( - () => _audioProcessing.vadBackend = normalizedVadBackend( - s.first, - onnxRuntimeAvailable: widget.onnxRuntimeAvailable, - ), - ), + onSelectionChanged: (s) => + setState(() => _audioProcessing.vadBackend = s.first), ), const SizedBox(height: 8), - if (_isDesktopSileroVadHost) - Text( - widget.onnxRuntimeAvailable - ? 'Silero gives the best quality when ONNX Runtime is installed. WebRTC works without ONNX Runtime and is the safer fallback if Linux setup is incomplete.' - : 'Silero is unavailable because ONNX Runtime was not found. WebRTC is selected until libonnxruntime.so is installed or bundled.', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - if (_isDesktopSileroVadHost) const SizedBox(height: 8), // ── PTT capability badge ──────────────────────────────── if (_mode == rust.BridgeTransmitMode.ptt && diff --git a/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart b/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart index 41cb657..7c15441 100644 --- a/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart +++ b/apps/chanora_flutter/lib/widgets/voice_settings_controls.dart @@ -59,30 +59,15 @@ const vadBackendSegments = [ /// Desktop VAD selector segments. /// -/// Desktop keeps Silero as the default, but WebRTC remains a supported -/// manual fallback when ONNX Runtime is missing or when the user wants a -/// smaller dependency surface. -const desktopVadBackendSegments = vadBackendSegments; - -List> vadBackendSegmentsForAvailability({ - required bool desktop, - required bool onnxRuntimeAvailable, -}) { - final segments = desktop ? desktopVadBackendSegments : vadBackendSegments; - if (!desktop || onnxRuntimeAvailable) return segments; - return [ - for (final segment in segments) - if (segment.value == rust.BridgeVadBackend.sileroOnnx) - ButtonSegment( - value: segment.value, - label: segment.label, - icon: segment.icon, - enabled: false, - ) - else - segment, - ]; -} +/// Windows and Linux use Silero as the primary VAD. WebRTC remains an +/// internal runtime fallback when the model/runtime is unavailable. +const desktopVadBackendSegments = [ + ButtonSegment( + value: rust.BridgeVadBackend.sileroOnnx, + label: Text('Silero'), + icon: Icon(Icons.psychology, size: 14), + ), +]; /// Section subheader used by both voice settings surfaces. class VoiceSubHeader extends StatelessWidget { diff --git a/apps/chanora_flutter/linux/CMakeLists.txt b/apps/chanora_flutter/linux/CMakeLists.txt index da17239..48b596b 100644 --- a/apps/chanora_flutter/linux/CMakeLists.txt +++ b/apps/chanora_flutter/linux/CMakeLists.txt @@ -9,21 +9,6 @@ set(BINARY_NAME "chanora_flutter") # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "app.chanora.chanora_flutter") -# Repository root for Rust bridge builds / bundle staging. -set(CHANORA_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../..") - -# Optional path to a Linux ONNX Runtime shared library for Silero VAD. -# When set, the bundle carries a local `lib/libonnxruntime.so` sidecar and the -# runner exports `ORT_DYLIB_PATH` to that file before Flutter starts. -set(CHANORA_ONNXRUNTIME_SHARED_LIB "$ENV{CHANORA_ONNXRUNTIME_SHARED_LIB}" CACHE FILEPATH - "Absolute path to libonnxruntime.so for Linux bundle packaging") - -# Optional path to a prebuilt Rust bridge shared library. When unset, the Linux -# bundle build runs `cargo build -p chanora_bridge` for the host profile and -# stages the resulting `libchanora_bridge.so` automatically. -set(CHANORA_BRIDGE_SHARED_LIB "$ENV{CHANORA_BRIDGE_SHARED_LIB}" CACHE FILEPATH - "Absolute path to libchanora_bridge.so for Linux bundle packaging") - # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) @@ -68,7 +53,6 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -find_program(CARGO_EXECUTABLE cargo REQUIRED) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") @@ -76,27 +60,6 @@ add_subdirectory("runner") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) -if(CMAKE_BUILD_TYPE MATCHES "Debug") - set(CHANORA_BRIDGE_PROFILE_DIR "debug") - set(CHANORA_BRIDGE_CARGO_ARGS build --package chanora_bridge) -else() - set(CHANORA_BRIDGE_PROFILE_DIR "release") - set(CHANORA_BRIDGE_CARGO_ARGS build --release --package chanora_bridge) -endif() - -if(CHANORA_BRIDGE_SHARED_LIB) - set(CHANORA_BRIDGE_STAGED_LIB "${CHANORA_BRIDGE_SHARED_LIB}") -else() - set(CHANORA_BRIDGE_STAGED_LIB - "${CHANORA_REPO_ROOT}/target/${CHANORA_BRIDGE_PROFILE_DIR}/libchanora_bridge.so") - add_custom_target(chanora_bridge_bundle - COMMAND "${CARGO_EXECUTABLE}" ${CHANORA_BRIDGE_CARGO_ARGS} - WORKING_DIRECTORY "${CHANORA_REPO_ROOT}" - COMMENT "Building chanora_bridge for Linux bundle" - VERBATIM) - add_dependencies(${BINARY_NAME} chanora_bridge_bundle) -endif() - # 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 @@ -137,29 +100,6 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR} install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -if(EXISTS "${CHANORA_BRIDGE_STAGED_LIB}") - install(FILES "${CHANORA_BRIDGE_STAGED_LIB}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -else() - message(WARNING - "Chanora bridge shared library was not found for Linux bundling: " - "${CHANORA_BRIDGE_STAGED_LIB}") -endif() - -if(CHANORA_ONNXRUNTIME_SHARED_LIB) - if(EXISTS "${CHANORA_ONNXRUNTIME_SHARED_LIB}") - install(FILES "${CHANORA_ONNXRUNTIME_SHARED_LIB}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - RENAME "libonnxruntime.so" - COMPONENT Runtime) - else() - message(WARNING - "CHANORA_ONNXRUNTIME_SHARED_LIB was set but the file was not found: " - "${CHANORA_ONNXRUNTIME_SHARED_LIB}") - endif() -endif() - foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" diff --git a/apps/chanora_flutter/linux/runner/my_application.cc b/apps/chanora_flutter/linux/runner/my_application.cc index d8529f3..ec1f808 100644 --- a/apps/chanora_flutter/linux/runner/my_application.cc +++ b/apps/chanora_flutter/linux/runner/my_application.cc @@ -14,27 +14,6 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) -static void configure_onnxruntime_dylib_path() { - const gchar* existing = g_getenv("ORT_DYLIB_PATH"); - if (existing != nullptr && *existing != '\0') { - return; - } - - g_autofree gchar* exe_path = g_file_read_link("/proc/self/exe", nullptr); - if (exe_path == nullptr) { - return; - } - - g_autofree gchar* exe_dir = g_path_get_dirname(exe_path); - g_autofree gchar* ort_path = - g_build_filename(exe_dir, "lib", "libonnxruntime.so", nullptr); - if (!g_file_test(ort_path, G_FILE_TEST_IS_REGULAR)) { - return; - } - - g_setenv("ORT_DYLIB_PATH", ort_path, TRUE); -} - // Called when first Flutter frame received. static void first_frame_cb(MyApplication* self, FlView* view) { gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); @@ -131,7 +110,6 @@ static void my_application_startup(GApplication* application) { // MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application startup. - configure_onnxruntime_dylib_path(); G_APPLICATION_CLASS(my_application_parent_class)->startup(application); } diff --git a/apps/chanora_flutter/test/services/connection_phase_state_test.dart b/apps/chanora_flutter/test/services/connection_phase_state_test.dart index 14cb2cf..0e34b3e 100644 --- a/apps/chanora_flutter/test/services/connection_phase_state_test.dart +++ b/apps/chanora_flutter/test/services/connection_phase_state_test.dart @@ -21,6 +21,69 @@ void main() { expect(ConnectionPhase.connected.canDisconnect, isTrue); }); + test('server-reachable phases show loading until snapshot is available', () { + expect( + ConnectionPhase.connecting.shouldShowSnapshotLoading(hasSnapshot: false), + isFalse, + ); + expect( + ConnectionPhase.synchronizing.shouldShowSnapshotLoading( + hasSnapshot: false, + ), + isTrue, + ); + expect( + ConnectionPhase.connected.shouldShowSnapshotLoading(hasSnapshot: false), + isTrue, + ); + expect( + ConnectionPhase.reconnecting.shouldShowSnapshotLoading( + hasSnapshot: false, + ), + isTrue, + ); + expect( + ConnectionPhase.synchronizing.shouldShowSnapshotLoading( + hasSnapshot: true, + ), + isFalse, + ); + }); + + test('chat only opens after a snapshot is available', () { + expect( + ConnectionPhase.synchronizing.canOpenChatWithSnapshot(hasSnapshot: false), + isFalse, + ); + expect( + ConnectionPhase.synchronizing.canOpenChatWithSnapshot(hasSnapshot: true), + isTrue, + ); + expect( + ConnectionPhase.connected.canOpenChatWithSnapshot(hasSnapshot: true), + isTrue, + ); + expect( + ConnectionPhase.reconnecting.canOpenChatWithSnapshot(hasSnapshot: true), + isFalse, + ); + }); + + test('snapshot application completes synchronizing phase', () { + expect( + phaseAfterSnapshotApplied(ConnectionPhase.synchronizing), + ConnectionPhase.connected, + ); + expect( + phaseAfterSnapshotApplied(ConnectionPhase.connected), + ConnectionPhase.connected, + ); + expect( + phaseAfterSnapshotApplied(ConnectionPhase.reconnecting), + ConnectionPhase.reconnecting, + ); + }); + test('connection status text maps all phases', () { expect( connectionStatusText(phase: ConnectionPhase.idle, l10n: l10n), @@ -78,4 +141,22 @@ void main() { expect(ConnectionPhase.reconnecting.tokens(scheme).icon, Icons.restart_alt); expect(ConnectionPhase.disconnected.tokens(scheme).icon, Icons.cloud_off); }); + + test('connected event preserves connected phase when snapshot exists', () { + expect( + phaseAfterConnectedEvent(ConnectionPhase.connected, hasSnapshot: true), + ConnectionPhase.connected, + ); + }); + + test('connected event synchronizes when snapshot is missing', () { + expect( + phaseAfterConnectedEvent(ConnectionPhase.connected, hasSnapshot: false), + ConnectionPhase.synchronizing, + ); + expect( + phaseAfterConnectedEvent(ConnectionPhase.reconnecting, hasSnapshot: true), + ConnectionPhase.synchronizing, + ); + }); } diff --git a/apps/chanora_flutter/test/services/prefetch_debouncer_test.dart b/apps/chanora_flutter/test/services/prefetch_debouncer_test.dart new file mode 100644 index 0000000..8cfecc7 --- /dev/null +++ b/apps/chanora_flutter/test/services/prefetch_debouncer_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chanora_flutter/services/prefetch_debouncer.dart'; + +void main() { + test('debounces host edits and prefetches latest trimmed host', () async { + final calls = []; + final debouncer = PrefetchDebouncer( + delay: const Duration(milliseconds: 20), + onPrefetch: (host) async => calls.add(host), + ); + + debouncer.schedule(' first.example.com '); + debouncer.schedule(' second.example.com '); + await Future.delayed(const Duration(milliseconds: 35)); + + expect(calls, ['second.example.com']); + debouncer.dispose(); + }); + + test('skips empty hosts', () async { + final calls = []; + final debouncer = PrefetchDebouncer( + delay: const Duration(milliseconds: 10), + onPrefetch: (host) async => calls.add(host), + ); + + debouncer.schedule(' '); + await Future.delayed(const Duration(milliseconds: 25)); + + expect(calls, isEmpty); + debouncer.dispose(); + }); + + test('dispose cancels pending prefetch', () async { + final calls = []; + final debouncer = PrefetchDebouncer( + delay: const Duration(milliseconds: 30), + onPrefetch: (host) async => calls.add(host), + ); + + debouncer.schedule('example.com'); + debouncer.dispose(); + await Future.delayed(const Duration(milliseconds: 45)); + + expect(calls, isEmpty); + }); +} diff --git a/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart b/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart index 1ff3901..af67869 100644 --- a/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart +++ b/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart @@ -18,7 +18,6 @@ void main() { rust.BridgeClient client({ BigInt? id, BigInt? channelId, - String uid = '', int talkPower = 0, bool talkPowerGranted = false, bool inputMuted = false, @@ -26,7 +25,6 @@ void main() { }) { return rust.BridgeClient( id: id ?? BigInt.from(7), - uid: uid, channel: channelId ?? BigInt.one, name: 'Me', inputMuted: inputMuted, diff --git a/apps/chanora_flutter/test/services/startup_dependency_check_test.dart b/apps/chanora_flutter/test/services/startup_dependency_check_test.dart deleted file mode 100644 index eba8065..0000000 --- a/apps/chanora_flutter/test/services/startup_dependency_check_test.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'dart:ffi'; -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:chanora_flutter/services/startup_dependency_check.dart'; - -void main() { - tearDown(() { - debugResetStartupDependencyCheck(); - }); - - test('non-linux hosts skip startup dependency issues', () async { - debugResetStartupDependencyCheck(platformIsLinux: () => false); - - final result = await checkStartupDependencies(); - - expect(result.issues, isEmpty); - }); - - test('missing Linux audio runtimes are reported as recommended', () async { - debugResetStartupDependencyCheck( - platformIsLinux: () => true, - libraryProbe: (candidate) => false, - fileExists: (_) async => false, - osReleaseProvider: () async => 'ID=ubuntu\nID_LIKE=debian\n', - resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter', - currentDirectoryProvider: () => '/tmp', - ); - - final result = await checkStartupDependencies(); - - expect(result.hasIssues, isTrue); - final pipewire = result.issues.firstWhere( - (issue) => issue.id == 'linux-pipewire-runtime', - ); - final pulse = result.issues.firstWhere( - (issue) => issue.id == 'linux-pulseaudio-runtime', - ); - expect(pipewire.isRequired, isFalse); - expect(pulse.isRequired, isFalse); - expect( - pipewire.installHints.single.command, - 'sudo apt install libpipewire-0.3-0', - ); - expect(pulse.installHints.single.command, 'sudo apt install libpulse0'); - }); - - test('distro detection parses quoted ID_LIKE lists', () async { - debugResetStartupDependencyCheck( - platformIsLinux: () => true, - libraryProbe: (candidate) => false, - fileExists: (_) async => true, - osReleaseProvider: () async => 'ID=rocky\nID_LIKE="fedora rhel"\n', - resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter', - currentDirectoryProvider: () => '/tmp', - ); - - final result = await checkStartupDependencies(); - - expect(result.platformLabel, 'Fedora'); - final pipewire = result.issues.singleWhere( - (issue) => issue.id == 'linux-pipewire-runtime', - ); - expect( - pipewire.installHints.single.command, - 'sudo dnf install pipewire-libs', - ); - }); - - test( - 'missing ONNX runtime is reported as recommended when Linux audio runtimes are present', - () async { - debugResetStartupDependencyCheck( - platformIsLinux: () => true, - libraryProbe: (candidate) => - candidate.contains('pipewire') || candidate.contains('pulse'), - fileExists: (_) async => false, - osReleaseProvider: () async => 'ID=fedora\n', - resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter', - currentDirectoryProvider: () => '/tmp', - currentAbiProvider: () => Abi.linuxX64, - ); - - final result = await checkStartupDependencies(); - - expect(result.issues, hasLength(1)); - final ort = result.issues.single; - expect(ort.id, 'linux-onnxruntime'); - expect(ort.isRequired, isFalse); - expect( - ort.installHints.any( - (hint) => - hint.command == - 'https://github.com/microsoft/onnxruntime/releases', - ), - isTrue, - ); - expect( - ort.installHints.any( - (hint) => hint.command == 'onnxruntime-linux-x64-.tgz', - ), - isTrue, - ); - expect( - ort.installHints.any((hint) => hint.command.contains('ORT_DYLIB_PATH')), - isTrue, - ); - expect( - ort.details, - contains( - 'This machine needs the Linux x64 CPU archive (onnxruntime-linux-x64-.tgz).', - ), - ); - }, - ); - - test('bundle-local ONNX runtime clears the recommendation', () async { - debugResetStartupDependencyCheck( - platformIsLinux: () => true, - libraryProbe: (candidate) => - candidate.contains('pipewire') || candidate.contains('pulse'), - fileExists: (path) async => path == '/opt/chanora/lib/libonnxruntime.so', - resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter', - currentDirectoryProvider: () => '/tmp', - ); - - final result = await checkStartupDependencies(); - - expect(result.issues, isEmpty); - }); - - test('logging missing startup dependencies appends a log line', () async { - final tempDir = await Directory.systemTemp.createTemp( - 'chanora-startup-log', - ); - addTearDown(() => tempDir.delete(recursive: true)); - final logFile = File('${tempDir.path}/chanora.log'); - - debugResetStartupDependencyCheck(logFilePathProvider: () => logFile.path); - - const result = StartupDependencyCheckResult( - platformLabel: 'Fedora', - issues: [ - StartupDependencyIssue( - id: 'linux-pipewire-runtime', - title: 'PipeWire runtime is missing', - summary: 'Primary Linux audio needs PipeWire.', - details: ['Install PipeWire and recheck.'], - severity: StartupDependencySeverity.recommended, - ), - ], - ); - - await logStartupDependencyIssues(result); - - final text = await logFile.readAsString(); - expect(text, contains('[startup_dependency_check]')); - expect(text, contains('platform="Fedora"')); - expect( - text, - contains( - 'issues=[linux-pipewire-runtime:recommended:PipeWire runtime is missing]', - ), - ); - }); -} diff --git a/apps/chanora_flutter/test/services/ui_preferences_service_test.dart b/apps/chanora_flutter/test/services/ui_preferences_service_test.dart index 78302a4..93613c0 100644 --- a/apps/chanora_flutter/test/services/ui_preferences_service_test.dart +++ b/apps/chanora_flutter/test/services/ui_preferences_service_test.dart @@ -16,6 +16,7 @@ void main() { expect(settings.host, isEmpty); expect(settings.nickname, isEmpty); + expect(settings.themeMode, UiThemeMode.system); }); test('saves and loads host and nickname independently', () async { @@ -40,78 +41,30 @@ void main() { expect(await service.hasExplainedPermissions(), isTrue); }); - test('stores per-user playback preferences per server host', () async { - await service.saveClientPlaybackPreference( - serverHost: 'Example.COM', - userUid: 'user-a', - volume: 0.5, - muted: true, - ); - await service.saveClientPlaybackPreference( - serverHost: 'other.example', - userUid: 'user-a', - volume: 2.0, - muted: false, - ); + test('saves and loads theme mode independently', () async { + await service.saveSettings(host: 'example.com', nickname: 'Chanora'); + await service.saveThemeMode(UiThemeMode.dark); - final examplePrefs = await service.loadClientPlaybackPreferencesForServer( - 'example.com', - ); - final otherPrefs = await service.loadClientPlaybackPreferencesForServer( - 'other.example', - ); + var settings = await service.loadSettings(); - expect(examplePrefs['user-a']?.volume, 0.5); - expect(examplePrefs['user-a']?.muted, isTrue); - expect(otherPrefs['user-a']?.volume, 2.0); - expect(otherPrefs['user-a']?.muted, isFalse); + expect(settings.host, 'example.com'); + expect(settings.nickname, 'Chanora'); + expect(settings.themeMode, UiThemeMode.dark); + + await service.saveThemeMode(UiThemeMode.light); + settings = await service.loadSettings(); + + expect(settings.host, 'example.com'); + expect(settings.nickname, 'Chanora'); + expect(settings.themeMode, UiThemeMode.light); }); - test( - 'drops default playback preferences instead of persisting them', - () async { - await service.saveClientPlaybackPreference( - serverHost: 'example.com', - userUid: 'user-a', - volume: 0.2, - muted: true, - ); - await service.saveClientPlaybackPreference( - serverHost: 'example.com', - userUid: 'user-a', - volume: 1.0, - muted: false, - ); + test('falls back to system theme mode for invalid stored values', () async { + SharedPreferences.setMockInitialValues({'ui.theme_mode': 'sepia'}); + service = const UiPreferencesService(); - final prefs = await service.loadClientPlaybackPreferencesForServer( - 'example.com', - ); - expect(prefs, isEmpty); - }, - ); + final settings = await service.loadSettings(); - test('ignores corrupt playback preference JSON', () async { - SharedPreferences.setMockInitialValues({ - 'audio.client_playback_prefs': '{not-json', - }); - - final prefs = await service.loadClientPlaybackPreferencesForServer( - 'example.com', - ); - - expect(prefs, isEmpty); - }); - - test('clamps invalid playback volumes to unity', () async { - SharedPreferences.setMockInitialValues({ - 'audio.client_playback_prefs': - '{"example.com|user-a":{"volume":"invalid","muted":false}}', - }); - - final prefs = await service.loadClientPlaybackPreferencesForServer( - 'example.com', - ); - - expect(prefs['user-a']?.volume, 1.0); + expect(settings.themeMode, UiThemeMode.system); }); } diff --git a/apps/chanora_flutter/test/widget_test.dart b/apps/chanora_flutter/test/widget_test.dart index ccb3286..42eeb11 100644 --- a/apps/chanora_flutter/test/widget_test.dart +++ b/apps/chanora_flutter/test/widget_test.dart @@ -18,12 +18,44 @@ import 'package:flutter_test/flutter_test.dart'; // ignore_for_file: deprecated_member_use import 'package:chanora_flutter/l10n/generated/app_localizations.dart'; +import 'package:chanora_flutter/main.dart'; import 'package:chanora_flutter/services/android_permissions_service.dart'; import 'package:chanora_flutter/services/ios_permissions_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:chanora_flutter/widgets/permission_state_banner.dart'; import 'package:chanora_flutter/widgets/voice_compact.dart'; void main() { + testWidgets('theme menu emits selected mode from user action', (tester) async { + SharedPreferences.setMockInitialValues({}); + var selected = ThemeMode.system; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + actions: [ + ChanoraThemeModeMenu( + themeMode: ThemeMode.system, + onThemeModeChanged: (mode) async { + selected = mode; + }, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.palette_outlined)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Dark').last); + await tester.pumpAndSettle(); + + expect(selected, ThemeMode.dark); + }); + testWidgets('renders English banner', (tester) async { await tester.pumpWidget( MaterialApp( diff --git a/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart b/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart index cb575c0..bc3053a 100644 --- a/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart +++ b/apps/chanora_flutter/test/widgets/audio_processing_config_state_test.dart @@ -31,14 +31,14 @@ void main() { expect(state.agcEnabled, isTrue); }); - test('desktop VAD normalization keeps explicit WebRTC selections', () { + test('normalizes desktop VAD backend to Silero', () { expect( normalizedVadBackend( rust.BridgeVadBackend.webrtcVad, isWindows: true, isLinux: false, ), - rust.BridgeVadBackend.webrtcVad, + rust.BridgeVadBackend.sileroOnnx, ); expect( normalizedVadBackend( @@ -46,33 +46,10 @@ void main() { isWindows: false, isLinux: true, ), - rust.BridgeVadBackend.webrtcVad, - ); - expect( - normalizedVadBackend( - rust.BridgeVadBackend.energyDebug, - isWindows: true, - isLinux: false, - ), rust.BridgeVadBackend.sileroOnnx, ); }); - test( - 'desktop VAD normalization falls back when ONNX Runtime is unavailable', - () { - expect( - normalizedVadBackend( - rust.BridgeVadBackend.sileroOnnx, - isWindows: false, - isLinux: true, - onnxRuntimeAvailable: false, - ), - rust.BridgeVadBackend.webrtcVad, - ); - }, - ); - test('default config matches the current platform fallback', () { final fallback = defaultAudioProcessingConfig(); final desktop = Platform.isWindows || Platform.isLinux; @@ -126,7 +103,6 @@ void main() { test('builds Windows/Linux software WebRTC APM config consistently', () { final state = AudioProcessingConfigState.fromConfig(baseConfig) - ..vadBackend = rust.BridgeVadBackend.webrtcVad ..nsEnabled = true ..aecEnabled = false ..agcEnabled = true; @@ -151,7 +127,7 @@ void main() { for (final config in [windowsConfig, linuxConfig]) { expect(config.processingBackend, rust.BridgeAudioBackend.webrtcApm); - expect(config.vadBackend, rust.BridgeVadBackend.webrtcVad); + expect(config.vadBackend, rust.BridgeVadBackend.sileroOnnx); expect(config.aec, rust.BridgeEffectOwner.off); expect(config.ns, rust.BridgeEffectOwner.webrtcApm); expect(config.agc, rust.BridgeEffectOwner.webrtcApm); diff --git a/apps/chanora_flutter/test/widgets/chat_views_test.dart b/apps/chanora_flutter/test/widgets/chat_views_test.dart index f9692f4..d81512e 100644 --- a/apps/chanora_flutter/test/widgets/chat_views_test.dart +++ b/apps/chanora_flutter/test/widgets/chat_views_test.dart @@ -36,12 +36,10 @@ void main() { required BigInt id, required String name, required BigInt channelId, - String uid = '', bool isServerQuery = false, }) { return rust.BridgeClient( id: id, - uid: uid, channel: channelId, name: name, inputMuted: false, @@ -466,6 +464,8 @@ void main() { ) async { await tester.pumpWidget( MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, home: ChatPage( messages: [ entry(rust.BridgeMessageTarget.poke(BigInt.from(2))), @@ -498,15 +498,7 @@ void main() { final privateTop = tester.getTopLeft(find.text('Alpha').first).dy; final serverTileSize = tester.getSize( find - .ancestor( - of: find.byIcon(Icons.dns_outlined), - matching: find.byWidgetPredicate( - (widget) => - widget is SizedBox && - widget.width == 92 && - widget.height == 92, - ), - ) + .ancestor(of: find.text('Server').first, matching: find.byType(Ink)) .first, ); @@ -516,63 +508,23 @@ void main() { expect(serverTileSize.height, 92); }); - testWidgets('chat sidebar uses MD3-style selected container colors', ( + testWidgets('chat page uses a compact top rail on phone width', ( tester, ) async { - final theme = ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), - ); - - await tester.pumpWidget( - MaterialApp( - theme: theme, - home: ChatPage( - messages: const [], - snapshot: snapshot(channels: const [], clients: const []), - initialTarget: const rust.BridgeMessageTarget.server(), - ), - ), - ); - - final serverIndicator = find.ancestor( - of: find.byIcon(Icons.dns_outlined), - matching: find.byType(AnimatedContainer), - ); - final channelIndicator = find.ancestor( - of: find.byIcon(Icons.tag), - matching: find.byType(AnimatedContainer), - ); - - final serverDecoration = - tester.widget(serverIndicator).decoration - as BoxDecoration; - final channelDecoration = - tester.widget(channelIndicator).decoration - as BoxDecoration; - final serverLabel = tester.widget(find.text('Server').last); - final channelLabel = tester.widget(find.text('Channel')); - - expect(tester.getSize(serverIndicator), const Size(76, 76)); - expect(tester.getSize(channelIndicator), const Size(76, 76)); - expect(serverDecoration.color, theme.colorScheme.primaryContainer); - expect(channelDecoration.color, Colors.transparent); - expect(serverLabel.style?.color, theme.colorScheme.onPrimaryContainer); - expect(channelLabel.style?.color, theme.colorScheme.onSurfaceVariant); - }); - - testWidgets('long private chat labels stay within fixed rail indicator', ( - tester, - ) async { - const longName = 'Very Long Private Chat Name That Should Not Stretch'; + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, home: ChatPage( messages: [ ChatEntry( senderId: BigInt.from(2), - senderName: longName, + senderName: 'Alpha', message: 'Private', target: rust.BridgeMessageTarget.client(BigInt.from(2)), ), @@ -580,29 +532,24 @@ void main() { snapshot: snapshot( channels: const [], clients: [ - client( - id: BigInt.from(2), - name: longName, - channelId: BigInt.zero, - ), + client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.zero), ], ), - initialTarget: rust.BridgeMessageTarget.client(BigInt.from(2)), - initialClientName: longName, ), ), ); - final selectedIndicator = find.ancestor( - of: find.text(longName).first, - matching: find.byType(AnimatedContainer), - ); - final privateLabel = tester.widget(find.text(longName).first); + await tester.pumpAndSettle(); - expect(tester.getSize(selectedIndicator), const Size(76, 76)); - expect(privateLabel.maxLines, 2); - expect(privateLabel.overflow, TextOverflow.ellipsis); - expect(privateLabel.textAlign, TextAlign.center); + expect(find.byType(VerticalDivider), findsNothing); + final serverTileSize = tester.getSize( + find + .ancestor(of: find.text('Server').first, matching: find.byType(Ink)) + .first, + ); + expect(serverTileSize.width, lessThan(92)); + expect(serverTileSize.height, lessThan(92)); + expect(tester.getTopLeft(find.text('Private').first).dy, greaterThan(92)); }); testWidgets( @@ -610,6 +557,8 @@ void main() { (tester) async { await tester.pumpWidget( MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, home: ChatPage( messages: [ ChatEntry( @@ -715,6 +664,8 @@ void main() { ) async { await tester.pumpWidget( MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, home: ChatPage( messages: [ ChatEntry( @@ -755,6 +706,8 @@ void main() { await tester.pumpWidget( MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, home: ChatPage( messages: messages, snapshot: currentSnapshot, @@ -822,6 +775,8 @@ void main() { Ts3ServerLink? tapped; await tester.pumpWidget( MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, home: ChatPage( messages: [ ChatEntry( diff --git a/apps/chanora_flutter/test/widgets/client_info_sheet_test.dart b/apps/chanora_flutter/test/widgets/client_info_sheet_test.dart new file mode 100644 index 0000000..cc0e42a --- /dev/null +++ b/apps/chanora_flutter/test/widgets/client_info_sheet_test.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chanora_flutter/l10n/generated/app_localizations.dart'; +import 'package:chanora_flutter/src/rust/api.dart' as rust; +import 'package:chanora_flutter/widgets/client_info_sheet.dart'; + +void main() { + rust.BridgeClientProfile profile() { + return rust.BridgeClientProfile( + id: BigInt.from(101), + channel: BigInt.from(7), + name: 'Bob', + uniqueId: 'client-unique-id', + databaseId: BigInt.from(55), + countryCode: 'US', + description: 'Operator', + version: '3.6.2', + platform: 'Windows', + onlineSeconds: 3661, + idleMilliseconds: 42000, + pingMilliseconds: 38, + clientAddress: '203.0.113.24', + serverGroups: const ['Admin', 'Talk Power'], + channelGroup: 'Guest', + avatarPath: '/avatar_aabbcc', + bytesDownloadedMonth: BigInt.from(2048), + bytesUploadedMonth: BigInt.from(4096), + bytesDownloadedTotal: BigInt.from(1048576), + bytesUploadedTotal: BigInt.from(2097152), + packetLossClientToServerTotal: 0.0123, + packetLossServerToClientTotal: 0.0456, + ); + } + + testWidgets('renders loaded profile data in a modal sheet', (tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, + home: Builder( + builder: (context) => Scaffold( + body: Center( + child: FilledButton( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.86, + child: ClientInfoSheet( + clientName: 'Bob', + loadProfile: () async => profile(), + ), + ), + ), + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('Bob'), findsOneWidget); + expect(find.text('Identity'), findsOneWidget); + expect(find.text('Membership'), findsOneWidget); + expect(find.text('Admin, Talk Power'), findsOneWidget); + + await tester.drag(find.byType(ListView), const Offset(0, -360)); + await tester.pumpAndSettle(); + + expect(find.text('Connection'), findsOneWidget); + expect(find.text('1h 1m 1s'), findsOneWidget); + expect(find.text('42.00 s'), findsOneWidget); + expect(find.text('203.0.113.24'), findsOneWidget); + }); + + testWidgets('loading and error states use localized copy', (tester) async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('zh'), + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, + home: Scaffold( + body: SizedBox( + height: 500, + child: ClientInfoSheet( + clientName: 'Bob', + loadProfile: () => Future.error( + StateError('not available'), + ), + ), + ), + ), + ), + ); + + expect(find.text('正在加载资料…'), findsOneWidget); + expect(find.text('Loading profile...'), findsNothing); + + await tester.pumpAndSettle(); + + expect(find.text('资料不可用'), findsOneWidget); + expect(find.text('重试'), findsOneWidget); + expect(find.text('Profile unavailable'), findsNothing); + }); +} diff --git a/apps/chanora_flutter/test/widgets/connect_widgets_test.dart b/apps/chanora_flutter/test/widgets/connect_widgets_test.dart deleted file mode 100644 index 1d3b620..0000000 --- a/apps/chanora_flutter/test/widgets/connect_widgets_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:chanora_flutter/l10n/generated/app_localizations.dart'; -import 'package:chanora_flutter/widgets/connect_widgets.dart'; - -void main() { - testWidgets('pressing enter in password field submits connection', ( - tester, - ) async { - var connectCount = 0; - final hostCtl = TextEditingController(text: 'example.com'); - final nickCtl = TextEditingController(text: 'alice'); - final passwordCtl = TextEditingController(); - addTearDown(hostCtl.dispose); - addTearDown(nickCtl.dispose); - addTearDown(passwordCtl.dispose); - - await tester.pumpWidget( - MaterialApp( - localizationsDelegates: AppL10n.localizationsDelegates, - supportedLocales: AppL10n.supportedLocales, - home: Scaffold( - body: ConnectForm( - hostCtl: hostCtl, - nickCtl: nickCtl, - passwordCtl: passwordCtl, - onConnect: () => connectCount += 1, - onAddBookmark: () {}, - ), - ), - ), - ); - - await tester.enterText(find.byType(TextField).last, 'secret'); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); - - expect(connectCount, 1); - }); -} diff --git a/apps/chanora_flutter/test/widgets/input_dialogs_test.dart b/apps/chanora_flutter/test/widgets/input_dialogs_test.dart deleted file mode 100644 index 553cb51..0000000 --- a/apps/chanora_flutter/test/widgets/input_dialogs_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:chanora_flutter/widgets/input_dialogs.dart'; - -void main() { - test('browser back and forward logical keys map to mouse side bindings', () { - expect( - pttMouseSideButtonPlatformKeyForLogicalKey( - LogicalKeyboardKey.browserBack, - ), - 'mouse-side-button:8', - ); - expect( - pttMouseSideButtonPlatformKeyForLogicalKey(LogicalKeyboardKey.goBack), - 'mouse-side-button:8', - ); - expect( - pttMouseSideButtonPlatformKeyForLogicalKey( - LogicalKeyboardKey.browserForward, - ), - 'mouse-side-button:16', - ); - }); - - test('pointer button bitmasks map to mouse side bindings', () { - expect( - pttMouseSideButtonPlatformKeyForButtons(0x08), - 'mouse-side-button:8', - ); - expect( - pttMouseSideButtonPlatformKeyForButtons(0x10), - 'mouse-side-button:16', - ); - expect( - pttMouseSideButtonPlatformKeyForButtons(0x18), - 'mouse-side-button:8', - ); - expect(pttMouseSideButtonPlatformKeyForButtons(0x00), isNull); - }); -} diff --git a/apps/chanora_flutter/test/widgets/mobile_ui_resilience_test.dart b/apps/chanora_flutter/test/widgets/mobile_ui_resilience_test.dart new file mode 100644 index 0000000..da29a6f --- /dev/null +++ b/apps/chanora_flutter/test/widgets/mobile_ui_resilience_test.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chanora_flutter/l10n/generated/app_localizations.dart'; +import 'package:chanora_flutter/main.dart'; +import 'package:chanora_flutter/widgets/connect_widgets.dart'; +import 'package:chanora_flutter/widgets/input_dialogs.dart'; +import 'package:chanora_flutter/widgets/ptt_capability_badge.dart'; + +void main() { + Widget localizedHarness(Widget child, {Locale? locale}) { + return MaterialApp( + locale: locale, + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, + home: Scaffold(body: child), + ); + } + + testWidgets('connect actions stack on narrow mobile widths', (tester) async { + await tester.pumpWidget( + localizedHarness( + SizedBox( + width: 320, + child: ConnectForm( + hostCtl: TextEditingController(text: 'server.example.com'), + nickCtl: TextEditingController(text: 'MobileUser'), + passwordCtl: TextEditingController(), + onConnect: () {}, + onAddBookmark: () {}, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + final connectTop = tester.getTopLeft(find.text('Connect')).dy; + final bookmarkTop = tester.getTopLeft(find.text('Save bookmark')).dy; + + expect(bookmarkTop, greaterThan(connectTop + 44)); + }); + + testWidgets('connect actions stack at standard phone content widths', ( + tester, + ) async { + await tester.pumpWidget( + localizedHarness( + SizedBox( + width: 361, + child: ConnectForm( + hostCtl: TextEditingController(text: 'server.example.com'), + nickCtl: TextEditingController(text: 'MobileUser'), + passwordCtl: TextEditingController(), + onConnect: () {}, + onAddBookmark: () {}, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + final connectTop = tester.getTopLeft(find.text('Connect')).dy; + final bookmarkTop = tester.getTopLeft(find.text('Save bookmark')).dy; + + expect(bookmarkTop, greaterThan(connectTop + 44)); + }); + + testWidgets('disconnected mobile chrome keeps content near the safe area', ( + tester, + ) async { + const primaryContentKey = Key('primary-content'); + + await tester.binding.setSurfaceSize(const Size(393, 852)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + localizedHarness( + ChanoraMobileScaffold( + compactIdleChrome: true, + canDisconnect: false, + title: null, + actions: [ + IconButton( + tooltip: 'About', + icon: const Icon(Icons.info_outline), + onPressed: () {}, + ), + ], + onDisconnect: () {}, + compactHeader: const SizedBox(height: 44, child: Text('Chanora')), + body: const KeyedSubtree( + key: primaryContentKey, + child: Text('Primary content'), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(AppBar), findsNothing); + expect(tester.getTopLeft(find.byKey(primaryContentKey)).dy, lessThan(120)); + }); + + testWidgets('PTT explanation sheet is scrollable for mobile text scaling', ( + tester, + ) async { + await tester.pumpWidget( + localizedHarness( + MediaQuery( + data: const MediaQueryData(textScaler: TextScaler.linear(1.8)), + child: const SizedBox( + width: 320, + child: PttCapabilityBadge( + level: 'L0Focused', + backendId: 'focused', + boundInputClass: 'keyboard', + ), + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.info_outline)); + await tester.pumpAndSettle(); + + expect(find.byType(SingleChildScrollView), findsWidgets); + expect(tester.takeException(), isNull); + }); + + testWidgets('PTT capture dialog wraps content for small screens', ( + tester, + ) async { + await tester.pumpWidget( + localizedHarness( + Builder( + builder: (context) { + return Center( + child: FilledButton( + onPressed: () => showDialog( + context: context, + builder: (_) => const PttBindingCaptureDialog(), + ), + child: const Text('Open'), + ), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(tester.takeException(), isNull); + }); +} diff --git a/apps/chanora_flutter/test/widgets/snapshot_view_test.dart b/apps/chanora_flutter/test/widgets/snapshot_view_test.dart index 54ffb93..0e151ca 100644 --- a/apps/chanora_flutter/test/widgets/snapshot_view_test.dart +++ b/apps/chanora_flutter/test/widgets/snapshot_view_test.dart @@ -1,9 +1,8 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:chanora_flutter/l10n/generated/app_localizations.dart'; -import 'package:chanora_flutter/services/ui_preferences_service.dart'; import 'package:chanora_flutter/src/rust/api.dart' as rust; import 'package:chanora_flutter/widgets/snapshot_view.dart'; @@ -30,14 +29,12 @@ void main() { required int id, required int channelId, required String name, - String uid = '', bool speaking = false, int talkPower = 0, bool talkPowerGranted = false, }) { return rust.BridgeClient( id: BigInt.from(id), - uid: uid, channel: BigInt.from(channelId), name: name, inputMuted: false, @@ -52,11 +49,14 @@ void main() { Widget snapshotHarness({ required List channels, required List clients, + String welcomeMessage = '', BigInt? ownClientId, BigInt? currentVoiceChannelId, rust.BridgeAudioStats? audioStats, - Map clientPlaybackPreferences = const {}, - ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged, + bool enableClientLongPressMenu = false, + ValueChanged? onOpenClientInfo, + ValueChanged? onOpenClientChat, + ValueChanged? onOpenClientPoke, }) { return MaterialApp( localizationsDelegates: AppL10n.localizationsDelegates, @@ -65,7 +65,7 @@ void main() { body: SnapshotView( snapshot: rust.BridgeSnapshot( serverName: 'Server', - welcomeMessage: '', + welcomeMessage: welcomeMessage, platform: '', version: '', channels: channels, @@ -81,8 +81,10 @@ void main() { canJoinVoiceChannel: true, onJoinChannel: (_) {}, onJoinChannelWithPassword: (_) {}, - clientPlaybackPreferences: clientPlaybackPreferences, - onClientPlaybackPreferenceChanged: onClientPlaybackPreferenceChanged, + enableClientLongPressMenu: enableClientLongPressMenu, + onOpenClientInfo: onOpenClientInfo, + onOpenClientChat: onOpenClientChat, + onOpenClientPoke: onOpenClientPoke, ), ), ); @@ -197,6 +199,27 @@ void main() { expect(childUserX - childX, inInclusiveRange(22, 28)); }); + testWidgets('server welcome starts collapsed above channel tree', ( + tester, + ) async { + await tester.pumpWidget( + snapshotHarness( + welcomeMessage: + 'This is a long server welcome that should not push channels ' + 'below the first connected screen.', + channels: [channel(id: 1, name: 'Default Channel')], + clients: [client(id: 100, channelId: 1, name: 'Alice')], + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Server welcome message'), findsOneWidget); + expect(find.textContaining('long server welcome'), findsNothing); + expect(find.text('Default Channel'), findsOneWidget); + expect(find.text('Alice'), findsOneWidget); + }); + testWidgets('password channel shows lock at row end', (tester) async { await tester.pumpWidget( snapshotHarness( @@ -242,6 +265,219 @@ void main() { expect(userHighlight.decoration, isNull); }); + testWidgets('long press opens client menu for other users', (tester) async { + rust.BridgeClient? chatClient; + rust.BridgeClient? pokeClient; + + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + enableClientLongPressMenu: true, + onOpenClientChat: (client) => chatClient = client, + onOpenClientPoke: (client) => pokeClient = client, + ), + ); + + await tester.pumpAndSettle(); + await tester.longPress(find.text('Bob')); + await tester.pumpAndSettle(); + + expect(find.text('Private message'), findsOneWidget); + expect(find.text('Poke'), findsOneWidget); + + await tester.tap(find.text('Private message')); + await tester.pumpAndSettle(); + + expect(chatClient?.id, BigInt.from(101)); + expect(chatClient?.name, 'Bob'); + expect(pokeClient, isNull); + }); + + testWidgets('long press is ignored when mobile client menus are disabled', ( + tester, + ) async { + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + enableClientLongPressMenu: false, + onOpenClientChat: (_) {}, + onOpenClientPoke: (_) {}, + ), + ); + + await tester.pumpAndSettle(); + await tester.longPress(find.text('Bob')); + await tester.pumpAndSettle(); + + expect(find.text('Private message'), findsNothing); + expect(find.text('Poke'), findsNothing); + }); + + testWidgets('secondary click opens client menu for other users', ( + tester, + ) async { + rust.BridgeClient? chatClient; + + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + onOpenClientChat: (client) => chatClient = client, + ), + ); + + await tester.pumpAndSettle(); + await tester.tap( + find.text('Bob'), + buttons: kSecondaryMouseButton, + kind: PointerDeviceKind.mouse, + ); + await tester.pumpAndSettle(); + + expect(find.text('Private message'), findsOneWidget); + + await tester.tap(find.text('Private message')); + await tester.pumpAndSettle(); + + expect(chatClient?.id, BigInt.from(101)); + }); + + testWidgets('client menu poke action targets selected other user', ( + tester, + ) async { + rust.BridgeClient? pokeClient; + + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + enableClientLongPressMenu: true, + onOpenClientPoke: (client) => pokeClient = client, + ), + ); + + await tester.pumpAndSettle(); + await tester.longPress(find.text('Bob')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Poke')); + await tester.pumpAndSettle(); + + expect(pokeClient?.id, BigInt.from(101)); + expect(pokeClient?.name, 'Bob'); + expect(find.text('Poke'), findsNothing); + }); + + testWidgets('client menu info action targets selected other user', ( + tester, + ) async { + rust.BridgeClient? infoClient; + + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + enableClientLongPressMenu: true, + onOpenClientInfo: (client) => infoClient = client, + ), + ); + + await tester.pumpAndSettle(); + await tester.longPress(find.text('Bob')); + await tester.pumpAndSettle(); + + expect(find.text('Info'), findsOneWidget); + + await tester.tap(find.text('Info')); + await tester.pumpAndSettle(); + + expect(infoClient?.id, BigInt.from(101)); + expect(infoClient?.name, 'Bob'); + }); + + testWidgets('client menu info action is available for self', (tester) async { + rust.BridgeClient? infoClient; + rust.BridgeClient? chatClient; + rust.BridgeClient? pokeClient; + + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + enableClientLongPressMenu: true, + onOpenClientInfo: (client) => infoClient = client, + onOpenClientChat: (client) => chatClient = client, + onOpenClientPoke: (client) => pokeClient = client, + ), + ); + + await tester.pumpAndSettle(); + await tester.longPress(find.text('Alice')); + await tester.pumpAndSettle(); + + expect(find.text('Info'), findsOneWidget); + expect(find.text('Private message'), findsNothing); + expect(find.text('Poke'), findsNothing); + + await tester.tap(find.text('Info')); + await tester.pumpAndSettle(); + + expect(infoClient?.id, BigInt.from(100)); + expect(infoClient?.name, 'Alice'); + expect(chatClient, isNull); + expect(pokeClient, isNull); + }); + + testWidgets('self menu is hidden when only peer actions are available', ( + tester, + ) async { + await tester.pumpWidget( + snapshotHarness( + channels: [channel(id: 1, name: 'Default Channel')], + clients: [ + client(id: 100, channelId: 1, name: 'Alice'), + client(id: 101, channelId: 1, name: 'Bob'), + ], + ownClientId: BigInt.from(100), + enableClientLongPressMenu: true, + onOpenClientChat: (_) {}, + onOpenClientPoke: (_) {}, + ), + ); + + await tester.pumpAndSettle(); + await tester.longPress(find.text('Alice')); + await tester.pumpAndSettle(); + + expect(find.text('Private message'), findsNothing); + expect(find.text('Poke'), findsNothing); + }); + testWidgets( 'local voice activity does not light speaking state when blocked', (tester) async { @@ -270,114 +506,6 @@ void main() { }, ); - testWidgets('remote client row does not show playback ellipsis', ( - tester, - ) async { - await tester.pumpWidget( - snapshotHarness( - channels: [channel(id: 1, name: 'Default Channel')], - clients: [ - client(id: 100, channelId: 1, name: 'Self', uid: 'self'), - client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'), - ], - ownClientId: BigInt.from(100), - clientPlaybackPreferences: const { - 'user-b': ClientPlaybackPreference(volume: 0.5, muted: false), - }, - onClientPlaybackPreferenceChanged: (_, _) async {}, - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.more_horiz), findsNothing); - }); - - testWidgets('remote client long press opens playback menu', (tester) async { - await tester.pumpWidget( - snapshotHarness( - channels: [channel(id: 1, name: 'Default Channel')], - clients: [ - client(id: 100, channelId: 1, name: 'Self', uid: 'self'), - client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'), - ], - ownClientId: BigInt.from(100), - onClientPlaybackPreferenceChanged: (_, _) async {}, - ), - ); - - await tester.pumpAndSettle(); - await tester.longPress(find.widgetWithText(ListTile, 'Bob')); - await tester.pumpAndSettle(); - - expect(find.text('Mute playback'), findsOneWidget); - expect(find.text('Volume 100%'), findsOneWidget); - }); - - testWidgets('remote client secondary click opens playback menu', ( - tester, - ) async { - await tester.pumpWidget( - snapshotHarness( - channels: [channel(id: 1, name: 'Default Channel')], - clients: [ - client(id: 100, channelId: 1, name: 'Self', uid: 'self'), - client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'), - ], - ownClientId: BigInt.from(100), - onClientPlaybackPreferenceChanged: (_, _) async {}, - ), - ); - - await tester.pumpAndSettle(); - await tester.tap( - find.widgetWithText(ListTile, 'Bob'), - buttons: kSecondaryButton, - ); - await tester.pumpAndSettle(); - - expect(find.text('Mute playback'), findsOneWidget); - expect(find.text('Volume 100%'), findsOneWidget); - }); - - testWidgets('client playback menu forwards mute and volume changes', ( - tester, - ) async { - final changes = []; - - await tester.pumpWidget( - snapshotHarness( - channels: [channel(id: 1, name: 'Default Channel')], - clients: [ - client(id: 100, channelId: 1, name: 'Self', uid: 'self'), - client(id: 101, channelId: 1, name: 'Bob', uid: 'user-b'), - ], - ownClientId: BigInt.from(100), - onClientPlaybackPreferenceChanged: (_, preference) async { - changes.add(preference); - }, - ), - ); - - await tester.pumpAndSettle(); - await tester.longPress(find.widgetWithText(ListTile, 'Bob')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Mute playback')); - await tester.pumpAndSettle(); - - expect(changes, isNotEmpty); - expect(changes.last.muted, isTrue); - - await tester.longPress(find.widgetWithText(ListTile, 'Bob')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Volume 200%'), warnIfMissed: false); - await tester.pumpAndSettle(); - - expect(changes.last.volume, 2.0); - expect(changes.last.muted, isFalse); - }); - testWidgets('spacer channels render as layout rows and keep channel taps', ( tester, ) async { diff --git a/apps/chanora_flutter/test/widgets/startup_dependency_screen_test.dart b/apps/chanora_flutter/test/widgets/startup_dependency_screen_test.dart deleted file mode 100644 index 7e88b60..0000000 --- a/apps/chanora_flutter/test/widgets/startup_dependency_screen_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:chanora_flutter/services/startup_dependency_check.dart'; -import 'package:chanora_flutter/widgets/startup_dependency_screen.dart'; - -void main() { - testWidgets('startup gate shows install-help screen and can continue', ( - tester, - ) async { - tester.view.physicalSize = const Size(1200, 1800); - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - - final result = StartupDependencyCheckResult( - platformLabel: 'Fedora', - issues: const [ - StartupDependencyIssue( - id: 'linux-pipewire-runtime', - title: 'PipeWire runtime is missing', - summary: 'Primary Linux audio needs PipeWire.', - details: ['Install PipeWire and recheck.'], - severity: StartupDependencySeverity.recommended, - installHints: [ - StartupInstallHint( - label: 'Release downloads', - command: 'https://github.com/microsoft/onnxruntime/releases', - ), - StartupInstallHint( - label: 'Fedora', - command: 'sudo dnf install pipewire-libs', - ), - ], - ), - ], - ); - final loggedResults = []; - - await tester.pumpWidget( - MaterialApp( - home: StartupDependencyGate( - checker: () async => result, - logger: (value) async { - loggedResults.add(value); - }, - child: const Scaffold(body: Text('ready')), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.text('Finish Linux setup'), findsOneWidget); - expect(find.text('PipeWire runtime is missing'), findsOneWidget); - expect(find.text('Continue anyway'), findsOneWidget); - expect(find.text('Open'), findsOneWidget); - expect(find.text('Copy'), findsOneWidget); - expect(loggedResults, [result]); - - await tester.ensureVisible(find.text('Continue anyway')); - await tester.tap(find.text('Continue anyway')); - await tester.pumpAndSettle(); - - expect(find.text('ready'), findsOneWidget); - }); -} diff --git a/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart b/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart index c3d0493..9922be8 100644 --- a/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart +++ b/apps/chanora_flutter/test/widgets/voice_settings_controls_test.dart @@ -24,29 +24,12 @@ void main() { ]); }); - test('desktop VAD segments expose both Silero and WebRTC', () { + test('desktop VAD segments expose Silero as the primary backend', () { expect(desktopVadBackendSegments.map((s) => s.value), [ - rust.BridgeVadBackend.webrtcVad, rust.BridgeVadBackend.sileroOnnx, ]); }); - test('desktop VAD segments disable Silero when ONNX Runtime is missing', () { - final segments = vadBackendSegmentsForAvailability( - desktop: true, - onnxRuntimeAvailable: false, - ); - - final silero = segments.singleWhere( - (segment) => segment.value == rust.BridgeVadBackend.sileroOnnx, - ); - final webrtc = segments.singleWhere( - (segment) => segment.value == rust.BridgeVadBackend.webrtcVad, - ); - expect(silero.enabled, isFalse); - expect(webrtc.enabled, isTrue); - }); - testWidgets('shared segmented style applies compact visual density', ( tester, ) async { diff --git a/core/chanora_core/Cargo.toml b/core/chanora_core/Cargo.toml index 57b0a22..5827cd0 100644 --- a/core/chanora_core/Cargo.toml +++ b/core/chanora_core/Cargo.toml @@ -15,6 +15,7 @@ chanora_state = { path = "../../crates/chanora_state" } chanora_audio = { path = "../../crates/chanora_audio" } chanora_storage = { path = "../../crates/chanora_storage" } chanora_diagnostics = { path = "../../crates/chanora_diagnostics" } +chanora_server_prefetch = { path = "../../crates/chanora_server_prefetch" } thiserror.workspace = true tracing.workspace = true tokio = { version = "1", features = ["sync", "rt", "macros"] } @@ -22,5 +23,6 @@ tokio = { version = "1", features = ["sync", "rt", "macros"] } [dev-dependencies] # Used by integration tests to inspect the bookmark DB row layout # without going through the public repository API. +chanora_server_prefetch = { path = "../../crates/chanora_server_prefetch", features = ["test-support"] } rusqlite = { version = "0.32", features = ["bundled"] } tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } diff --git a/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index 696cea5..ee742aa 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -46,6 +46,7 @@ use tokio::sync::{broadcast, oneshot, watch, Mutex}; use tokio::task::JoinHandle; use tracing::{info, warn}; +use chanora_server_prefetch::ServerPrefetcher; use chanora_state::channel_join::{ self, AuthoritativeSource, ChannelId as JoinChannelId, ChannelJoinEvent, ChannelJoinState, ConnectionEpoch, JoinFailureKind, @@ -64,8 +65,8 @@ pub use chanora_diagnostics::{ RedactingLogLayer, Redactor, DEFAULT_LOG_CAPACITY, }; pub use chanora_protocol::{ - ChannelInfo, ChatMessage, ClientInfo, ConnectConfig, DisconnectReason, MessageTarget, - ProtocolError, ServerActivity, ServerSnapshot, + ChannelInfo, ChatMessage, ClientInfo, ClientProfile, ConnectConfig, DisconnectReason, + MessageTarget, ProtocolError, ServerActivity, ServerSnapshot, }; pub use chanora_storage::{Bookmark, BookmarkRepository, IdentityFileStore}; @@ -431,6 +432,9 @@ pub struct ChanoraSession { /// extension). Lives alongside the identity file. Wired by /// [`Self::init_storage`]. bookmark_store: Arc>>, + /// Invisible server-address prefetch cache. Warmed by Flutter typing + /// but validated by Rust before Connect can reuse it. + server_prefetch: ServerPrefetcher, /// Single transmit-mode selector for the whole session /// lifetime (SAD-083). Re-wired to a fresh /// [`AudioTransmitGate`] every time the audio engine starts; @@ -483,6 +487,7 @@ impl ChanoraSession { network_tx, identity_store: Arc::new(Mutex::new(None)), bookmark_store: Arc::new(Mutex::new(None)), + server_prefetch: ServerPrefetcher::new(), voice_selector: selector, release_tail, pending_binding: Arc::new(Mutex::new(None)), @@ -628,6 +633,38 @@ impl ChanoraSession { self.events_tx.subscribe() } + /// Warm server-address resolution for a later user-initiated connect. + pub async fn prefetch_server(&self, host: String) -> Result<(), CoreError> { + let host = host.trim().to_lowercase(); + if let Err(err) = self.server_prefetch.prefetch(host.clone()).await { + warn!( + target: "chanora_core", + host = %host, + error = %err, + "server prefetch setup failed" + ); + } + Ok(()) + } + + async fn apply_prefetched_resolution(&self, cfg: &mut ConnectConfig) { + cfg.resolved_address = None; + let host = cfg.address.trim(); + if host.is_empty() { + return; + } + if let Some(addr) = self.server_prefetch.fresh_match(host).await { + cfg.resolved_address = Some(addr); + } + } + + async fn prepare_connect_configs(&self, cfg: ConnectConfig) -> (ConnectConfig, ConnectConfig) { + let stored_cfg = connect_config_without_prefetched_resolution(&cfg); + let mut dial_cfg = stored_cfg.clone(); + self.apply_prefetched_resolution(&mut dial_cfg).await; + (stored_cfg, dial_cfg) + } + /// Connect to a server. Fails with [`CoreError::AlreadyConnected`] /// if a connection is already active (DEC-006). Audio is not /// started automatically; call [`Self::start_audio`] after. @@ -679,7 +716,9 @@ impl ChanoraSession { } } - let client = chanora_protocol::ProtocolClient::connect(cfg.clone()).await?; + let (cfg, dial_cfg) = self.prepare_connect_configs(cfg).await; + + let client = chanora_protocol::ProtocolClient::connect(dial_cfg).await?; let snap = client.snapshot().await?; let epoch = self.allocate_connection_epoch().await; let mut join_state = ChannelJoinState::new(epoch); @@ -896,6 +935,13 @@ impl ChanoraSession { Ok(snap) } + /// Fetch richer profile and live connection details for one online client. + pub async fn client_profile(&self, client_id: u64) -> Result { + let guard = self.inner.lock().await; + let state = guard.as_ref().ok_or(CoreError::NotConnected)?; + Ok(state.protocol.client_profile(client_id).await?) + } + /// True if a connection is currently active. pub async fn is_connected(&self) -> bool { self.inner.lock().await.is_some() @@ -928,6 +974,12 @@ impl ChanoraSession { rec.drain() } + /// Snapshot recorded protocol events without clearing them. + pub async fn protocol_events_snapshot(&self) -> Vec { + let rec = self.event_recorder.lock().await; + rec.snapshot() + } + /// Record a platform lifecycle event (SRS-138). pub async fn record_lifecycle_event(&self, state: &str) { let mut rec = self.event_recorder.lock().await; @@ -1234,11 +1286,11 @@ impl ChanoraSession { audio.set_output_muted(muted); } } - // Muting the speaker is treated as a local deafen: it must - // also clamp microphone TX so users cannot keep talking while - // unable to hear replies. This does not send server-side - // input-mute; it only drives the local transmit gate. - let mic_disabled = state.local_input_muted || state.local_output_muted; + // Input mute must also stop local outbound voice production + // so the transmit selector stays in sync with the server-side + // mic mute. Output mute is playback-only and must not affect + // the mic gate. + let mic_disabled = state.local_input_muted; self.voice_selector.set_hard_mute(mic_disabled); Ok(()) } @@ -1323,6 +1375,19 @@ impl ChanoraSession { Ok(audio.audio_processing_stats()) } + /// Best-effort audio-processing diagnostics for user exports. + /// + /// This intentionally never waits for the connection mutex. During + /// server join the connect path owns that mutex while awaiting network + /// I/O; diagnostics must stay responsive and omit live audio stats + /// rather than blocking the UI thread. + pub fn audio_processing_stats_if_ready(&self) -> Option { + let guard = self.inner.try_lock().ok()?; + let state = guard.as_ref()?; + let audio = state.audio.as_ref()?; + Some(audio.audio_processing_stats()) + } + /// Configure the preferred Silero ONNX VAD model path. /// /// This does not require an active connection. Running iOS audio @@ -2367,6 +2432,12 @@ fn map_join_error_code(code: channel_join::JoinErrorCode) -> VoiceJoinErrorCode } } +fn connect_config_without_prefetched_resolution(cfg: &ConnectConfig) -> ConnectConfig { + let mut cfg = cfg.clone(); + cfg.resolved_address = None; + cfg +} + #[cfg(test)] mod tests { use super::*; @@ -2376,6 +2447,109 @@ mod tests { let _ = ChanoraSession::new(); } + #[test] + fn audio_stats_nonblocking_under_lock() { + let s = ChanoraSession::new(); + let _guard = s.inner.try_lock().unwrap(); + + assert!(s.audio_processing_stats_if_ready().is_none()); + } + + #[test] + fn audio_stats_absent_when_disconnected() { + let s = ChanoraSession::new(); + + assert!(s.audio_processing_stats_if_ready().is_none()); + } + + #[tokio::test] + async fn connect_uses_fresh_prefetch() { + let session = ChanoraSession::new(); + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + + let generation = session.server_prefetch.begin_for_test("example.com").await; + session + .server_prefetch + .store_success_for_test(generation, "example.com", addr, std::time::Instant::now()) + .await; + + let mut cfg = ConnectConfig::default(); + cfg.address = " example.com ".to_string(); + session.apply_prefetched_resolution(&mut cfg).await; + + assert_eq!(cfg.resolved_address, Some(addr)); + } + + #[tokio::test] + async fn connect_clears_untrusted_resolved_address() { + let session = ChanoraSession::new(); + let mut cfg = ConnectConfig::default(); + cfg.address = "example.com".to_string(); + cfg.resolved_address = Some("127.0.0.1:9987".parse().unwrap()); + + session.apply_prefetched_resolution(&mut cfg).await; + + assert_eq!(cfg.resolved_address, None); + } + + #[tokio::test] + async fn prefetch_swallows_setup_errors() { + let session = ChanoraSession::new(); + session + .server_prefetch + .fail_next_prefetch_setup_for_test("resolver unavailable") + .await; + + let result = session + .prefetch_server(" Example.COM ".to_string()) + .await; + + assert!(result.is_ok()); + } + + #[test] + fn sanitize_config_clears_resolved_address() { + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let mut cfg = ConnectConfig::default(); + cfg.address = "example.com".to_string(); + cfg.nickname = "tester".to_string(); + cfg.password = Some("secret".to_string()); + cfg.identity = Some("identity".to_string()); + cfg.ready_timeout = std::time::Duration::from_secs(42); + cfg.resolved_address = Some(addr); + + let cleaned = connect_config_without_prefetched_resolution(&cfg); + + assert_eq!(cleaned.address, cfg.address); + assert_eq!(cleaned.nickname, cfg.nickname); + assert_eq!(cleaned.password, cfg.password); + assert_eq!(cleaned.identity, cfg.identity); + assert_eq!(cleaned.ready_timeout, cfg.ready_timeout); + assert_eq!(cleaned.resolved_address, None); + } + + #[tokio::test] + async fn prepare_configs_sanitizes_stored_keeps_dial() { + let session = ChanoraSession::new(); + let cached: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let untrusted: std::net::SocketAddr = "127.0.0.2:9987".parse().unwrap(); + + let generation = session.server_prefetch.begin_for_test("example.com").await; + session + .server_prefetch + .store_success_for_test(generation, "example.com", cached, std::time::Instant::now()) + .await; + + let mut cfg = ConnectConfig::default(); + cfg.address = "example.com".to_string(); + cfg.resolved_address = Some(untrusted); + + let (stored_cfg, dial_cfg) = session.prepare_connect_configs(cfg).await; + + assert_eq!(stored_cfg.resolved_address, None); + assert_eq!(dial_cfg.resolved_address, Some(cached)); + } + #[tokio::test] async fn disconnect_when_not_connected_is_noop() { let s = ChanoraSession::new(); @@ -2411,7 +2585,6 @@ mod tests { ], clients: vec![ClientInfo { id: chanora_protocol::ClientId(10), - uid: "uid".into(), channel: chanora_protocol::ChannelId(1), name: "u".into(), input_muted: false, diff --git a/core/chanora_core/tests/alpha_smoke.rs b/core/chanora_core/tests/alpha_smoke.rs index 0031667..4110c89 100644 --- a/core/chanora_core/tests/alpha_smoke.rs +++ b/core/chanora_core/tests/alpha_smoke.rs @@ -18,6 +18,7 @@ async fn alpha_smoke() { password: None, identity: None, ready_timeout: Duration::from_secs(15), + resolved_address: None, }; let snap = s.connect(cfg).await.expect("connect"); println!( diff --git a/crates/chanora_audio/Cargo.toml b/crates/chanora_audio/Cargo.toml index 01158c9..a66567c 100644 --- a/crates/chanora_audio/Cargo.toml +++ b/crates/chanora_audio/Cargo.toml @@ -9,10 +9,6 @@ license.workspace = true repository.workspace = true publish.workspace = true -[features] -default = [] -bench-harness = ["dep:criterion", "dep:dhat", "dep:serde_json"] - [dependencies] chanora_protocol = { path = "../chanora_protocol" } thiserror.workspace = true @@ -33,13 +29,10 @@ audiopus = "0.3.0-rc.0" tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] } tokio = { version = "1", features = ["sync", "rt", "macros", "time"] } rustfft = "6.2.0" -criterion = { version = "0.5", optional = true } -dhat = { version = "0.3", optional = true } -serde_json = { version = "1", optional = true } [target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies] -# Desktop audio I/O for Windows capture/playback and Linux fallback metadata. -# Linux voice I/O uses PipeWire first and PulseAudio as fallback; Apple platforms use direct VoiceProcessingIO +# Desktop audio I/O for Windows capture/playback and Linux capture. +# Linux playback uses SDL2; Apple platforms use direct VoiceProcessingIO # AudioUnits via `coreaudio-rs` for the voice path. cpal = "0.17.3" @@ -84,7 +77,7 @@ ndk-context = "0.1" # - get_raw_session_id() for JNI hardware effect binding # - deduplicated macro impls # - PowerSavingOffloaded PerformanceMode variant -oboe = { path = "../../../oboe-rs" } +oboe = { git = "https://github.com/EdisonJwa/oboe-rs", rev = "a14f9b83ecea8c93f5a692f2ee7808445b938c35" } [target.'cfg(target_os = "windows")'.dependencies] # Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices @@ -108,33 +101,34 @@ tokio = { version = "1", features = ["sync", "rt", "macros", "time", "test-util" # Cross-platform recording Layer for the SDD-090 / DEC-027 privacy # invariant integration test (`tests/ptt_privacy.rs`). tracing-subscriber = { version = "0.3", features = ["registry"] } +# SDD-120 §3 — criterion bench harness (realtime_capture / opus_codec / +# resampler). `harness = false` per bench entry below disables the +# default libtest harness so criterion can install its own. +criterion = "0.5" +# SDD-120 §3 item 1 — dhat is used as the global allocator inside +# `benches/realtime_capture.rs` to count post-warmup heap allocations +# on the realtime capture path. Dev-dep only — does NOT affect +# production builds. +dhat = "0.3" +# SDD-120 §5 / §8 — JSON serialization for `emit_baseline` / +# `compare_baseline` binaries that consume criterion's per-bench +# `estimates.json` outputs and emit the SRS-217 baseline schema. +serde_json = "1" + [[bench]] name = "realtime_capture" harness = false path = "benches/realtime_capture.rs" -required-features = ["bench-harness"] [[bench]] name = "opus_codec" harness = false path = "benches/opus_codec.rs" -required-features = ["bench-harness"] [[bench]] name = "resampler" harness = false path = "benches/resampler.rs" -required-features = ["bench-harness"] - -[[example]] -name = "compare_baseline" -path = "examples/compare_baseline.rs" -required-features = ["bench-harness"] - -[[example]] -name = "emit_baseline" -path = "examples/emit_baseline.rs" -required-features = ["bench-harness"] [target.'cfg(target_os = "linux")'.dependencies] # GNOME-on-Wayland Global Push-to-Talk uses the freedesktop @@ -154,8 +148,22 @@ futures-util = { version = "0.3", default-features = false, features = ["std"] } # options. The portal recommends fresh tokens to scope its own # object paths per call. rand = "0.8" -# Linux-native voice I/O. PipeWire is primary on modern desktops; -# PulseAudio is retained as fallback and for older environments. -pipewire = { version = "0.10", features = ["v0_3_44"] } -libpulse-binding = "2.30" -libpulse-simple-binding = "2.29" +# SDL2 audio for Linux. Replaces the cpal playback path on Linux only; +# cpal stays in use for Linux capture and Windows capture/playback. +# Apple platforms use direct VoiceProcessingIO AudioUnits. Rationale: the +# cpal Linux backend opens raw ALSA `default`, which on most Arch +# / Fedora / Debian installs routes through `dmix` + `plug` with +# nearest-neighbour resampling and very small period sizes — the +# combination produces audible crackling/popping. SDL2 on the same +# systems routes through PipeWire's PulseAudio compat bridge (or +# real PulseAudio), both of which carry a high-quality resampler +# and a sensible default period. The upstream tsclientlib audio +# example (`tsclientlib/examples/audio_utils/ts_to_audio.rs`) and +# the official Qint client both use SDL2 in exactly this shape; +# this dep brings Chanora in line with that pattern. +# +# `bundled` is OFF deliberately — we link against the system +# libSDL2.so. Arch ships `sdl2-compat`; Debian/Ubuntu ship +# `libsdl2-2.0-0`; Fedora ships `SDL2`. The chanora-flutter Linux +# build documentation lists this as a runtime dependency. +sdl2 = { version = "0.37", default-features = false } diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index bdc80b5..376ee23 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -5,35 +5,28 @@ //! and stopped before disconnect. It does not retry on device //! change. -use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -#[cfg(target_os = "linux")] -use cpal::traits::{DeviceTrait, HostTrait}; #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") ))] use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") ))] use cpal::{SampleFormat, SizedSample}; #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") ))] use std::collections::hash_map::DefaultHasher; #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") @@ -110,7 +103,6 @@ pub struct AudioDeviceInfo { } #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") @@ -120,7 +112,6 @@ fn desktop_device_id(device: &cpal::Device) -> Option { } #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") @@ -132,7 +123,6 @@ fn short_device_id(id: &str) -> String { } #[cfg(all( - not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") @@ -188,53 +178,29 @@ pub fn list_audio_devices() -> AudioDeviceList { output_devices: Vec::new(), }; let host = cpal::default_host(); - #[cfg(target_os = "linux")] - { - let default_input_name = host.default_input_device().and_then(|device| { - device - .description() - .ok() - .map(|description| description.name().trim().to_owned()) - .filter(|name| !name.is_empty()) - }); - let default_output_name = host.default_output_device().and_then(|device| { - device - .description() - .ok() - .map(|description| description.name().trim().to_owned()) - .filter(|name| !name.is_empty()) - }); - list.input_devices = - crate::linux_pipewire_input::list_input_devices(default_input_name.as_deref()); - list.output_devices = - crate::linux_pipewire_input::list_output_devices(default_output_name.as_deref()); - } - #[cfg(not(target_os = "linux"))] - { - let default_in = host - .default_input_device() - .and_then(|device| desktop_device_id(&device)); - if let Ok(devices) = host.input_devices() { - for d in devices { - if let Some(mut device) = describe_device(&d) { - device.is_default = default_in - .as_ref() - .is_some_and(|default_id| default_id == &device.id); - list.input_devices.push(device); - } + let default_in = host + .default_input_device() + .and_then(|device| desktop_device_id(&device)); + let default_out = host + .default_output_device() + .and_then(|device| desktop_device_id(&device)); + if let Ok(devices) = host.input_devices() { + for d in devices { + if let Some(mut device) = describe_device(&d) { + device.is_default = default_in + .as_ref() + .is_some_and(|default_id| default_id == &device.id); + list.input_devices.push(device); } } - let default_out = host - .default_output_device() - .and_then(|device| desktop_device_id(&device)); - if let Ok(devices) = host.output_devices() { - for d in devices { - if let Some(mut device) = describe_device(&d) { - device.is_default = default_out - .as_ref() - .is_some_and(|default_id| default_id == &device.id); - list.output_devices.push(device); - } + } + if let Ok(devices) = host.output_devices() { + for d in devices { + if let Some(mut device) = describe_device(&d) { + device.is_default = default_out + .as_ref() + .is_some_and(|default_id| default_id == &device.id); + list.output_devices.push(device); } } } @@ -340,23 +306,31 @@ pub struct AudioEngine { audio_processing_config: Arc>, audio_processing_stats: Arc, audio_handler: Arc>>, - client_volume_overrides: Arc>>, #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))] voice_out_tx: mpsc::Sender, #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))] voice_activity_selector: Option>, #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))] mic_gain: f32, - // Streams must be dropped to stop audio. Linux owns native - // PipeWire/PulseAudio threads; Windows owns cpal streams. On iOS both - // sides collapse into a single `IosVoiceUnit` (one VoiceProcessingIO - // AudioUnit hosts mic + speaker). - #[cfg(target_os = "linux")] - _input_stream: Mutex>, - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "ios", target_os = "macos"))] + // Streams must be dropped to stop audio. Both are `!Send` because + // cpal's Stream isn't Send on some backends; we keep them in an + // Option wrapped by Mutex so stop() can move them out. On Linux + // the output side is `crate::sdl_output::SdlOutput` instead of a + // cpal Stream (see the SDD note inside `sdl_output.rs`); the + // same unsafe Send/Sync impl below covers both. On iOS both + // sides collapse into a single `IosVoiceUnit` (one + // VoiceProcessingIO AudioUnit hosts mic + speaker) — cpal is + // unused on iOS for the reasons documented in + // `ios_voice_unit.rs`. + #[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") + ))] _input_stream: Mutex>, #[cfg(target_os = "linux")] - _output_stream: Mutex>, + _output_stream: Mutex>, #[cfg(all( not(target_os = "linux"), not(target_os = "ios"), @@ -483,31 +457,6 @@ fn open_ios_voice_backend( Ok(IosVoiceBackend::Vpio(unit)) } -fn apply_saved_client_volume( - audio_handler: &mut AudioHandler, - client_volume_overrides: &Mutex>, - client_id: SessionAudioId, -) { - let override_volume = match client_volume_overrides.lock() { - Ok(overrides) => overrides.get(&client_id).copied(), - Err(error) => { - tracing::warn!( - target: "chanora_audio", - client_id = client_id.0, - error = %error, - "client volume overrides lock poisoned — saved volume not applied" - ); - None - } - }; - - if let Some(volume) = override_volume { - if let Some(queue) = audio_handler.get_mut_queues().get_mut(&client_id) { - queue.volume = volume; - } - } -} - impl AudioEngine { #[cfg(target_os = "android")] fn spawn_android_backend_event_task( @@ -743,7 +692,7 @@ impl AudioEngine { Self::start_with_gate_cpal(cfg, voice_out_tx, voice_in_rx, transmit_gate) } - /// Non-Apple/non-Android implementation: native Linux voice I/O or cpal on Windows. + /// Non-Apple/non-Android implementation: cpal capture + (cpal | SDL2) output. /// Kept as a separate function so the Apple and Android paths can /// short-circuit at the top of `start_with_gate` without dragging a 200-line /// cfg-gated block. Body is the pre-iOS-port code, unchanged @@ -762,16 +711,7 @@ impl AudioEngine { "starting audio engine: cpal host selected" ); - fn device_name(device: &cpal::Device) -> Option { - device - .description() - .ok() - .map(|description| description.name().trim().to_owned()) - .filter(|name| !name.is_empty()) - } - /// Helper: find a device by stable id, falling back to default. - #[cfg(not(target_os = "linux"))] fn find_device( host: &cpal::Host, default_fn: DefaultFn, @@ -795,30 +735,28 @@ impl AudioEngine { default_fn(host) } - #[cfg(target_os = "linux")] - let in_dev = host.default_input_device(); - #[cfg(not(target_os = "linux"))] let in_dev = find_device( &host, cpal::Host::default_input_device, cpal::Host::input_devices, cfg.input_device_id.as_deref(), - ); + ) + .ok_or(AudioError::NoInputDevice)?; - #[cfg(target_os = "linux")] - let out_dev = host.default_output_device(); - #[cfg(not(target_os = "linux"))] let out_dev = find_device( &host, cpal::Host::default_output_device, cpal::Host::output_devices, cfg.output_device_id.as_deref(), - ); + ) + .ok_or(AudioError::NoOutputDevice)?; - #[cfg(not(target_os = "linux"))] - let in_dev = in_dev.ok_or(AudioError::NoInputDevice)?; - #[cfg(not(target_os = "linux"))] - let out_dev = out_dev.ok_or(AudioError::NoOutputDevice)?; + info!( + target: "chanora_audio", + in_device = %in_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(), + out_device = %out_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(), + "starting audio engine" + ); // Log the cpal-reported default configs *before* trying to // open streams, so a downstream stream-build failure can @@ -830,29 +768,6 @@ impl AudioEngine { // "Start audio button not work"). Promote what would // otherwise be silent or laconic errors into structured // log records the user can paste back. - #[cfg(target_os = "linux")] - if let Some(in_dev) = in_dev.as_ref() { - match in_dev.default_input_config() { - Ok(c) => info!( - target: "chanora_audio", - channels = c.channels(), - sample_rate = c.sample_rate(), - sample_format = ?c.sample_format(), - "default_input_config reported" - ), - Err(e) => warn!( - target: "chanora_audio", - error = %e, - "default_input_config FAILED — capture will be disabled" - ), - } - } else { - warn!( - target: "chanora_audio", - "cpal default_input_device unavailable; Linux PipeWire capture will be tried directly" - ); - } - #[cfg(not(target_os = "linux"))] match in_dev.default_input_config() { Ok(c) => info!( target: "chanora_audio", @@ -867,24 +782,6 @@ impl AudioEngine { "default_input_config FAILED — capture will be disabled" ), } - #[cfg(target_os = "linux")] - if let Some(out_dev) = out_dev.as_ref() { - match out_dev.default_output_config() { - Ok(c) => info!( - target: "chanora_audio", - channels = c.channels(), - sample_rate = c.sample_rate(), - sample_format = ?c.sample_format(), - "default_output_config reported" - ), - Err(e) => warn!( - target: "chanora_audio", - error = %e, - "default_output_config FAILED — output stream will fail to build" - ), - } - } - #[cfg(not(target_os = "linux"))] match out_dev.default_output_config() { Ok(c) => info!( target: "chanora_audio", @@ -900,7 +797,6 @@ impl AudioEngine { ), } - #[cfg(target_os = "windows")] let transmit_flag_for_capture = transmit_gate.flag_arc(); let frames_sent = Arc::new(AtomicU32::new(0)); let frames_received = Arc::new(AtomicU32::new(0)); @@ -908,7 +804,6 @@ impl AudioEngine { let output_muted = Arc::new(AtomicBool::new(false)); let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default())); let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default()); - let client_volume_overrides = Arc::new(Mutex::new(HashMap::new())); // ---------- Capture ---------- // Capture is best-effort. If the platform default input @@ -916,52 +811,24 @@ impl AudioEngine { // headless null sources or for users who deny the mic // permission) we log and continue — playback alone is // still useful. PTT becomes a no-op in that case. - #[cfg(target_os = "linux")] - let capture_result = match crate::linux_pipewire_input::start_capture( - cfg.input_device_id.as_deref(), - in_dev.as_ref().and_then(device_name).as_deref(), - voice_out_tx.clone(), - transmit_gate.clone(), - frames_sent.clone(), - cfg.mic_gain, - ) { - Ok((stream, device_info)) => Ok((Some(stream), true, device_info.name)), - Err(linux_error) => { - warn!( - target: "chanora_audio", - error = %linux_error, - "Linux native capture unavailable" - ); - Err(linux_error) - } - }; - #[cfg(target_os = "windows")] let capture_result = try_open_capture( &in_dev, voice_out_tx, transmit_flag_for_capture, frames_sent.clone(), cfg.mic_gain, - ) - .map(|stream| { - ( - Some(stream), - true, - device_name(&in_dev).unwrap_or_else(|| "cpal capture".to_string()), - ) - }); - let (input_stream, capture_active, capture_device_name) = match capture_result { - Ok((stream, active, device_name)) => (stream, active, device_name), + ); + let (input_stream, capture_active) = match capture_result { + Ok(s) => (Some(s), true), Err(e) => { warn!( target: "chanora_audio", error = %e, "capture stream unavailable; continuing with playback only" ); - (None, false, "".to_string()) + (None, false) } }; - #[cfg(target_os = "windows")] if let Some(s) = &input_stream { s.play() .map_err(|e| AudioError::Backend(format!("input play: {e}")))?; @@ -971,18 +838,21 @@ impl AudioEngine { let audio_handler: Arc>> = Arc::new(Mutex::new(AudioHandler::new())); - // Linux uses native PipeWire/PulseAudio playback. PipeWire is primary; - // PulseAudio is fallback and can also be selected explicitly by id. + // Linux uses SDL2 for output (Qint / upstream tsclientlib + // pattern). cpal's Linux backend opens raw ALSA which routes + // through `dmix`+`plug` and produces audible crackling / + // popping on the 48 kHz → device-rate step. SDL2 on the same + // box routes through PipeWire's PA bridge (or PulseAudio) + // whose resampler is high-quality. We keep cpal on Windows + // and macOS — both have native backends (WASAPI / CoreAudio) + // without this problem. See `crates/chanora_audio/src/sdl_output.rs` + // for the full rationale. #[cfg(target_os = "linux")] - let (output_stream, output_device_info) = crate::linux_pipewire_input::start_output( - cfg.output_device_id.as_deref(), - out_dev.as_ref().and_then(device_name).as_deref(), + let output_stream = crate::sdl_output::SdlOutput::start( audio_handler.clone(), output_gain.clone(), output_muted.clone(), )?; - #[cfg(target_os = "linux")] - let output_device_name = output_device_info.name; #[cfg(not(target_os = "linux"))] let output_stream = { @@ -1057,20 +927,10 @@ impl AudioEngine { .map_err(|e| AudioError::Backend(format!("output play: {e}")))?; stream }; - #[cfg(not(target_os = "linux"))] - let output_device_name = device_name(&out_dev).unwrap_or_default(); - - info!( - target: "chanora_audio", - in_device = %capture_device_name, - out_device = %output_device_name, - "starting audio engine" - ); // ---------- Inbound forwarder ---------- let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); let handler_for_task = audio_handler.clone(); - let client_volume_overrides_for_task = client_volume_overrides.clone(); let frames_received_for_task = frames_received.clone(); tokio::spawn(async move { loop { @@ -1087,11 +947,6 @@ impl AudioEngine { if let Err(e) = h.handle_packet(id, v.packet) { debug!(target: "chanora_audio", error = %e, "decode failed"); } else { - apply_saved_client_volume( - &mut h, - &client_volume_overrides_for_task, - id, - ); frames_received_for_task.fetch_add(1, Ordering::Relaxed); } } @@ -1111,10 +966,6 @@ impl AudioEngine { audio_processing_config, audio_processing_stats, audio_handler, - client_volume_overrides, - #[cfg(target_os = "linux")] - _input_stream: Mutex::new(input_stream), - #[cfg(target_os = "windows")] _input_stream: Mutex::new(input_stream), _output_stream: Mutex::new(Some(output_stream)), shutdown_tx: Some(shutdown_tx), @@ -1140,7 +991,6 @@ impl AudioEngine { let output_muted = Arc::new(AtomicBool::new(false)); let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default())); let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default()); - let client_volume_overrides = Arc::new(Mutex::new(HashMap::new())); let audio_handler: Arc>> = Arc::new(Mutex::new(AudioHandler::new())); @@ -1287,7 +1137,6 @@ impl AudioEngine { let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); let handler_for_task = audio_handler.clone(); - let client_volume_overrides_for_task = client_volume_overrides.clone(); let frames_received_for_task = frames_received.clone(); tokio::spawn(async move { loop { @@ -1304,11 +1153,6 @@ impl AudioEngine { if let Err(e) = h.handle_packet(id, v.packet) { debug!(target: "chanora_audio", error = %e, "decode failed"); } else { - apply_saved_client_volume( - &mut h, - &client_volume_overrides_for_task, - id, - ); frames_received_for_task.fetch_add(1, Ordering::Relaxed); } } @@ -1328,7 +1172,6 @@ impl AudioEngine { audio_processing_config, audio_processing_stats, audio_handler, - client_volume_overrides, voice_out_tx, voice_activity_selector: cfg.voice_activity_selector.clone(), mic_gain: cfg.mic_gain, @@ -1381,7 +1224,6 @@ impl AudioEngine { let output_muted = Arc::new(AtomicBool::new(false)); let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default())); let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default()); - let client_volume_overrides = Arc::new(Mutex::new(HashMap::new())); let audio_handler: Arc>> = Arc::new(Mutex::new(AudioHandler::new())); @@ -1416,7 +1258,6 @@ impl AudioEngine { // waiting. let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); let handler_for_task = audio_handler.clone(); - let client_volume_overrides_for_task = client_volume_overrides.clone(); let frames_received_for_task = frames_received.clone(); tokio::spawn(async move { loop { @@ -1433,11 +1274,6 @@ impl AudioEngine { if let Err(e) = h.handle_packet(id, v.packet) { debug!(target: "chanora_audio", error = %e, "decode failed"); } else { - apply_saved_client_volume( - &mut h, - &client_volume_overrides_for_task, - id, - ); frames_received_for_task.fetch_add(1, Ordering::Relaxed); } } @@ -1457,7 +1293,6 @@ impl AudioEngine { audio_processing_config, audio_processing_stats, audio_handler, - client_volume_overrides, voice_out_tx, voice_activity_selector: cfg.voice_activity_selector.clone(), mic_gain: cfg.mic_gain, @@ -1477,8 +1312,12 @@ impl AudioEngine { // common contract is that dropping the wrapper stops // audio. iOS collapses input + output into one // `IosVoiceUnit` (see `ios_voice_unit.rs`); every other - // platform has separate input + output stream owners. - #[cfg(any(target_os = "linux", target_os = "windows"))] + // platform has separate cpal input + cpal/SDL output. + #[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") + ))] { let _ = self._input_stream.lock().unwrap().take(); let _ = self._output_stream.lock().unwrap().take(); @@ -1774,11 +1613,11 @@ impl AudioEngine { } /// Latest Android voice-audio diagnostics snapshot (SDD-112 item - /// 10 / SDD-113 item 7 / SDD-116 item 3). Returns `Some(...)` + /// 10 / SDD-113 item 7 / SDD-116 item 3). On non-Android targets + /// this always returns `None`. On Android it returns `Some(...)` /// once `AndroidVoiceUnit::open()` has published a snapshot; the - /// slot is cleared on `close()` / `Drop`. Per SDD-090 the snapshot - /// contains only device-side technical scalars — no PII. - #[cfg(target_os = "android")] + /// slot is cleared on `close()` / `Drop`. Per SDD-090 the + /// snapshot contains only device-side technical scalars — no PII. pub fn android_diagnostics( &self, ) -> Option { @@ -1830,25 +1669,6 @@ impl AudioEngine { ); } } - - match self.client_volume_overrides.lock() { - Ok(mut overrides) => { - if (clamped - 1.0).abs() <= f32::EPSILON { - overrides.remove(&SessionAudioId(client_id)); - } else { - overrides.insert(SessionAudioId(client_id), clamped); - } - } - Err(e) => { - tracing::warn!( - target: "chanora_audio", - client_id, - volume = clamped, - error = %e, - "set_client_volume: client volume overrides lock poisoned" - ); - } - } } } @@ -1860,7 +1680,7 @@ impl Drop for AudioEngine { // ---------- Capture pipeline ---------- -#[cfg(target_os = "windows")] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] fn try_open_capture( in_dev: &cpal::Device, voice_out_tx: mpsc::Sender, @@ -1879,8 +1699,9 @@ fn try_open_capture( // jitter of WASAPI shared mode. // * macOS / iOS: CoreAudio picks a HAL-friendly default; // iOS RemoteIO rejects arbitrary buffer-size requests. - // * Linux does not reach this cpal capture path in production; - // it uses native PipeWire/PulseAudio capture instead. + // * Linux: same SDL2-vs-cpal split as the output path; we + // still use cpal for capture but leave Default since + // PipeWire's ALSA shim works well there. let mut in_stream_cfg: cpal::StreamConfig = in_cfg.into(); #[cfg(target_os = "windows")] { @@ -1917,7 +1738,7 @@ fn try_open_capture( } #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] -pub(crate) struct CaptureState { +struct CaptureState { encoder: OpusEncoder, in_sample_rate: u32, in_channels: usize, @@ -1956,7 +1777,7 @@ pub(crate) struct CaptureState { #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl CaptureState { - pub(crate) fn new( + fn new( encoder: OpusEncoder, in_sample_rate: u32, in_channels: usize, @@ -1989,7 +1810,7 @@ impl CaptureState { /// Consume an arbitrary-rate, multichannel cpal buffer; produce /// 48 kHz mono frames; encode and send when `transmit_active` /// is true (PTT engaged). - pub(crate) fn ingest(&mut self, buf: &[T]) { + fn ingest(&mut self, buf: &[T]) { if !self.transmit_active.load(Ordering::Relaxed) { // Drain accumulator while muted so we don't pop on PTT release. self.pcm_accum.clear(); @@ -2138,7 +1959,7 @@ impl CaptureState { /// Per-sample format conversion to f32 in the range [-1.0, 1.0]. #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] -pub(crate) trait ToF32 { +trait ToF32 { fn to_f32_sample(self) -> f32; } #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] @@ -2160,7 +1981,7 @@ impl ToF32 for u16 { } } -#[cfg(target_os = "windows")] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] fn build_input_stream( device: &cpal::Device, config: &cpal::StreamConfig, diff --git a/crates/chanora_audio/src/ios_voice_unit.rs b/crates/chanora_audio/src/ios_voice_unit.rs index 5212566..b36769b 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -25,7 +25,7 @@ //! stream-format requests on bus 0 (output) and bus 1 (input). //! //! This file replaces the cpal capture + playback streams on iOS and macOS. -//! Linux uses native PipeWire/PulseAudio voice I/O; +//! Linux uses SDL2 for output (see `sdl_output.rs`) and cpal for capture; //! Windows uses cpal's WASAPI backend. //! //! ## What VPIO gives us @@ -53,7 +53,7 @@ //! thread that owns the unit. We open the unit on the same thread //! that calls `Self::start` (the tokio worker that runs //! `chanora_core::ChanoraSession::start_audio`, the same pattern -//! the desktop backends use) and never move it. The outer `AudioEngine` +//! cpal + SDL use) and never move it. The outer `AudioEngine` //! already carries an `unsafe impl Send` to satisfy the same //! constraint for cpal's `!Send` Stream type; that impl covers //! VPIO too. @@ -589,16 +589,16 @@ impl IosVoiceUnit { /// captures samples, render callback fires when the hardware /// needs samples to play. /// - /// Parameters mirror the capture + playback inputs the desktop - /// backends accept so engine.rs can swap backends with + /// Parameters mirror the capture + playback inputs the cpal + /// and SDL backends accept so engine.rs can swap backends with /// a `cfg`. /// /// * `handler` — shared AudioHandler the inbound forwarder /// feeds Opus packets into. The output render callback pulls /// decoded f32 frames from it (48 kHz stereo) and downmixes /// to the i16 mono buffer VPIO expects. - /// * `output_gain` / `output_muted` — same atomics the desktop - /// output paths read on every callback so the master + /// * `output_gain` / `output_muted` — same atomics the cpal + /// and SDL output paths read on every callback so the master /// volume + local-mute UI works identically across backends. /// * `voice_out_tx` — channel the capture pipeline sends /// encoded `OutPacket`s on. @@ -734,7 +734,7 @@ impl IosVoiceUnit { // 1. Lock the AudioHandler, ask it to fill a scratch // f32 stereo buffer (length = 2 * num_frames). The // handler runs Opus decode + per-client jitter - // buffer + mix. Same primitive desktop output + // buffer + mix. Same primitive cpal + SDL output // paths use; this is the platform-neutral playback // contract from `tsclientlib::audio::AudioHandler`. // 2. Downmix to i16 mono with master gain. VPIO expects @@ -748,10 +748,12 @@ impl IosVoiceUnit { // AudioHandler in step 1 so its jitter buffer // doesn't grow unbounded while muted. This is the // contract every other backend follows (matches - // Linux PipeWire/PulseAudio and the cpal output stream). + // SdlOutput::callback and the cpal output stream). // // Build the playback pipeline (direct fill_buffer in - // render callback; matches the desktop voice backends). + // render callback; matches tsclientlib's reference SDL + // example at + // tsclientlib/examples/audio_utils/ts_to_audio.rs). // // The earlier ring-buffer attempt (rc.8+73..+74) decoupled // AudioHandler from the render callback via a 50 Hz @@ -764,12 +766,14 @@ impl IosVoiceUnit { // consumer pull being larger, the ring averaged out empty // — fill_buffer was returning silence 65-84% of ticks // because we drained it too aggressively before packets - // arrived. Direct desktop callbacks follow the same pattern. + // arrived. Linux/SDL's same pattern works fine because + // SDL calls fill_buffer at exactly the device callback + // rate. // // Revert to direct call: render callback locks // AudioHandler, asks for `num_frames` stereo frames, and // immediately downmixes to i16 mono into the output - // buffer. Same as desktop output, just stereo-f32 -> mono-i16 + // buffer. Same as Linux/SDL, just stereo-f32 -> mono-i16 // converted at the boundary. let mut scratch_stereo: Vec = Vec::with_capacity(2048); let handler_for_render = params.handler.clone(); diff --git a/crates/chanora_audio/src/lib.rs b/crates/chanora_audio/src/lib.rs index 24bb352..f49c4e4 100644 --- a/crates/chanora_audio/src/lib.rs +++ b/crates/chanora_audio/src/lib.rs @@ -6,7 +6,7 @@ //! ## What's wired in this Beta //! //! * Default-input capture via platform audio backends (Oboe on Android, -//! VoiceProcessingIO on Apple platforms, PipeWire/PulseAudio on Linux, cpal on Windows) +//! VoiceProcessingIO on Apple platforms, cpal/SDL elsewhere) //! * Frame-aligned 20 ms / 48 kHz mono Opus encoding via `audiopus` //! * Forward encoded frames to the protocol crate as `OutPacket`s //! * Inbound voice packets fed to `tsclientlib::audio::AudioHandler` @@ -32,7 +32,6 @@ pub mod audio_processing; pub mod debug_wav; mod engine; pub mod frame; -#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] pub mod mobile_voice_backend; pub mod mode_stack; pub(crate) mod opus_voice; @@ -45,11 +44,10 @@ pub mod transmit_mode; pub mod transmit_selector; pub mod vad; pub mod voice_activity; -#[cfg(any(target_os = "android", target_os = "ios", test))] pub(crate) mod voice_render; #[cfg(target_os = "linux")] -mod linux_pipewire_input; +mod sdl_output; #[cfg(any(target_os = "ios", target_os = "macos"))] mod ios_voice_unit; diff --git a/crates/chanora_audio/src/linux_pipewire_input.rs b/crates/chanora_audio/src/linux_pipewire_input.rs deleted file mode 100644 index e9e921a..0000000 --- a/crates/chanora_audio/src/linux_pipewire_input.rs +++ /dev/null @@ -1,967 +0,0 @@ -use std::cell::{Cell, RefCell}; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use std::sync::{mpsc as std_mpsc, Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; - -use libpulse_binding as pulse; -use libpulse_simple_binding as psimple; -use pipewire as pw; -use pulse::callbacks::ListResult; -use pulse::context::{self, Context}; -use pulse::mainloop::threaded::Mainloop; -use pulse::proplist::Proplist; -use pulse::sample::{Format, Spec}; -use pulse::stream::Direction; -use pw::spa::pod::Pod; -use pw::{properties::properties, spa}; -use tokio::sync::mpsc; -use tracing::{info, warn}; -use tsclientlib::audio::AudioHandler; - -use crate::engine::{AudioDeviceInfo, CaptureState, SessionAudioId}; -use crate::{AudioError, AudioTransmitGate}; -use chanora_protocol::OutPacket; - -const INPUT_ID_PREFIX: &str = "pw-input:"; -const OUTPUT_ID_PREFIX: &str = "pw-output:"; -const PULSE_INPUT_ID_PREFIX: &str = "pulse-input:"; -const PULSE_OUTPUT_ID_PREFIX: &str = "pulse-output:"; -const SAMPLE_RATE: u32 = 48_000; -const CAPTURE_CHANNELS: u32 = 1; -const PLAYBACK_CHANNELS: u32 = 2; -const PLAYBACK_FRAMES: usize = 960; - -#[derive(Clone, Debug)] -struct LinuxDevice { - backend: LinuxBackend, - direction: LinuxDirection, - raw_id: String, - label: String, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum LinuxBackend { - PipeWire, - PulseAudio, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum LinuxDirection { - Input, - Output, -} - -#[derive(Clone, Debug)] -struct PipeWireDevice { - node_id: u32, - label: String, -} - -impl LinuxDevice { - fn to_audio_device_info(&self, default_label: Option<&str>) -> AudioDeviceInfo { - let is_default = default_label.is_some_and(|label| label == self.label); - let (backend, id) = match (self.backend, self.direction) { - (LinuxBackend::PipeWire, LinuxDirection::Input) => ( - "PipeWire", - input_device_id(self.raw_id.parse::().unwrap_or_default()), - ), - (LinuxBackend::PipeWire, LinuxDirection::Output) => ( - "PipeWire", - output_device_id(self.raw_id.parse::().unwrap_or_default()), - ), - (LinuxBackend::PulseAudio, LinuxDirection::Input) => { - ("PulseAudio", pulse_input_device_id(&self.raw_id)) - } - (LinuxBackend::PulseAudio, LinuxDirection::Output) => { - ("PulseAudio", pulse_output_device_id(&self.raw_id)) - } - }; - AudioDeviceInfo { - id, - name: self.label.clone(), - details: format!("{backend} · id={}", self.raw_id), - is_default, - } - } -} - -pub(crate) fn input_device_id(node_id: u32) -> String { - format!("{INPUT_ID_PREFIX}{node_id}") -} - -#[cfg(test)] -pub(crate) fn input_device_node_id_from_id(id: &str) -> Option { - id.strip_prefix(INPUT_ID_PREFIX)?.parse().ok() -} - -pub(crate) fn output_device_id(node_id: u32) -> String { - format!("{OUTPUT_ID_PREFIX}{node_id}") -} - -#[cfg(test)] -pub(crate) fn output_device_node_id_from_id(id: &str) -> Option { - id.strip_prefix(OUTPUT_ID_PREFIX)?.parse().ok() -} - -fn pulse_input_device_id(name: &str) -> String { - format!("{PULSE_INPUT_ID_PREFIX}{name}") -} - -fn pulse_output_device_id(name: &str) -> String { - format!("{PULSE_OUTPUT_ID_PREFIX}{name}") -} - -#[cfg(test)] -fn pulse_input_name_from_id(id: &str) -> Option<&str> { - id.strip_prefix(PULSE_INPUT_ID_PREFIX) -} - -#[cfg(test)] -fn pulse_output_name_from_id(id: &str) -> Option<&str> { - id.strip_prefix(PULSE_OUTPUT_ID_PREFIX) -} - -pub(crate) fn list_input_devices(default_label: Option<&str>) -> Vec { - list_devices(LinuxDirection::Input) - .iter() - .map(|device| device.to_audio_device_info(default_label)) - .collect() -} - -pub(crate) fn list_output_devices(default_label: Option<&str>) -> Vec { - list_devices(LinuxDirection::Output) - .iter() - .map(|device| device.to_audio_device_info(default_label)) - .collect() -} - -pub(crate) fn start_capture( - prefer: Option<&str>, - default_label: Option<&str>, - voice_out_tx: mpsc::Sender, - transmit_gate: AudioTransmitGate, - frames_sent: Arc, - mic_gain: f32, -) -> Result<(LinuxInput, AudioDeviceInfo), AudioError> { - let devices = list_devices(LinuxDirection::Input); - let device = select_device(&devices, prefer, default_label).ok_or_else(|| { - AudioError::Backend("Linux audio did not expose a usable capture source".to_string()) - })?; - let info = device.to_audio_device_info(default_label); - let encoder = crate::opus_voice::new_voip_encoder("pipewire capture")?; - let capture_state = Arc::new(Mutex::new(CaptureState::new( - encoder, - SAMPLE_RATE, - CAPTURE_CHANNELS as usize, - mic_gain, - voice_out_tx, - transmit_gate.flag_arc(), - frames_sent, - ))); - let stream = match device.backend { - LinuxBackend::PipeWire => LinuxInput::PipeWire(PipeWireInput::start( - PipeWireDevice { - node_id: device.raw_id.parse().map_err(|_| { - AudioError::Backend(format!("invalid PipeWire source id `{}`", device.raw_id)) - })?, - label: device.label.clone(), - }, - capture_state, - )?), - LinuxBackend::PulseAudio => LinuxInput::PulseAudio(PulseAudioInput::start( - device.raw_id.clone(), - capture_state, - )?), - }; - Ok((stream, info)) -} - -pub(crate) fn start_output( - prefer: Option<&str>, - default_label: Option<&str>, - handler: Arc>>, - output_gain: Arc, - output_muted: Arc, -) -> Result<(LinuxOutput, AudioDeviceInfo), AudioError> { - let devices = list_devices(LinuxDirection::Output); - let device = select_device(&devices, prefer, default_label).ok_or_else(|| { - AudioError::Backend("Linux audio did not expose a usable playback sink".to_string()) - })?; - let info = device.to_audio_device_info(default_label); - let stream = match device.backend { - LinuxBackend::PipeWire => LinuxOutput::PipeWire(PipeWireOutput::start( - PipeWireDevice { - node_id: device.raw_id.parse().map_err(|_| { - AudioError::Backend(format!("invalid PipeWire sink id `{}`", device.raw_id)) - })?, - label: device.label.clone(), - }, - handler, - output_gain, - output_muted, - )?), - LinuxBackend::PulseAudio => LinuxOutput::PulseAudio(PulseAudioOutput::start( - device.raw_id.clone(), - handler, - output_gain, - output_muted, - )?), - }; - Ok((stream, info)) -} - -fn select_device( - devices: &[LinuxDevice], - prefer: Option<&str>, - default_label: Option<&str>, -) -> Option { - if devices.is_empty() { - return None; - } - if let Some(prefer) = prefer { - if let Some(device) = devices - .iter() - .find(|device| device.to_audio_device_info(default_label).id == prefer) - { - return Some(device.clone()); - } - } - if let Some(default_label) = default_label { - if let Some(device) = devices.iter().find(|device| device.label == default_label) { - return Some(device.clone()); - } - } - devices.first().cloned() -} - -fn list_devices(direction: LinuxDirection) -> Vec { - match list_pipewire_nodes(direction) { - Ok(pipewire) if !pipewire.is_empty() => return pipewire, - Ok(_) => { - warn!(target: "chanora_audio", "PipeWire device enumeration returned no devices; trying PulseAudio fallback") - } - Err(error) => { - warn!(target: "chanora_audio", error = %error, "PipeWire device enumeration failed") - } - } - match list_pulse_devices(direction) { - Ok(pulse) => pulse, - Err(error) => { - warn!(target: "chanora_audio", error = %error, "PulseAudio device enumeration failed"); - Vec::new() - } - } -} - -fn list_pipewire_nodes(direction: LinuxDirection) -> Result, String> { - pw::init(); - let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?; - let context = pw::context::ContextRc::new(&mainloop, None).map_err(|err| err.to_string())?; - let core = context.connect_rc(None).map_err(|err| err.to_string())?; - let registry = core.get_registry().map_err(|err| err.to_string())?; - - let done = Rc::new(Cell::new(false)); - let devices = Rc::new(RefCell::new(Vec::::new())); - let class_prefix = match direction { - LinuxDirection::Input => "Audio/Source", - LinuxDirection::Output => "Audio/Sink", - }; - - let devices_for_registry = devices.clone(); - let _registry_listener = registry - .add_listener_local() - .global(move |global| { - if global.type_ != pw::types::ObjectType::Node { - return; - } - let Some(props) = global.props else { - return; - }; - let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { - return; - }; - if !media_class.starts_with(class_prefix) { - return; - } - let label = props - .get(*pw::keys::NODE_DESCRIPTION) - .or_else(|| props.get(*pw::keys::NODE_NICK)) - .or_else(|| props.get(*pw::keys::NODE_NAME)) - .unwrap_or("Unnamed PipeWire node") - .to_string(); - devices_for_registry.borrow_mut().push(LinuxDevice { - backend: LinuxBackend::PipeWire, - direction, - raw_id: global.id.to_string(), - label, - }); - }) - .register(); - - let pending = core.sync(0).map_err(|err| err.to_string())?; - let done_for_core = done.clone(); - let loop_for_core = mainloop.clone(); - let _core_listener = core - .add_listener_local() - .done(move |id, seq| { - if id == pw::core::PW_ID_CORE && seq == pending { - done_for_core.set(true); - loop_for_core.quit(); - } - }) - .error(move |_id, _seq, _res, message| { - warn!( - target: "chanora_audio", - message, - "PipeWire core error during input enumeration" - ); - }) - .register(); - - while !done.get() { - mainloop.run(); - } - - let mut devices = devices.borrow().clone(); - devices.sort_by(|left, right| { - left.label - .cmp(&right.label) - .then_with(|| left.raw_id.cmp(&right.raw_id)) - }); - Ok(devices) -} - -fn list_pulse_devices(direction: LinuxDirection) -> Result, String> { - with_pulse_context(|context, mainloop| { - let done = Rc::new(Cell::new(false)); - let devices = Rc::new(RefCell::new(Vec::::new())); - let done_for_cb = done.clone(); - let devices_for_cb = devices.clone(); - let mainloop_for_cb = mainloop.clone(); - let introspector = context.borrow().introspect(); - match direction { - LinuxDirection::Input => { - let _operation = introspector.get_source_info_list(move |result| match result { - ListResult::Item(info) => { - if info.monitor_of_sink.is_some() { - return; - } - let Some(name) = info.name.as_ref() else { - return; - }; - let label = info - .description - .as_ref() - .map(|value| value.to_string()) - .unwrap_or_else(|| name.to_string()); - devices_for_cb.borrow_mut().push(LinuxDevice { - backend: LinuxBackend::PulseAudio, - direction, - raw_id: name.to_string(), - label, - }); - } - ListResult::End | ListResult::Error => { - done_for_cb.set(true); - unsafe { (*mainloop_for_cb.as_ptr()).signal(false) }; - } - }); - while !done.get() { - mainloop.borrow_mut().wait(); - } - } - LinuxDirection::Output => { - let _operation = introspector.get_sink_info_list(move |result| match result { - ListResult::Item(info) => { - let Some(name) = info.name.as_ref() else { - return; - }; - let label = info - .description - .as_ref() - .map(|value| value.to_string()) - .unwrap_or_else(|| name.to_string()); - devices_for_cb.borrow_mut().push(LinuxDevice { - backend: LinuxBackend::PulseAudio, - direction, - raw_id: name.to_string(), - label, - }); - } - ListResult::End | ListResult::Error => { - done_for_cb.set(true); - unsafe { (*mainloop_for_cb.as_ptr()).signal(false) }; - } - }); - while !done.get() { - mainloop.borrow_mut().wait(); - } - } - } - let mut devices = devices.borrow().clone(); - devices.sort_by(|left, right| { - left.label - .cmp(&right.label) - .then_with(|| left.raw_id.cmp(&right.raw_id)) - }); - Ok(devices) - }) -} - -fn with_pulse_context(operation: F) -> Result -where - F: FnOnce(&Rc>, &Rc>) -> Result, -{ - let proplist = build_pulse_proplist()?; - let mainloop = Rc::new(RefCell::new( - Mainloop::new().ok_or("failed to create PulseAudio mainloop")?, - )); - let context = Rc::new(RefCell::new( - Context::new_with_proplist(&*mainloop.borrow(), "chanora", &proplist) - .ok_or("failed to create PulseAudio context")?, - )); - let mainloop_for_cb = mainloop.clone(); - let context_for_cb = context.clone(); - context - .borrow_mut() - .set_state_callback(Some(Box::new(move || { - let state = unsafe { (*context_for_cb.as_ptr()).get_state() }; - match state { - context::State::Ready | context::State::Failed | context::State::Terminated => { - unsafe { (*mainloop_for_cb.as_ptr()).signal(false) }; - } - _ => {} - } - }))); - context - .borrow_mut() - .connect(None, context::FlagSet::NOFLAGS, None) - .map_err(pa_error)?; - mainloop.borrow_mut().lock(); - mainloop.borrow_mut().start().map_err(pa_error)?; - loop { - match context.borrow().get_state() { - context::State::Ready => break, - context::State::Failed | context::State::Terminated => { - mainloop.borrow_mut().unlock(); - mainloop.borrow_mut().stop(); - return Err(format!( - "PulseAudio context state {:?}", - context.borrow().get_state() - )); - } - _ => mainloop.borrow_mut().wait(), - } - } - context.borrow_mut().set_state_callback(None); - let result = operation(&context, &mainloop); - mainloop.borrow_mut().unlock(); - mainloop.borrow_mut().stop(); - result -} - -fn build_pulse_proplist() -> Result { - let mut proplist = Proplist::new().ok_or("failed to create PulseAudio proplist")?; - proplist - .set_str(pulse::proplist::properties::APPLICATION_NAME, "chanora") - .map_err(|_| "failed to set PulseAudio application name".to_string())?; - Ok(proplist) -} - -fn pulse_capture_spec() -> Spec { - Spec { - format: Format::F32le, - channels: CAPTURE_CHANNELS as u8, - rate: SAMPLE_RATE, - } -} - -fn pulse_playback_spec() -> Spec { - Spec { - format: Format::F32le, - channels: PLAYBACK_CHANNELS as u8, - rate: SAMPLE_RATE, - } -} - -fn pa_error(err: pulse::error::PAErr) -> String { - err.to_string() - .unwrap_or_else(|| format!("PulseAudio error {err:?}")) -} - -fn wait_for_startup( - ready_rx: std_mpsc::Receiver>, - handle: JoinHandle>, - label: &str, -) -> Result>, AudioError> { - match ready_rx.recv() { - Ok(Ok(())) => Ok(handle), - Ok(Err(err)) => { - let _ = handle.join(); - Err(AudioError::Backend(err)) - } - Err(err) => { - let _ = handle.join(); - Err(AudioError::Backend(format!( - "{label} startup failed: {err}" - ))) - } - } -} - -pub(crate) enum LinuxInput { - #[allow(dead_code)] - PipeWire(PipeWireInput), - #[allow(dead_code)] - PulseAudio(PulseAudioInput), -} - -pub(crate) enum LinuxOutput { - #[allow(dead_code)] - PipeWire(PipeWireOutput), - #[allow(dead_code)] - PulseAudio(PulseAudioOutput), -} - -pub(crate) struct PipeWireInput { - device: PipeWireDevice, - stop: Arc, - handle: Option>>, -} - -impl PipeWireInput { - fn start( - device: PipeWireDevice, - capture_state: Arc>, - ) -> Result { - let stop = Arc::new(AtomicBool::new(false)); - let (ready_tx, ready_rx) = std_mpsc::sync_channel::>(1); - let stop_for_thread = stop.clone(); - let device_for_thread = device.clone(); - let handle = thread::spawn(move || { - pw::init(); - let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?; - let context = - pw::context::ContextRc::new(&mainloop, None).map_err(|err| err.to_string())?; - let core = context.connect_rc(None).map_err(|err| err.to_string())?; - - let mut props = properties! { - *pw::keys::MEDIA_TYPE => "Audio", - *pw::keys::MEDIA_CATEGORY => "Capture", - *pw::keys::MEDIA_ROLE => "Communication", - }; - let target = device_for_thread.node_id.to_string(); - props.insert(*pw::keys::TARGET_OBJECT, target.as_str()); - - let stream = pw::stream::StreamBox::new(&core, "chanora-pipewire-capture", props) - .map_err(|err| err.to_string())?; - - let _listener = stream - .add_local_listener_with_user_data(capture_state) - .process(|stream, state| { - if let Some(mut buffer) = stream.dequeue_buffer() { - let datas = buffer.datas_mut(); - if datas.is_empty() { - return; - } - let data_ref = &mut datas[0]; - let byte_count = data_ref.chunk().size() as usize; - let Some(bytes) = data_ref.data() else { - return; - }; - let byte_count = byte_count.min(bytes.len()); - if byte_count < 4 { - return; - } - let mut scratch = Vec::with_capacity(byte_count / 4); - for chunk in bytes[..byte_count].chunks_exact(4) { - scratch - .push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); - } - state.lock().unwrap().ingest(&scratch); - } - }) - .register() - .map_err(|err| err.to_string())?; - - let values = build_format_param()?; - let mut params = - [Pod::from_bytes(&values).ok_or("invalid PipeWire capture format pod")?]; - - stream - .connect( - spa::utils::Direction::Input, - None, - pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::MAP_BUFFERS - | pw::stream::StreamFlags::RT_PROCESS, - &mut params, - ) - .map_err(|err| err.to_string())?; - - let _ = ready_tx.send(Ok(())); - while !stop_for_thread.load(Ordering::SeqCst) { - let _ = mainloop - .loop_() - .iterate(pw::loop_::Timeout::Finite(Duration::from_millis(100))); - } - mainloop.quit(); - Ok(()) - }); - - let handle = wait_for_startup(ready_rx, handle, "PipeWire capture")?; - info!(target: "chanora_audio", device = %device.label, node_id = device.node_id, "PipeWire capture backend initialised"); - Ok(Self { - device, - stop, - handle: Some(handle), - }) - } -} - -pub(crate) struct PipeWireOutput { - device: PipeWireDevice, - stop: Arc, - handle: Option>>, -} - -impl PipeWireOutput { - fn start( - device: PipeWireDevice, - handler: Arc>>, - output_gain: Arc, - output_muted: Arc, - ) -> Result { - let stop = Arc::new(AtomicBool::new(false)); - let (ready_tx, ready_rx) = std_mpsc::sync_channel::>(1); - let stop_for_thread = stop.clone(); - let device_for_thread = device.clone(); - let handle = thread::spawn(move || { - pw::init(); - let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?; - let context = - pw::context::ContextRc::new(&mainloop, None).map_err(|err| err.to_string())?; - let core = context.connect_rc(None).map_err(|err| err.to_string())?; - let mut props = properties! { - *pw::keys::MEDIA_TYPE => "Audio", - *pw::keys::MEDIA_CATEGORY => "Playback", - *pw::keys::MEDIA_ROLE => "Communication", - }; - let target = device_for_thread.node_id.to_string(); - props.insert(*pw::keys::TARGET_OBJECT, target.as_str()); - let stream = pw::stream::StreamBox::new(&core, "chanora-pipewire-playback", props) - .map_err(|err| err.to_string())?; - let _listener = stream - .add_local_listener_with_user_data((handler, output_gain, output_muted)) - .process(|stream, state| { - if let Some(mut buffer) = stream.dequeue_buffer() { - let datas = buffer.datas_mut(); - if datas.is_empty() { - return; - } - let data_ref = &mut datas[0]; - let byte_len = { - let Some(bytes) = data_ref.data() else { - return; - }; - let samples = bytes.len() / std::mem::size_of::(); - let mut scratch = vec![0.0_f32; samples]; - if let Ok(mut handler) = state.0.lock() { - handler.fill_buffer(&mut scratch); - } - apply_output_controls(&mut scratch, &state.1, &state.2); - for (chunk, sample) in - bytes.chunks_exact_mut(4).zip(scratch.into_iter()) - { - chunk.copy_from_slice(&sample.to_le_bytes()); - } - bytes.len() - }; - let chunk = data_ref.chunk_mut(); - *chunk.offset_mut() = 0; - *chunk.stride_mut() = - (PLAYBACK_CHANNELS * std::mem::size_of::() as u32) as i32; - *chunk.size_mut() = byte_len as u32; - } - }) - .register() - .map_err(|err| err.to_string())?; - let values = build_playback_format_param()?; - let mut params = - [Pod::from_bytes(&values).ok_or("invalid PipeWire playback format pod")?]; - stream - .connect( - spa::utils::Direction::Output, - None, - pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::MAP_BUFFERS - | pw::stream::StreamFlags::RT_PROCESS, - &mut params, - ) - .map_err(|err| err.to_string())?; - let _ = ready_tx.send(Ok(())); - while !stop_for_thread.load(Ordering::SeqCst) { - let _ = mainloop - .loop_() - .iterate(pw::loop_::Timeout::Finite(Duration::from_millis(100))); - } - mainloop.quit(); - Ok(()) - }); - let handle = wait_for_startup(ready_rx, handle, "PipeWire playback")?; - info!(target: "chanora_audio", device = %device.label, node_id = device.node_id, "PipeWire playback backend initialised"); - Ok(Self { - device, - stop, - handle: Some(handle), - }) - } -} - -fn apply_output_controls(samples: &mut [f32], output_gain: &AtomicU32, output_muted: &AtomicBool) { - if output_muted.load(Ordering::Relaxed) { - samples.fill(0.0); - return; - } - let gain = f32::from_bits(output_gain.load(Ordering::Relaxed)); - if (gain - 1.0).abs() > f32::EPSILON { - for sample in samples.iter_mut() { - *sample *= gain; - } - } -} - -pub(crate) struct PulseAudioInput { - device_name: String, - stop: Arc, - handle: Option>>, -} - -impl PulseAudioInput { - fn start( - device_name: String, - capture_state: Arc>, - ) -> Result { - let stop = Arc::new(AtomicBool::new(false)); - let (ready_tx, ready_rx) = std_mpsc::sync_channel::>(1); - let stop_for_thread = stop.clone(); - let device_for_thread = device_name.clone(); - let handle = thread::spawn(move || { - let spec = pulse_capture_spec(); - let stream = psimple::Simple::new( - None, - "chanora", - Direction::Record, - Some(device_for_thread.as_str()), - "chanora-record", - &spec, - None, - None, - ) - .map_err(pa_error)?; - let _ = ready_tx.send(Ok(())); - let mut bytes = - vec![ - 0_u8; - PLAYBACK_FRAMES * CAPTURE_CHANNELS as usize * std::mem::size_of::() - ]; - while !stop_for_thread.load(Ordering::SeqCst) { - stream.read(&mut bytes).map_err(pa_error)?; - let mut scratch = Vec::with_capacity(bytes.len() / 4); - for chunk in bytes.chunks_exact(4) { - scratch.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); - } - capture_state.lock().unwrap().ingest(&scratch); - } - stream.flush().map_err(pa_error)?; - Ok(()) - }); - let handle = wait_for_startup(ready_rx, handle, "PulseAudio capture")?; - info!(target: "chanora_audio", device = %device_name, "PulseAudio capture backend initialised"); - Ok(Self { - device_name, - stop, - handle: Some(handle), - }) - } -} - -pub(crate) struct PulseAudioOutput { - device_name: String, - stop: Arc, - handle: Option>>, -} - -impl PulseAudioOutput { - fn start( - device_name: String, - handler: Arc>>, - output_gain: Arc, - output_muted: Arc, - ) -> Result { - let stop = Arc::new(AtomicBool::new(false)); - let (ready_tx, ready_rx) = std_mpsc::sync_channel::>(1); - let stop_for_thread = stop.clone(); - let device_for_thread = device_name.clone(); - let handle = thread::spawn(move || { - let spec = pulse_playback_spec(); - let stream = psimple::Simple::new( - None, - "chanora", - Direction::Playback, - Some(device_for_thread.as_str()), - "chanora-playback", - &spec, - None, - None, - ) - .map_err(pa_error)?; - let _ = ready_tx.send(Ok(())); - let mut samples = vec![0.0_f32; PLAYBACK_FRAMES * PLAYBACK_CHANNELS as usize]; - let mut bytes = vec![0_u8; samples.len() * std::mem::size_of::()]; - while !stop_for_thread.load(Ordering::SeqCst) { - samples.fill(0.0); - if let Ok(mut handler) = handler.lock() { - handler.fill_buffer(&mut samples); - } - apply_output_controls(&mut samples, &output_gain, &output_muted); - for (chunk, sample) in bytes.chunks_exact_mut(4).zip(samples.iter().copied()) { - chunk.copy_from_slice(&sample.to_le_bytes()); - } - stream.write(&bytes).map_err(pa_error)?; - } - stream.drain().map_err(pa_error)?; - Ok(()) - }); - let handle = wait_for_startup(ready_rx, handle, "PulseAudio playback")?; - info!(target: "chanora_audio", device = %device_name, "PulseAudio playback backend initialised"); - Ok(Self { - device_name, - stop, - handle: Some(handle), - }) - } -} - -impl Drop for PipeWireInput { - fn drop(&mut self) { - self.stop.store(true, Ordering::SeqCst); - if let Some(handle) = self.handle.take() { - if let Err(_panic) = handle.join() { - warn!( - target: "chanora_audio", - device = %self.device.label, - node_id = self.device.node_id, - "PipeWire capture thread panicked during shutdown" - ); - } - } - } -} - -impl Drop for PipeWireOutput { - fn drop(&mut self) { - self.stop.store(true, Ordering::SeqCst); - if let Some(handle) = self.handle.take() { - if handle.join().is_err() { - warn!(target: "chanora_audio", device = %self.device.label, node_id = self.device.node_id, "PipeWire playback thread panicked during shutdown"); - } - } - } -} - -impl Drop for PulseAudioInput { - fn drop(&mut self) { - self.stop.store(true, Ordering::SeqCst); - if let Some(handle) = self.handle.take() { - if handle.join().is_err() { - warn!(target: "chanora_audio", device = %self.device_name, "PulseAudio capture thread panicked during shutdown"); - } - } - } -} - -impl Drop for PulseAudioOutput { - fn drop(&mut self) { - self.stop.store(true, Ordering::SeqCst); - if let Some(handle) = self.handle.take() { - if handle.join().is_err() { - warn!(target: "chanora_audio", device = %self.device_name, "PulseAudio playback thread panicked during shutdown"); - } - } - } -} - -fn build_format_param() -> Result, String> { - let mut audio_info = spa::param::audio::AudioInfoRaw::new(); - audio_info.set_format(spa::param::audio::AudioFormat::F32LE); - audio_info.set_rate(SAMPLE_RATE); - audio_info.set_channels(CAPTURE_CHANNELS); - - build_format_param_for_audio_info(audio_info) -} - -fn build_playback_format_param() -> Result, String> { - let mut audio_info = spa::param::audio::AudioInfoRaw::new(); - audio_info.set_format(spa::param::audio::AudioFormat::F32LE); - audio_info.set_rate(SAMPLE_RATE); - audio_info.set_channels(PLAYBACK_CHANNELS); - - build_format_param_for_audio_info(audio_info) -} - -fn build_format_param_for_audio_info( - audio_info: spa::param::audio::AudioInfoRaw, -) -> Result, String> { - let object = pw::spa::pod::Object { - type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(), - id: pw::spa::param::ParamType::EnumFormat.as_raw(), - properties: audio_info.into(), - }; - - pw::spa::pod::serialize::PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &pw::spa::pod::Value::Object(object), - ) - .map_err(|err| err.to_string()) - .map(|serializer| serializer.0.into_inner()) -} - -#[cfg(test)] -mod tests { - use super::{ - input_device_id, input_device_node_id_from_id, output_device_id, - output_device_node_id_from_id, pulse_input_device_id, pulse_input_name_from_id, - pulse_output_device_id, pulse_output_name_from_id, - }; - - #[test] - fn input_device_id_round_trip() { - let id = input_device_id(43); - assert_eq!(input_device_node_id_from_id(&id), Some(43)); - assert_eq!(input_device_node_id_from_id("legacy-id"), None); - } - - #[test] - fn output_device_id_round_trip() { - let id = output_device_id(44); - assert_eq!(output_device_node_id_from_id(&id), Some(44)); - assert_eq!(output_device_node_id_from_id("legacy-id"), None); - } - - #[test] - fn pulse_device_id_round_trip() { - let input = pulse_input_device_id("alsa_input.pci-0000_00_1f.3"); - let output = pulse_output_device_id("alsa_output.pci-0000_00_1f.3"); - assert_eq!( - pulse_input_name_from_id(&input), - Some("alsa_input.pci-0000_00_1f.3") - ); - assert_eq!( - pulse_output_name_from_id(&output), - Some("alsa_output.pci-0000_00_1f.3") - ); - } -} diff --git a/crates/chanora_audio/src/ptt_backends/linux.rs b/crates/chanora_audio/src/ptt_backends/linux.rs index 1f6a788..f2580e6 100644 --- a/crates/chanora_audio/src/ptt_backends/linux.rs +++ b/crates/chanora_audio/src/ptt_backends/linux.rs @@ -17,13 +17,11 @@ //! * `try_select` runs a synchronous portal-version probe on //! the blocking proxy. Failure → `None` → Focused fallback. //! * `start(gate, binding)` spawns a worker tokio task that -//! owns an async `zbus::Connection` and calls `CreateSession`. -//! If a binding was already persisted, it also calls -//! `BindShortcuts` (sentinel id `"chanora-ptt"`). Otherwise -//! `BindShortcuts` waits for the user's explicit Configure -//! action, which is when the portal opens its system dialog. -//! While the dialog is open the engine continues at L0Focused — -//! the audio path is not blocked. +//! owns an async `zbus::Connection`, calls `CreateSession`, +//! then `BindShortcuts` (sentinel id `"chanora-ptt"`). The +//! portal opens its own system dialog asking the user to +//! pick a key; while the dialog is open the engine continues +//! at L0Focused — the audio path is not blocked. //! * Once the user accepts, the task subscribes to //! `Activated`/`Deactivated` signals scoped to the session //! handle and calls `gate.set(true/false)` accordingly. @@ -107,7 +105,7 @@ fn is_gnome_on_wayland() -> bool { trait BlockingGlobalShortcuts { /// Version property (read on the blocking proxy as a fast /// reachability probe). - #[zbus(property, name = "version")] + #[zbus(property)] fn version(&self) -> zbus::Result; } @@ -221,7 +219,7 @@ struct BackendInner { enum WorkerCmd { /// Rebind: re-issue `BindShortcuts` on the same session. - Bind, + Rebind, /// Stop: close the session and exit. Stop, } @@ -298,10 +296,9 @@ impl DesktopPttBackend for LinuxGnomeWaylandBackend { // the bridge's tokio runtime, so this is satisfied. let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let desc_tx = self.desc_tx.clone(); - let bind_on_start = binding.input_class != PttInputClass::None; let class_hint = binding.input_class; let worker = tokio::spawn(async move { - run_worker(gate, cmd_rx, desc_tx, class_hint, bind_on_start).await; + run_worker(gate, cmd_rx, desc_tx, class_hint).await; }); // Stash command + worker handles. `try_lock` is fine: the // backend isn't yet shared, and `start` is called once at @@ -353,7 +350,7 @@ impl DesktopPttBackend for LinuxGnomeWaylandBackend { // only — the portal decides the actual binding. if let Ok(inner) = self.inner.try_lock() { if let Some(tx) = inner.cmd_tx.as_ref() { - let _ = tx.send(WorkerCmd::Bind); + let _ = tx.send(WorkerCmd::Rebind); return Ok(()); } } @@ -376,7 +373,6 @@ async fn run_worker( mut cmd_rx: mpsc::UnboundedReceiver, desc_tx: watch::Sender, initial_class_hint: PttInputClass, - bind_on_start: bool, ) { // Open an async D-Bus session connection. If this fails we // emit a warning and exit; the descriptor stays at L0Focused @@ -421,13 +417,26 @@ async fn run_worker( "linux ptt: portal session created" ); - if bind_on_start { - run_bind_shortcut(&proxy, &conn, &session_handle, &desc_tx, initial_class_hint).await; - } else { - info!( - target: "chanora_audio", - "linux ptt: portal session ready; waiting for explicit BindShortcuts request" - ); + // Bind the initial shortcut. The portal opens its own dialog. + match bind_shortcut(&proxy, &conn, &session_handle).await { + Ok(class) => publish_bound(&desc_tx, class.unwrap_or(initial_class_hint)), + Err(BindError::Cancelled) => { + warn!( + target: "chanora_audio", + bind_status = "cancelled", + "linux ptt: user cancelled BindShortcuts; descriptor stays at L0Focused" + ); + publish_l0(&desc_tx); + } + Err(BindError::Failed(e)) => { + warn!( + target: "chanora_audio", + bind_status = "failed", + error = %e, + "linux ptt: BindShortcuts failed; descriptor stays at L0Focused" + ); + publish_l0(&desc_tx); + } } // Subscribe to Activated / Deactivated signals scoped to the @@ -459,8 +468,12 @@ async fn run_worker( tokio::select! { cmd = cmd_rx.recv() => { match cmd { - Some(WorkerCmd::Bind) => { - run_bind_shortcut(&proxy, &conn, &session_handle, &desc_tx, initial_class_hint).await; + Some(WorkerCmd::Rebind) => { + match bind_shortcut(&proxy, &conn, &session_handle).await { + Ok(class) => publish_bound(&desc_tx, class.unwrap_or(initial_class_hint)), + Err(BindError::Cancelled) => publish_l0(&desc_tx), + Err(BindError::Failed(_)) => publish_l0(&desc_tx), + } } Some(WorkerCmd::Stop) | None => { // Close the portal session via the @@ -573,35 +586,6 @@ async fn bind_shortcut( Ok(class) } -async fn run_bind_shortcut( - proxy: &GlobalShortcutsProxy<'_>, - conn: &AsyncConnection, - session_handle: &zbus::zvariant::OwnedObjectPath, - desc_tx: &watch::Sender, - class_hint: PttInputClass, -) { - match bind_shortcut(proxy, conn, session_handle).await { - Ok(class) => publish_bound(desc_tx, class.unwrap_or(class_hint)), - Err(BindError::Cancelled) => { - warn!( - target: "chanora_audio", - bind_status = "cancelled", - "linux ptt: user cancelled BindShortcuts; descriptor stays at L0Focused" - ); - publish_l0(desc_tx); - } - Err(BindError::Failed(e)) => { - warn!( - target: "chanora_audio", - bind_status = "failed", - error = %e, - "linux ptt: BindShortcuts failed; descriptor stays at L0Focused" - ); - publish_l0(desc_tx); - } - } -} - /// Wait for the `Response` signal on `request_path`. Returns the /// `results` dict on success (response code 0) or an Error /// otherwise. diff --git a/crates/chanora_audio/src/sdl_output.rs b/crates/chanora_audio/src/sdl_output.rs new file mode 100644 index 0000000..13fa98e --- /dev/null +++ b/crates/chanora_audio/src/sdl_output.rs @@ -0,0 +1,203 @@ +//! Linux output stream via SDL2 (Qint / upstream `tsclientlib` +//! `ts_to_audio` pattern). +//! +//! ## Why SDL2 and not cpal on Linux +//! +//! cpal's Linux backend opens raw ALSA's `default` PCM. On most +//! modern distributions (Arch with `pipewire-alsa`, Fedora 38+, +//! Debian/Ubuntu with PipeWire) that virtual device still goes +//! through ALSA's `dmix` + `plug` layers when bypassing the +//! PulseAudio/PipeWire client. The `plug` layer's default +//! resampler is **nearest-neighbour**, which causes pronounced +//! aliasing on the 48 kHz → 44.1 kHz step that the +//! `tsclientlib::AudioHandler` output requires. Users perceive +//! this as constant crackling and popping. cpal also picks a +//! small default period size (≈256 frames), which leaves no +//! headroom for kernel scheduler jitter and causes additional +//! xruns. +//! +//! SDL2 on the same systems opens the audio device through the +//! SDL audio driver — which prefers PulseAudio when available +//! and falls back to ALSA otherwise. On a PipeWire box the SDL +//! PulseAudio driver lands inside PipeWire's PulseAudio +//! compatibility layer, whose resampler is high quality. SDL +//! also defaults to a buffer ≈ samples-requested, so we get +//! exactly one Opus frame (20 ms) per callback. +//! +//! This file replaces the cpal output path on Linux only. The +//! capture path stays on cpal until we have a reason to swap it +//! (the user reports outbound is currently fine). Windows continues +//! to use cpal's WASAPI backend; Apple platforms use direct +//! VoiceProcessingIO AudioUnits for the voice path. +//! +//! ## Threading & lifecycle +//! +//! `sdl2::AudioDevice` is `!Send + !Sync` — the SDL audio +//! lock is associated with the calling thread. We open the device +//! on the same thread that calls `Self::start_with_gate` (the +//! tokio worker that runs `chanora_core::ChanoraSession::start_audio`) +//! and never move it. The outer `AudioEngine` already carries an +//! `unsafe impl Send` to bypass cpal's identical constraint; we +//! reuse that and stash the SDL device behind a `Mutex>` +//! the same way cpal does. +//! +//! Dropping the `SdlOutput` closes the SDL device cleanly and +//! drops the playback callback — that releases the +//! `Arc>` clone the callback held. + +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::{Arc, Mutex}; + +use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired}; +use sdl2::AudioSubsystem; +use tracing::{info, warn}; +use tsclientlib::audio::AudioHandler; + +use crate::engine::SessionAudioId; +use crate::AudioError; + +/// 20 ms at 48 kHz mono == one Opus frame's worth of samples. +/// Stereo doubles the byte count but the frame-count stays the +/// same. Aligning the callback size to the Opus frame keeps the +/// jitter-buffer / playback handshake tight (no fractional frame +/// reads inside fill_buffer). +const FRAME_SAMPLES: u16 = 960; + +/// SDL output device wrapper. Holds the live `AudioDevice` so its +/// callback keeps firing for the engine's lifetime, plus a +/// reference to the same `AudioHandler` the inbound forwarder +/// pushes into. +pub struct SdlOutput { + // Drop order: device first (stops the callback), then any + // remaining references release naturally. We keep `_subsystem` + // alive because dropping the AudioSubsystem before the device + // would invalidate SDL's internal state. + device: AudioDevice, + _subsystem: AudioSubsystem, +} + +impl SdlOutput { + /// Open the default SDL playback device at the AudioHandler's + /// native format (48 kHz stereo f32) and start the device + /// playing immediately. The callback drains the AudioHandler + /// directly — no user-side resampling. + /// + /// `output_gain` is read on every callback to apply the + /// master-volume slider; `output_muted` zeroes the output (but + /// still drains AudioHandler so its jitter buffer doesn't grow + /// unbounded while muted). These two atomics share the same + /// definitions the cpal path uses, so the same FFI surface + /// (`set_output_gain` / `set_output_muted`) drives both. + pub fn start( + handler: Arc>>, + output_gain: Arc, + output_muted: Arc, + ) -> Result { + let sdl = sdl2::init().map_err(|e| AudioError::Backend(format!("sdl init: {e}")))?; + let subsystem = sdl + .audio() + .map_err(|e| AudioError::Backend(format!("sdl audio subsystem: {e}")))?; + + info!( + target: "chanora_audio", + driver = subsystem.current_audio_driver(), + "sdl audio subsystem initialised" + ); + + let desired = AudioSpecDesired { + freq: Some(48_000), + channels: Some(2), + samples: Some(FRAME_SAMPLES), + }; + + let device = AudioDevice::open_playback(&subsystem, None, &desired, |spec| { + info!( + target: "chanora_audio", + freq = spec.freq, + channels = spec.channels, + samples = spec.samples, + "sdl playback spec accepted" + ); + TsPlaybackCallback { + handler, + output_gain, + output_muted, + } + }) + .map_err(|e| AudioError::Backend(format!("sdl open_playback: {e}")))?; + + // Begin pumping audio frames out. SDL's device starts paused; + // resume() flips it into the playing state. The callback + // will fire repeatedly at ~50 Hz (every 20 ms) thereafter. + device.resume(); + + Ok(Self { + device, + _subsystem: subsystem, + }) + } +} + +impl Drop for SdlOutput { + fn drop(&mut self) { + // AudioDevice::drop closes the device which stops the + // callback. We log so the chanora.log timeline matches + // engine shutdown. + warn!(target: "chanora_audio", "sdl playback device closing"); + } +} + +/// Playback callback invoked by SDL's audio thread. The shape +/// mirrors the upstream `tsclientlib` example's `SdlCallback`: +/// zero the output buffer (so silent regions emit silence rather +/// than stale memory), then ask the `AudioHandler` to fill in +/// whatever decoded frames it has buffered. +struct TsPlaybackCallback { + handler: Arc>>, + output_gain: Arc, + output_muted: Arc, +} + +impl AudioCallback for TsPlaybackCallback { + type Channel = f32; + + fn callback(&mut self, buffer: &mut [f32]) { + // The buffer is interleaved stereo at 48 kHz from SDL. + // Length is FRAME_SAMPLES * 2 (= 1920 f32) per the spec + // we requested. + for sample in buffer.iter_mut() { + *sample = 0.0; + } + // Lock window kept as tight as the upstream example — + // fill_buffer does the actual Opus decode + jitter logic. + // Contention with the inbound forwarder is the same as + // in the cpal path, but the upstream design has shipped + // this way for years. + { + let mut data = self.handler.lock().unwrap(); + let _removed_ids = data.fill_buffer(buffer); + // `_removed_ids` is the list of clients whose stream the + // handler just finished draining. We could publish that + // upward as a "stopped speaking" hint, but the existing + // BridgeEvent::VoiceState already covers that case via + // the bridge layer; ignoring matches upstream behaviour. + } + + // Apply local mute + master gain after fill so the jitter + // buffer still drains while muted (matching the cpal path's + // contract). `output_gain` is encoded as f32 bits inside an + // AtomicU32 — same encoding the cpal path uses. + if self.output_muted.load(Ordering::Relaxed) { + for sample in buffer.iter_mut() { + *sample = 0.0; + } + return; + } + let gain = f32::from_bits(self.output_gain.load(Ordering::Relaxed)); + if (gain - 1.0).abs() > f32::EPSILON { + for sample in buffer.iter_mut() { + *sample *= gain; + } + } + } +} diff --git a/crates/chanora_audio/src/vad/silero_onnx.rs b/crates/chanora_audio/src/vad/silero_onnx.rs index 31f01db..7fe550b 100644 --- a/crates/chanora_audio/src/vad/silero_onnx.rs +++ b/crates/chanora_audio/src/vad/silero_onnx.rs @@ -33,16 +33,12 @@ //! ## Fallback //! //! `try_new` returns `None` when the model file is missing, the ONNX -//! Runtime is unavailable, or the platform cannot load ONNX Runtime. The caller +//! Runtime is unavailable, or the platform is not iOS/macOS. The caller //! falls back to `WebRtcFallbackVad`. use super::{VadOutput, VoiceActivityDetector}; -#[cfg(target_os = "linux")] -use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; -#[cfg(target_os = "linux")] -use std::sync::OnceLock; use std::thread::JoinHandle; /// 16 kHz frame size for Silero VAD v6 (32 ms). @@ -92,7 +88,7 @@ impl SileroOnnxVad { /// Attempt to load the Silero v6 ONNX model from `model_path`. /// /// Returns `None` when the model file is missing, the ONNX Runtime - /// is unavailable, or the platform cannot initialize ONNX Runtime. + /// is unavailable, or the platform does not support ONNX. pub fn try_new(model_path: &str) -> Option { Self::try_new_onnx(model_path) } @@ -109,17 +105,6 @@ impl SileroOnnxVad { return None; } - #[cfg(target_os = "linux")] - if let Err(err) = ensure_linux_onnxruntime_loaded() { - error!( - target: "chanora_audio", - path = model_path, - error = %err, - "SileroOnnxVad: ONNX Runtime is unavailable on Linux; falling back to WebRtcFallbackVad" - ); - return None; - } - let session_result = std::panic::catch_unwind(|| { ort::session::Session::builder().and_then(|mut b| b.commit_from_file(model_path)) }); @@ -316,111 +301,6 @@ impl VoiceActivityDetector for SileroOnnxVad { } } -#[cfg(target_os = "linux")] -fn ensure_linux_onnxruntime_loaded() -> Result<(), String> { - static ORT_INIT: OnceLock> = OnceLock::new(); - - ORT_INIT - .get_or_init(|| { - let dylib_path = linux_onnxruntime_dylib_path(); - match dylib_path.as_ref() { - Some(path) => { - tracing::info!( - target: "chanora_audio", - path = %path.display(), - "SileroOnnxVad: loading Linux ONNX Runtime from discovered shared library" - ); - ort::init_from(path) - .map_err(|e| format!("init_from({}): {e}", path.display()))? - .with_name("chanora_audio") - .commit() - .then_some(()) - .ok_or_else(|| { - format!("commit(init_from {}): environment already initialized", path.display()) - }) - } - None => { - tracing::warn!( - target: "chanora_audio", - "SileroOnnxVad: no explicit Linux ONNX Runtime shared library path found; trying loader default" - ); - ort::init() - .with_name("chanora_audio") - .commit() - .then_some(()) - .ok_or_else(|| "commit(init): environment already initialized".to_string()) - } - } - }) - .clone() -} - -#[cfg(target_os = "linux")] -fn linux_onnxruntime_dylib_path() -> Option { - if let Some(path) = existing_env_file("ORT_DYLIB_PATH") { - return Some(path); - } - - let mut candidates = Vec::new(); - - if let Ok(exe) = std::env::current_exe() { - if let Some(exe_dir) = exe.parent() { - candidates.push(exe_dir.join("lib").join("libonnxruntime.so")); - candidates.push(exe_dir.join("libonnxruntime.so")); - } - } - - if let Ok(cwd) = std::env::current_dir() { - candidates.push(cwd.join("libonnxruntime.so")); - } - - candidates.extend([ - PathBuf::from("/usr/lib/libonnxruntime.so"), - PathBuf::from("/usr/lib64/libonnxruntime.so"), - PathBuf::from("/usr/local/lib/libonnxruntime.so"), - PathBuf::from("/lib/x86_64-linux-gnu/libonnxruntime.so"), - PathBuf::from("/usr/lib/x86_64-linux-gnu/libonnxruntime.so"), - PathBuf::from("/lib/aarch64-linux-gnu/libonnxruntime.so"), - PathBuf::from("/usr/lib/aarch64-linux-gnu/libonnxruntime.so"), - ]); - - for candidate in candidates { - if candidate.is_file() { - return Some(candidate); - } - } - - first_matching_dir_entry("/usr/lib", "libonnxruntime.so") - .or_else(|| first_matching_dir_entry("/usr/lib64", "libonnxruntime.so")) - .or_else(|| first_matching_dir_entry("/usr/local/lib", "libonnxruntime.so")) - .or_else(|| first_matching_dir_entry("/usr/lib/x86_64-linux-gnu", "libonnxruntime.so")) - .or_else(|| first_matching_dir_entry("/usr/lib/aarch64-linux-gnu", "libonnxruntime.so")) -} - -#[cfg(target_os = "linux")] -fn existing_env_file(name: &str) -> Option { - let path = std::env::var_os(name).map(PathBuf::from)?; - path.is_file().then_some(path) -} - -#[cfg(target_os = "linux")] -fn first_matching_dir_entry(dir: &str, prefix: &str) -> Option { - let mut matches: Vec = std::fs::read_dir(dir) - .ok()? - .filter_map(|entry| entry.ok()) - .map(|entry| entry.path()) - .filter(|path| { - path.is_file() - && path - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with(prefix)) - }) - .collect(); - matches.sort(); - matches.into_iter().next() -} - // SAFETY: ONNX Runtime sessions are thread-safe for inference. // State arrays are owned by this struct and accessed only from // the single capture callback thread. @@ -521,8 +401,6 @@ impl Drop for SileroOnnxVadWorker { #[cfg(test)] mod tests { use super::*; - #[cfg(target_os = "linux")] - use std::path::PathBuf; fn make_stub_vad() -> SileroOnnxVad { SileroOnnxVad { @@ -618,17 +496,4 @@ mod tests { &completed_frame[SILERO_FRAME_16K - SILERO_CONTEXT_16K..] ); } - - #[cfg(target_os = "linux")] - #[test] - fn existing_env_file_ignores_missing_paths() { - let var_name = format!("CHANORA_TEST_ORT_{}", std::process::id()); - std::env::remove_var(&var_name); - assert!(existing_env_file(&var_name).is_none()); - - let missing = PathBuf::from("/definitely/missing/libonnxruntime.so"); - std::env::set_var(&var_name, &missing); - assert!(existing_env_file(&var_name).is_none()); - std::env::remove_var(&var_name); - } } diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 29ee1b7..f42ca38 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -355,8 +355,6 @@ pub struct BridgeChannel { pub struct BridgeClient { /// Stable client id. pub id: u64, - /// Stable TeamSpeak unique identifier. - pub uid: String, /// Channel id the client is currently in. pub channel: u64, /// Nickname. @@ -375,6 +373,61 @@ pub struct BridgeClient { pub talk_power_granted: bool, } +/// Rich profile and live connection details for one online client. +#[derive(Debug, Clone)] +pub struct BridgeClientProfile { + /// Stable online client id. + pub id: u64, + /// Channel the client is currently in. + pub channel: u64, + /// Nickname. + pub name: String, + /// TeamSpeak unique id. + pub unique_id: String, + /// Stable TeamSpeak database id, when visible. + pub database_id: Option, + /// ISO country code, when visible. + pub country_code: String, + /// User description, when visible. + pub description: String, + /// Client version string. + pub version: String, + /// Client platform string. + pub platform: String, + /// Account creation time as Unix seconds. + pub created_unix_seconds: Option, + /// Last connection time as Unix seconds. + pub last_connected_unix_seconds: Option, + /// Total historical connections. + pub connections_total: Option, + /// Current online duration in seconds. + pub online_seconds: Option, + /// Current idle time in milliseconds. + pub idle_milliseconds: Option, + /// Current ping in milliseconds. + pub ping_milliseconds: Option, + /// Client address. Empty when permission-gated. + pub client_address: String, + /// Resolved server group names. + pub server_groups: Vec, + /// Resolved channel group name. + pub channel_group: String, + /// TeamSpeak avatar file path suffix. + pub avatar_path: String, + /// Downloaded bytes this month. + pub bytes_downloaded_month: Option, + /// Uploaded bytes this month. + pub bytes_uploaded_month: Option, + /// Downloaded bytes across all time. + pub bytes_downloaded_total: Option, + /// Uploaded bytes across all time. + pub bytes_uploaded_total: Option, + /// Client-to-server total packet loss ratio. + pub packet_loss_client_to_server_total: Option, + /// Server-to-client total packet loss ratio. + pub packet_loss_server_to_client_total: Option, +} + /// Server snapshot as seen by Dart. #[derive(Debug, Clone)] pub struct BridgeSnapshot { @@ -396,6 +449,38 @@ pub struct BridgeSnapshot { pub own_client_id: u64, } +impl From for BridgeClientProfile { + fn from(profile: chanora_core::ClientProfile) -> Self { + Self { + id: profile.id.0, + channel: profile.channel.0, + name: profile.name, + unique_id: profile.unique_id, + database_id: profile.database_id, + country_code: profile.country_code, + description: profile.description, + version: profile.version, + platform: profile.platform, + created_unix_seconds: profile.created_unix_seconds, + last_connected_unix_seconds: profile.last_connected_unix_seconds, + connections_total: profile.connections_total, + online_seconds: profile.online_seconds, + idle_milliseconds: profile.idle_milliseconds, + ping_milliseconds: profile.ping_milliseconds, + client_address: profile.client_address, + server_groups: profile.server_groups, + channel_group: profile.channel_group, + avatar_path: profile.avatar_path, + bytes_downloaded_month: profile.bytes_downloaded_month, + bytes_uploaded_month: profile.bytes_uploaded_month, + bytes_downloaded_total: profile.bytes_downloaded_total, + bytes_uploaded_total: profile.bytes_uploaded_total, + packet_loss_client_to_server_total: profile.packet_loss_client_to_server_total, + packet_loss_server_to_client_total: profile.packet_loss_server_to_client_total, + } + } +} + impl From for BridgeSnapshot { fn from(s: chanora_core::ServerSnapshot) -> Self { Self { @@ -420,7 +505,6 @@ impl From for BridgeSnapshot { .into_iter() .map(|c| BridgeClient { id: c.id.0, - uid: c.uid, channel: c.channel.0, name: c.name, input_muted: c.input_muted, @@ -459,6 +543,7 @@ pub async fn connect( }, identity: None, ready_timeout: Duration::from_secs(15), + resolved_address: None, }; let snap = match runtime() .spawn(async move { session().connect(cfg).await }) @@ -475,6 +560,17 @@ pub async fn connect( Ok(snap.into()) } +/// Warm server address resolution for the active host field. This is +/// intentionally fire-and-forget from the UI perspective: it schedules +/// Rust-side prefetch work and never opens a TS3 session. +pub async fn prefetch_server(host: String) -> Result<(), BridgeError> { + runtime() + .spawn(async move { session().prefetch_server(host).await }) + .await + .map_err(|e| task_join_error("prefetch_server", e))??; + Ok(()) +} + /// Re-fetch a fresh snapshot from the active connection. pub async fn snapshot() -> Result { let snap = runtime() @@ -484,6 +580,15 @@ pub async fn snapshot() -> Result { Ok(snap.into()) } +/// Fetch richer profile and live connection details for one online client. +pub async fn client_profile(client_id: u64) -> Result { + let profile = runtime() + .spawn(async move { session().client_profile(client_id).await }) + .await + .map_err(|e| task_join_error("client_profile", e))??; + Ok(profile.into()) +} + /// Disconnect from the server. No-op if not connected. pub async fn disconnect() -> Result<(), BridgeError> { runtime() @@ -816,9 +921,10 @@ pub async fn set_input_muted(muted: bool) -> Result<(), BridgeError> { Ok(()) } -/// Toggle self output-mute (speaker). Mutes locally, informs the -/// server, and clamps local microphone transmit while speaker mute -/// is active so the user cannot continue talking while deafened. +/// Toggle self output-mute (speaker). Mutes locally *and* informs +/// the server. The server uses this for the channel icon next to +/// the client name; the local mute kicks in immediately even +/// before the server acknowledges. pub async fn set_output_muted(muted: bool) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_self_muted(None, Some(muted)).await }) @@ -1229,7 +1335,8 @@ impl From for BridgeAudioProcessingStats { /// On non-Android targets or before a voice session opens the /// section is omitted. Per SDD-090 every field in that section is /// a device-side technical scalar — no PII admitted. -pub async fn export_diagnostics() -> String { +#[frb(sync)] +pub fn export_diagnostics() -> String { let metadata = vec![ ( "crate_version".to_string(), @@ -1245,15 +1352,12 @@ pub async fn export_diagnostics() -> String { // diagnostics snapshot from the process-global slot published // by AndroidVoiceUnit::open(). Returns None on non-Android and // before any voice session has opened. - #[cfg(target_os = "android")] let android_audio_yaml = chanora_audio::mobile_voice_backend::current_android_audio_diagnostics() .map(|d| d.to_yaml_fragment()); - #[cfg(not(target_os = "android"))] - let android_audio_yaml: Option = None; - let audio_health = session().audio_processing_stats().await.ok(); - let network_info = session().network_diagnostics_summary().await; - let protocol_events = session().drain_protocol_events().await; + let audio_health = session().audio_processing_stats_if_ready(); + let network_info = runtime().block_on(async { session().network_diagnostics_summary().await }); + let protocol_events = runtime().block_on(async { session().protocol_events_snapshot().await }); let android_audio_yaml = android_audio_yaml.map(|mut yaml| { if let Some(stats) = audio_health { yaml.push_str(&format!( diff --git a/crates/chanora_bridge/src/frb_generated.rs b/crates/chanora_bridge/src/frb_generated.rs index 90eca61..b63fc00 100644 --- a/crates/chanora_bridge/src/frb_generated.rs +++ b/crates/chanora_bridge/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -560177922; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 281698435; // Section: executor @@ -186,6 +186,42 @@ fn wire__crate__api__bridge_init_impl( }, ) } +fn wire__crate__api__client_profile_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: "client_profile", + 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_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::BridgeError>( + (move || async move { + let output_ok = crate::api::client_profile(api_client_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__connect_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -370,16 +406,15 @@ fn wire__crate__api__events_stream_impl( ) } fn wire__crate__api__export_diagnostics_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::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { debug_name: "export_diagnostics", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, move || { let message = unsafe { @@ -392,15 +427,10 @@ fn wire__crate__api__export_diagnostics_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); deserializer.end(); - move |context| async move { - transform_result_sse::<_, crate::BridgeError>( - (move || async move { - let output_ok = crate::api::export_diagnostics().await; - Ok(output_ok) - })() - .await, - ) - } + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::export_diagnostics())?; + Ok(output_ok) + })()) }, ) } @@ -880,6 +910,42 @@ fn wire__crate__api__move_to_channel_impl( }, ) } +fn wire__crate__api__prefetch_server_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: "prefetch_server", + 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_host = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::BridgeError>( + (move || async move { + let output_ok = crate::api::prefetch_server(api_host).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__ptt_descriptor_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1946,7 +2012,6 @@ impl SseDecode for crate::api::BridgeClient { // 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_id = ::sse_decode(deserializer); - let mut var_uid = ::sse_decode(deserializer); let mut var_channel = ::sse_decode(deserializer); let mut var_name = ::sse_decode(deserializer); let mut var_inputMuted = ::sse_decode(deserializer); @@ -1957,7 +2022,6 @@ impl SseDecode for crate::api::BridgeClient { let mut var_talkPowerGranted = ::sse_decode(deserializer); return crate::api::BridgeClient { id: var_id, - uid: var_uid, channel: var_channel, name: var_name, input_muted: var_inputMuted, @@ -1970,6 +2034,64 @@ impl SseDecode for crate::api::BridgeClient { } } +impl SseDecode for crate::api::BridgeClientProfile { + // 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_id = ::sse_decode(deserializer); + let mut var_channel = ::sse_decode(deserializer); + let mut var_name = ::sse_decode(deserializer); + let mut var_uniqueId = ::sse_decode(deserializer); + let mut var_databaseId = >::sse_decode(deserializer); + let mut var_countryCode = ::sse_decode(deserializer); + let mut var_description = ::sse_decode(deserializer); + let mut var_version = ::sse_decode(deserializer); + let mut var_platform = ::sse_decode(deserializer); + let mut var_createdUnixSeconds = >::sse_decode(deserializer); + let mut var_lastConnectedUnixSeconds = >::sse_decode(deserializer); + let mut var_connectionsTotal = >::sse_decode(deserializer); + let mut var_onlineSeconds = >::sse_decode(deserializer); + let mut var_idleMilliseconds = >::sse_decode(deserializer); + let mut var_pingMilliseconds = >::sse_decode(deserializer); + let mut var_clientAddress = ::sse_decode(deserializer); + let mut var_serverGroups = >::sse_decode(deserializer); + let mut var_channelGroup = ::sse_decode(deserializer); + let mut var_avatarPath = ::sse_decode(deserializer); + let mut var_bytesDownloadedMonth = >::sse_decode(deserializer); + let mut var_bytesUploadedMonth = >::sse_decode(deserializer); + let mut var_bytesDownloadedTotal = >::sse_decode(deserializer); + let mut var_bytesUploadedTotal = >::sse_decode(deserializer); + let mut var_packetLossClientToServerTotal = >::sse_decode(deserializer); + let mut var_packetLossServerToClientTotal = >::sse_decode(deserializer); + return crate::api::BridgeClientProfile { + id: var_id, + channel: var_channel, + name: var_name, + unique_id: var_uniqueId, + database_id: var_databaseId, + country_code: var_countryCode, + description: var_description, + version: var_version, + platform: var_platform, + created_unix_seconds: var_createdUnixSeconds, + last_connected_unix_seconds: var_lastConnectedUnixSeconds, + connections_total: var_connectionsTotal, + online_seconds: var_onlineSeconds, + idle_milliseconds: var_idleMilliseconds, + ping_milliseconds: var_pingMilliseconds, + client_address: var_clientAddress, + server_groups: var_serverGroups, + channel_group: var_channelGroup, + avatar_path: var_avatarPath, + bytes_downloaded_month: var_bytesDownloadedMonth, + bytes_uploaded_month: var_bytesUploadedMonth, + bytes_downloaded_total: var_bytesDownloadedTotal, + bytes_uploaded_total: var_bytesUploadedTotal, + packet_loss_client_to_server_total: var_packetLossClientToServerTotal, + packet_loss_server_to_client_total: var_packetLossServerToClientTotal, + }; + } +} + impl SseDecode for crate::api::BridgeEffectOwner { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -2351,6 +2473,18 @@ impl SseDecode for i64 { } } +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::with_capacity(len_ as usize); + 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 { @@ -2435,6 +2569,17 @@ impl SseDecode for Option { } } +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 { @@ -2446,6 +2591,17 @@ impl SseDecode for Option { } } +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 { @@ -2510,43 +2666,44 @@ fn pde_ffi_dispatcher_primary_impl( 2 => wire__crate__api__audio_processing_stats_impl(port, ptr, rust_vec_len, data_len), 3 => wire__crate__api__audio_stats_impl(port, ptr, rust_vec_len, data_len), 4 => wire__crate__api__bridge_init_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__connect_impl(port, ptr, rust_vec_len, data_len), - 6 => wire__crate__api__delete_bookmark_impl(port, ptr, rust_vec_len, data_len), - 7 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len), - 8 => wire__crate__api__enable_audio_debug_wav_dump_impl(port, ptr, rust_vec_len, data_len), - 9 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len), - 10 => wire__crate__api__export_diagnostics_impl(port, ptr, rust_vec_len, data_len), - 11 => wire__crate__api__get_audio_processing_config_impl(port, ptr, rust_vec_len, data_len), - 12 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len), - 13 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), - 14 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len), - 19 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len), - 20 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len), - 21 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len), - 22 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len), - 24 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len), - 25 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len), - 27 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len), - 29 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len), - 30 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len), - 31 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len), - 32 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len), - 33 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len), - 34 => { + 5 => wire__crate__api__client_profile_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__connect_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__delete_bookmark_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__enable_audio_debug_wav_dump_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len), + 12 => wire__crate__api__get_audio_processing_config_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), + 15 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len), + 20 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len), + 21 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len), + 22 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len), + 23 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len), + 27 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len), + 29 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len), + 31 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len), + 33 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len), + 34 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len), + 36 => { wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len) } - 36 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len), - 37 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len), - 38 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len), - 39 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len), - 40 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len), - 41 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), - 42 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len), - 43 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len), - 44 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), - 45 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len), - 47 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len), + 38 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len), + 39 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len), + 40 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len), + 41 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len), + 42 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len), + 43 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), + 44 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len), + 45 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), + 47 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -2559,18 +2716,19 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 15 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len), - 16 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len), - 17 => wire__crate__api__handle_media_services_reset_with_route_impl( + 11 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len), + 16 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len), + 17 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len), + 18 => wire__crate__api__handle_media_services_reset_with_route_impl( ptr, rust_vec_len, data_len, ), - 18 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len), - 23 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len), - 26 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len), - 28 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len), - 35 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len), + 19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len), + 24 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len), + 28 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len), + 30 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len), + 37 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -2804,7 +2962,6 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeClient { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ self.id.into_into_dart().into_dart(), - self.uid.into_into_dart().into_dart(), self.channel.into_into_dart().into_dart(), self.name.into_into_dart().into_dart(), self.input_muted.into_into_dart().into_dart(), @@ -2824,6 +2981,56 @@ impl flutter_rust_bridge::IntoIntoDart for crate::api: } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgeClientProfile { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.channel.into_into_dart().into_dart(), + self.name.into_into_dart().into_dart(), + self.unique_id.into_into_dart().into_dart(), + self.database_id.into_into_dart().into_dart(), + self.country_code.into_into_dart().into_dart(), + self.description.into_into_dart().into_dart(), + self.version.into_into_dart().into_dart(), + self.platform.into_into_dart().into_dart(), + self.created_unix_seconds.into_into_dart().into_dart(), + self.last_connected_unix_seconds + .into_into_dart() + .into_dart(), + self.connections_total.into_into_dart().into_dart(), + self.online_seconds.into_into_dart().into_dart(), + self.idle_milliseconds.into_into_dart().into_dart(), + self.ping_milliseconds.into_into_dart().into_dart(), + self.client_address.into_into_dart().into_dart(), + self.server_groups.into_into_dart().into_dart(), + self.channel_group.into_into_dart().into_dart(), + self.avatar_path.into_into_dart().into_dart(), + self.bytes_downloaded_month.into_into_dart().into_dart(), + self.bytes_uploaded_month.into_into_dart().into_dart(), + self.bytes_downloaded_total.into_into_dart().into_dart(), + self.bytes_uploaded_total.into_into_dart().into_dart(), + self.packet_loss_client_to_server_total + .into_into_dart() + .into_dart(), + self.packet_loss_server_to_client_total + .into_into_dart() + .into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::BridgeClientProfile +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::BridgeClientProfile +{ + fn into_into_dart(self) -> crate::api::BridgeClientProfile { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::BridgeEffectOwner { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -3443,7 +3650,6 @@ impl SseEncode for crate::api::BridgeClient { // 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.id, serializer); - ::sse_encode(self.uid, serializer); ::sse_encode(self.channel, serializer); ::sse_encode(self.name, serializer); ::sse_encode(self.input_muted, serializer); @@ -3455,6 +3661,37 @@ impl SseEncode for crate::api::BridgeClient { } } +impl SseEncode for crate::api::BridgeClientProfile { + // 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.id, serializer); + ::sse_encode(self.channel, serializer); + ::sse_encode(self.name, serializer); + ::sse_encode(self.unique_id, serializer); + >::sse_encode(self.database_id, serializer); + ::sse_encode(self.country_code, serializer); + ::sse_encode(self.description, serializer); + ::sse_encode(self.version, serializer); + ::sse_encode(self.platform, serializer); + >::sse_encode(self.created_unix_seconds, serializer); + >::sse_encode(self.last_connected_unix_seconds, serializer); + >::sse_encode(self.connections_total, serializer); + >::sse_encode(self.online_seconds, serializer); + >::sse_encode(self.idle_milliseconds, serializer); + >::sse_encode(self.ping_milliseconds, serializer); + ::sse_encode(self.client_address, serializer); + >::sse_encode(self.server_groups, serializer); + ::sse_encode(self.channel_group, serializer); + ::sse_encode(self.avatar_path, serializer); + >::sse_encode(self.bytes_downloaded_month, serializer); + >::sse_encode(self.bytes_uploaded_month, serializer); + >::sse_encode(self.bytes_downloaded_total, serializer); + >::sse_encode(self.bytes_uploaded_total, serializer); + >::sse_encode(self.packet_loss_client_to_server_total, serializer); + >::sse_encode(self.packet_loss_server_to_client_total, serializer); + } +} + impl SseEncode for crate::api::BridgeEffectOwner { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -3828,6 +4065,16 @@ impl SseEncode for i64 { } } +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) { @@ -3898,6 +4145,16 @@ impl SseEncode for Option { } } +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) { @@ -3908,6 +4165,16 @@ impl SseEncode for Option { } } +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) { diff --git a/crates/chanora_diagnostics/src/lib.rs b/crates/chanora_diagnostics/src/lib.rs index a4d87f9..31701e0 100644 --- a/crates/chanora_diagnostics/src/lib.rs +++ b/crates/chanora_diagnostics/src/lib.rs @@ -774,6 +774,11 @@ impl ProtocolEventRecorder { pub fn drain(&mut self) -> Vec { std::mem::take(&mut self.events) } + + /// Snapshot all recorded events without clearing the buffer. + pub fn snapshot(&self) -> Vec { + self.events.clone() + } } impl Default for ProtocolEventRecorder { @@ -1080,4 +1085,17 @@ mod tests { "explicit with_android_audio(None) must also omit the section" ); } + + #[test] + fn protocol_event_snapshot_does_not_drain_buffer() { + let mut recorder = ProtocolEventRecorder::new(4); + recorder.record_connected("Server"); + + let first = recorder.snapshot(); + let second = recorder.snapshot(); + let drained = recorder.drain(); + + assert_eq!(first, second); + assert_eq!(drained, first); + } } diff --git a/crates/chanora_protocol/Cargo.toml b/crates/chanora_protocol/Cargo.toml index 98384e5..adda0b6 100644 --- a/crates/chanora_protocol/Cargo.toml +++ b/crates/chanora_protocol/Cargo.toml @@ -13,8 +13,9 @@ publish.workspace = true thiserror.workspace = true tracing.workspace = true serde.workspace = true -base64 = "0.22" chanora_resolver = { path = "../chanora_resolver" } +base64 = "0.22" +time = "0.3" # tsclientlib is git-only and not on crates.io. The "audio" feature # pulls in `audiopus` only — `sdl2` is a dev-dep used by upstream @@ -32,14 +33,12 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync" futures = "0.3" async-trait = "0.1" -[target.'cfg(any(target_os = "ios", target_os = "macos", target_os = "windows"))'.dependencies] -# Apple + Windows keep the platform TLS backend (Security.framework / -# SChannel). iOS in particular avoids the rustls/aws-lc path here. +[target.'cfg(not(target_os = "android"))'.dependencies] +# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework +# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD). reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] } -[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] -# Android + Linux avoid a system OpenSSL dependency and use rustls instead. -# This keeps Linux CI / dev hosts buildable without `openssl-devel`, while -# preserving the native-tls path for Apple targets where rustls/aws-lc has -# been problematic. +[target.'cfg(target_os = "android")'.dependencies] +# Android cross-builds should not pull OpenSSL. Use rustls here while keeping +# native-tls for Apple targets where aws-lc/rustls is problematic for iOS. reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] } diff --git a/crates/chanora_protocol/src/adapter.rs b/crates/chanora_protocol/src/adapter.rs index 97408c0..8b55c90 100644 --- a/crates/chanora_protocol/src/adapter.rs +++ b/crates/chanora_protocol/src/adapter.rs @@ -28,21 +28,44 @@ use tokio::sync::{mpsc, oneshot}; use tracing::{info, warn}; use tsclientlib::data::{self, Channel, Client}; +use tsclientlib::messages::s2c::{InClientDbInfoPart, InMessage}; use tsclientlib::prelude::*; use tsclientlib::{ - ChannelId as TsChannelId, Connection, DisconnectOptions, Identity, MessageHandle, - OutCommandExt, StreamItem, Version, + ChannelId as TsChannelId, ClientId as TsClientId, Connection, DisconnectOptions, Identity, + MessageHandle, OutCommandExt, StreamItem, Version, }; -use tsproto_packets::packets::{InAudioBuf, OutPacket}; +use tsproto_packets::packets::{Direction, Flags, InAudioBuf, OutCommand, OutPacket, PacketType}; use tsproto_types::ClientType; use crate::dto::{ - ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, MessageTarget, ServerActivity, - ServerSnapshot, + ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget, + ServerActivity, ServerSnapshot, }; use crate::ProtocolError; const SPEAKING_ACTIVITY_WINDOW: Duration = Duration::from_millis(750); +const INBOUND_VOICE_SEND_TIMEOUT: Duration = Duration::from_millis(40); + +#[derive(Debug, PartialEq, Eq)] +enum SendTimeoutError { + Timeout(T), + Closed(T), +} + +async fn send_with_timeout( + tx: &mpsc::Sender, + value: T, + timeout_duration: Duration, +) -> Result<(), SendTimeoutError> { + match tokio::time::timeout(timeout_duration, tx.reserve()).await { + Ok(Ok(permit)) => { + permit.send(value); + Ok(()) + } + Ok(Err(_)) => Err(SendTimeoutError::Closed(value)), + Err(_) => Err(SendTimeoutError::Timeout(value)), + } +} /// Pick the TeamSpeak `client_version`/platform/signature triple /// (sourced from `ReSpeak/tsdeclarations/Versions.csv`, baked into @@ -116,6 +139,11 @@ pub struct ConnectConfig { /// How long to wait for the initial state snapshot before /// returning `ProtocolError::Timeout`. pub ready_timeout: Duration, + /// Optional already-resolved socket address from core's invisible + /// prefetch cache. When present, the protocol layer skips address + /// resolution but still opens a normal TS3 connection only after + /// the user requested Connect. + pub resolved_address: Option, } impl Default for ConnectConfig { @@ -126,6 +154,7 @@ impl Default for ConnectConfig { password: None, identity: None, ready_timeout: Duration::from_secs(10), + resolved_address: None, } } } @@ -159,6 +188,11 @@ enum Request { /// Reply channel for outcome. reply: oneshot::Sender>, }, + /// Fetch richer profile/connection details for an online client. + FetchClientProfile { + client_id: u64, + reply: oneshot::Sender>, + }, } /// Why a [`ProtocolClient`] task ended. Distinguishes a user-driven @@ -297,6 +331,20 @@ impl ProtocolClient { .map_err(|_| ProtocolError::Lost("snapshot reply dropped".to_string()))? } + /// Fetch richer profile and live connection details for one online client. + pub async fn client_profile(&self, client_id: u64) -> Result { + let (tx, rx) = oneshot::channel(); + self.tx + .send(Request::FetchClientProfile { + client_id, + reply: tx, + }) + .await + .map_err(|_| ProtocolError::Lost("connection task is gone".to_string()))?; + rx.await + .map_err(|_| ProtocolError::Lost("client_profile reply dropped".to_string()))? + } + /// Disconnect cleanly. Blocks until the task exits. pub async fn disconnect(self) { let (tx, rx) = oneshot::channel(); @@ -493,16 +541,30 @@ async fn connection_task( }}; } - let resolved = match resolve_server_socket(&cfg.address).await { - Ok(addr) => addr, - Err(err) => fail_ready!(err), + let resolved = match server_socket_from_config(&cfg) { + Some(addr) => { + info!( + target: "chanora_protocol", + input = %cfg.address, + resolved = %addr, + "using prefetched server address" + ); + addr + } + None => { + let addr = match resolve_server_socket(&cfg.address).await { + Ok(addr) => addr, + Err(err) => fail_ready!(err), + }; + info!( + target: "chanora_protocol", + input = %cfg.address, + resolved = %addr, + "server address resolved" + ); + addr + } }; - info!( - target: "chanora_protocol", - input = %cfg.address, - resolved = %resolved, - "server address resolved" - ); // Pass the resolved SocketAddr directly to tsclientlib so it // skips its own resolver entirely (tsclientlib accepts @@ -619,14 +681,6 @@ async fn connection_task( std::time::Instant, ), > = HashMap::new(); - let mut pending_text_messages: HashMap< - MessageHandle, - ( - MessageTarget, - oneshot::Sender>, - std::time::Instant, - ), - > = HashMap::new(); let mut voice_activity: HashMap = HashMap::new(); // Main loop: pump events, service requests, forward voice. @@ -650,14 +704,33 @@ async fn connection_task( let from = packet_sender_id(&buf); if let Some(from) = from { voice_activity.insert(from, Instant::now()); - if voice_in_tx - .try_send(InboundVoice { - from_client: from, - packet: buf, - }) - .is_err() + let inbound = InboundVoice { + from_client: from, + packet: buf, + }; + match send_with_timeout( + &voice_in_tx, + inbound, + INBOUND_VOICE_SEND_TIMEOUT, + ) + .await { - // Subscriber is too slow or absent; drop. + Ok(()) => {} + Err(SendTimeoutError::Timeout(_)) => { + warn!( + target: "chanora_protocol", + from_client = from, + timeout_ms = INBOUND_VOICE_SEND_TIMEOUT.as_millis() as u64, + "inbound voice queue stayed full; dropping packet" + ); + } + Err(SendTimeoutError::Closed(_)) => { + warn!( + target: "chanora_protocol", + from_client = from, + "inbound voice consumer closed; dropping packet" + ); + } } } } @@ -735,7 +808,28 @@ async fn connection_task( if let Some((_target_channel, reply, _deadline)) = pending_moves.remove(&handle) { - let mapped = map_command_result(result, "client_move"); + let mapped = match result { + Ok(()) => Ok(()), + Err(cmd_err) => { + // tsclientlib's CommandError carries a + // typed `TsError` (the canonical TS3 + // error code) plus an optional missing + // permission. We convert to our typed + // ProtocolError::ServerRejected so the + // upper layers can render a localised + // explanation by code instead of a + // generic backend string. + let code = cmd_err.error as u32; + let message = cmd_err.error.to_string(); + info!( + target: "chanora_protocol", + code, + message = %message, + "server rejected client_move" + ); + Err(ProtocolError::ServerRejected { code, message }) + } + }; if let Some(reply) = reply { let _ = reply.send(mapped); } else if let Err(err) = mapped { @@ -745,11 +839,6 @@ async fn connection_task( "client_move completed in background with error" ); } - } else if let Some((_target, reply, _deadline)) = - pending_text_messages.remove(&handle) - { - let mapped = map_command_result(result, "text_message"); - let _ = reply.send(mapped); } } _ => { /* book / message / other events: ignore */ } @@ -794,24 +883,6 @@ async fn connection_task( } } } - if !pending_text_messages.is_empty() { - let now = std::time::Instant::now(); - let expired: Vec = pending_text_messages - .iter() - .filter_map(|(handle, (_, _, deadline))| { - if now >= *deadline { - Some(*handle) - } else { - None - } - }) - .collect(); - for handle in expired { - if let Some((_target, reply, _)) = pending_text_messages.remove(&handle) { - let _ = reply.send(Err(ProtocolError::Timeout)); - } - } - } // 3. Service at most one control request (non-blocking). match rx.try_recv() { @@ -861,15 +932,14 @@ async fn connection_task( message, target, reply, - }) => match send_text_message(&mut con, &message, target) { - Ok(handle) => { - let deadline = std::time::Instant::now() + Duration::from_secs(3); - pending_text_messages.insert(handle, (target, reply, deadline)); - } - Err(e) => { - let _ = reply.send(Err(e)); - } - }, + }) => { + let r = send_text_message(&mut con, &message, target); + let _ = reply.send(r); + } + Ok(Request::FetchClientProfile { client_id, reply }) => { + let r = fetch_client_profile(&mut con, client_id).await; + let _ = reply.send(r); + } Ok(Request::Disconnect(reply)) => { let _ = con.disconnect(DisconnectOptions::new()); con.events().for_each(|_| future::ready(())).await; @@ -908,6 +978,10 @@ async fn resolve_server_socket(address: &str) -> Result Option { + cfg.resolved_address +} + /// Move our own client into `channel_id` with an optional password. /// Looks up our `own_client` in the current state and dispatches the /// generated `client_move` command via `send_with_result`. The @@ -973,11 +1047,11 @@ fn send_text_message( con: &mut Connection, message: &str, target: MessageTarget, -) -> Result { +) -> Result<(), ProtocolError> { use tsproto_types::TextMessageTargetMode; match target { MessageTarget::Server => { - send_text_to_mode(con, message, TextMessageTargetMode::Server, "server") + send_text_to_mode(con, message, TextMessageTargetMode::Server, "server")?; } MessageTarget::Channel => { // Fix: previously channel messages were sent via @@ -985,33 +1059,31 @@ fn send_text_message( // TextMessageTargetMode::Server. Now correctly uses // TextMessageTargetMode::Channel so the message is // scoped to the current channel, not server-wide. - send_text_to_mode(con, message, TextMessageTargetMode::Channel, "channel") + send_text_to_mode(con, message, TextMessageTargetMode::Channel, "channel")?; } MessageTarget::Client(client_id) => { let state = con .get_state() .map_err(|e| ProtocolError::Backend(format!("get_state: {e}")))?; let client = find_client_by_id(state.clients.values(), client_id)?; - let handle = client + client .send_textmessage(message) - .send_with_result(con) + .send(con) .map_err(|e| ProtocolError::Backend(format!("send_textmessage(client): {e}")))?; - info!(target: "chanora_protocol", len = message.len(), ?target, "text message queued"); - Ok(handle) } MessageTarget::Poke(client_id) => { let state = con .get_state() .map_err(|e| ProtocolError::Backend(format!("get_state: {e}")))?; let client = find_client_by_id(state.clients.values(), client_id)?; - let handle = client + client .poke(message) - .send_with_result(con) + .send(con) .map_err(|e| ProtocolError::Backend(format!("poke: {e}")))?; - info!(target: "chanora_protocol", len = message.len(), ?target, "text message queued"); - Ok(handle) } } + info!(target: "chanora_protocol", len = message.len(), ?target, "text message sent"); + Ok(()) } fn send_text_to_mode( @@ -1019,7 +1091,7 @@ fn send_text_to_mode( message: &str, target: tsproto_types::TextMessageTargetMode, label: &str, -) -> Result { +) -> Result<(), ProtocolError> { use ts_bookkeeping::messages::c2s; c2s::OutSendTextMessageMessage::new(&mut std::iter::once(c2s::OutSendTextMessagePart { @@ -1027,31 +1099,242 @@ fn send_text_to_mode( target_client_id: None, message: message.into(), })) - .send_with_result(con) + .send(con) .map_err(|e| ProtocolError::Backend(format!("send_textmessage({label}): {e}"))) } -fn map_command_result( - result: Result<(), tsclientlib::CommandError>, - action: &str, -) -> Result<(), ProtocolError> { - match result { - Ok(()) => Ok(()), - Err(cmd_err) => { - let code = cmd_err.error as u32; - let message = cmd_err.error.to_string(); - info!( - target: "chanora_protocol", - action, - code, - message = %message, - "server rejected command" - ); - Err(ProtocolError::ServerRejected { code, message }) +async fn fetch_client_profile( + con: &mut Connection, + client_id: u64, +) -> Result { + let target_id = TsClientId(client_id as u16); + let (database_id, uid_b64) = { + let state = con + .get_state() + .map_err(|e| ProtocolError::Backend(format!("get_state: {e}")))?; + let client = find_client_by_id(state.clients.values(), client_id)?; + ( + client.database_id, + client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())), + ) + }; + + let _ = request_messages(con, build_command("servergrouplist", &[], &[])).await; + let _ = request_messages(con, build_command("channelgrouplist", &[], &[])).await; + let _ = request_messages( + con, + build_command( + "clientgetvariables", + &[("clid", client_id.to_string())], + &[], + ), + ) + .await; + let _ = request_messages( + con, + build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]), + ) + .await; + + let db_info = request_client_db_info(con, database_id).await.ok(); + let state = con + .get_state() + .map_err(|e| ProtocolError::Backend(format!("get_state: {e}")))?; + let client = state + .clients + .get(&target_id) + .ok_or_else(|| ProtocolError::Backend(format!("client {client_id} not found")))?; + let optional = client.optional_data.as_ref(); + let connection = client.connection_data.as_ref(); + let server_group_names: HashMap<_, _> = state + .server_groups + .iter() + .map(|(id, group)| (*id, group.name.clone())) + .collect(); + let channel_group_names: HashMap<_, _> = state + .channel_groups + .iter() + .map(|(id, group)| (*id, group.name.clone())) + .collect(); + + let unique_id = db_info + .as_ref() + .map(|info| uid_to_b64(info.uid.as_ref())) + .or(uid_b64) + .unwrap_or_default(); + let avatar_path = if client.avatar_hash.is_empty() || unique_id.is_empty() { + String::new() + } else { + format!("/avatar_{}", uid_to_avatar_path(&unique_id)) + }; + + Ok(ClientProfile { + id: ClientId(client.id.0 as u64), + channel: ChannelId(client.channel.0), + name: db_info + .as_ref() + .map(|info| sanitize(&info.name)) + .unwrap_or_else(|| sanitize(&client.name)), + unique_id, + database_id: Some(client.database_id.0), + country_code: client.country_code.clone(), + description: db_info + .as_ref() + .map(|info| sanitize(&info.description)) + .unwrap_or_else(|| sanitize(&client.description)), + version: optional + .map(|info| info.version.clone()) + .unwrap_or_default(), + platform: optional + .map(|info| info.platform.clone()) + .unwrap_or_default(), + created_unix_seconds: optional + .map(|info| info.created.unix_timestamp()) + .or_else(|| db_info.as_ref().map(|info| info.created.unix_timestamp())), + last_connected_unix_seconds: optional + .map(|info| info.last_connected.unix_timestamp()) + .or_else(|| { + db_info + .as_ref() + .map(|info| info.last_connected.unix_timestamp()) + }), + connections_total: optional + .map(|info| u64::from(info.connections_total)) + .or_else(|| { + db_info + .as_ref() + .map(|info| u64::from(info.connections_total)) + }), + online_seconds: connection + .and_then(|info| info.connected_time.map(|duration| duration.whole_seconds())), + idle_milliseconds: connection.map(|info| duration_millis(info.idle_time)), + ping_milliseconds: connection.and_then(|info| info.ping.map(duration_millis)), + client_address: connection + .and_then(|info| info.client_address.map(|address| address.to_string())) + .unwrap_or_default(), + server_groups: format_server_groups(client.server_groups.iter(), &server_group_names), + channel_group: channel_group_names + .get(&client.channel_group) + .cloned() + .unwrap_or_else(|| format!("Unknown ({})", client.channel_group.0)), + avatar_path, + bytes_downloaded_month: optional + .map(|info| info.bytes_downloaded_month) + .or_else(|| db_info.as_ref().map(|info| info.bytes_downloaded_month)), + bytes_uploaded_month: optional + .map(|info| info.bytes_uploaded_month) + .or_else(|| db_info.as_ref().map(|info| info.bytes_uploaded_month)), + bytes_downloaded_total: optional + .map(|info| info.bytes_downloaded_total) + .or_else(|| db_info.as_ref().map(|info| info.bytes_downloaded_total)), + bytes_uploaded_total: optional + .map(|info| info.bytes_uploaded_total) + .or_else(|| db_info.as_ref().map(|info| info.bytes_uploaded_total)), + packet_loss_client_to_server_total: connection + .map(|info| info.client_to_server_packetloss_total), + packet_loss_server_to_client_total: connection + .and_then(|info| info.server_to_client_packetloss_total), + }) +} + +fn build_command(name: &str, args: &[(&str, String)], flags: &[&str]) -> OutCommand { + let mut command = OutCommand::new(Direction::C2S, Flags::empty(), PacketType::Command, name); + for flag in flags { + command.write_arg(flag, &""); + } + for (key, value) in args { + command.write_arg(key, value); + } + command +} + +async fn request_messages( + con: &mut Connection, + command: OutCommand, +) -> Result, ProtocolError> { + let handle = command + .send_with_result(con) + .map_err(|e| ProtocolError::Backend(format!("send command: {e}")))?; + let mut messages = Vec::new(); + loop { + let item = con + .events() + .next() + .await + .ok_or_else(|| ProtocolError::Lost("event stream ended".to_string()))? + .map_err(|e| ProtocolError::Lost(e.to_string()))?; + match item { + StreamItem::MessageEvent(message) => messages.push(message), + StreamItem::MessageResult(reply, status) if reply == handle => { + status.map_err(|error| ProtocolError::ServerRejected { + code: error.error as u32, + message: error.error.to_string(), + })?; + return Ok(messages); + } + _ => {} } } } +async fn request_client_db_info( + con: &mut Connection, + dbid: tsclientlib::ClientDbId, +) -> Result { + let messages = request_messages( + con, + build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]), + ) + .await?; + for message in messages { + if let InMessage::ClientDbInfo(info) = message { + if let Some(row) = info.iter().next() { + return Ok(row.clone()); + } + } + } + Err(ProtocolError::Backend( + "clientdbinfo returned no row".to_string(), + )) +} + +fn format_server_groups<'a>( + groups: impl IntoIterator, + names: &HashMap, +) -> Vec { + let mut rendered = groups + .into_iter() + .map(|group| { + names + .get(group) + .cloned() + .unwrap_or_else(|| format!("Unknown ({})", group.0)) + }) + .collect::>(); + rendered.sort(); + rendered +} + +fn duration_millis(duration: time::Duration) -> i64 { + duration + .whole_milliseconds() + .clamp(i64::MIN as i128, i64::MAX as i128) as i64 +} + +fn uid_to_b64(uid: &tsclientlib::Uid) -> String { + BASE64_STANDARD.encode(&uid.0) +} + +fn uid_to_avatar_path(uid_b64: &str) -> String { + let decoded = BASE64_STANDARD.decode(uid_b64).unwrap_or_default(); + let mut rendered = String::with_capacity(decoded.len() * 2); + for byte in decoded { + rendered.push((b'a' + (byte >> 4)) as char); + rendered.push((b'a' + (byte & 0x0f)) as char); + } + rendered +} + fn find_client_by_id<'a>( clients: impl IntoIterator, client_id: u64, @@ -1219,11 +1502,6 @@ fn build_snapshot( .iter() .map(|c| ClientInfo { id: ClientId(c.id.0 as u64), - uid: c - .uid - .as_ref() - .map(|uid| BASE64_STANDARD.encode(&uid.0)) - .unwrap_or_default(), channel: ChannelId(c.channel.0), name: sanitize(&c.name), input_muted: c.input_muted, @@ -1441,7 +1719,12 @@ const _: () = { #[cfg(test)] mod tests { - use super::{is_server_query_client_type, sort_channels_tree_by}; + use super::{ + is_server_query_client_type, send_with_timeout, server_socket_from_config, + sort_channels_tree_by, ConnectConfig, SendTimeoutError, + }; + use std::time::Duration; + use tokio::sync::mpsc; use tsproto_types::ClientType; /// Lightweight fixture mirroring just the (id, parent, order) @@ -1470,6 +1753,36 @@ mod tests { })); } + #[test] + fn connect_config_can_carry_prefetched_socket_address() { + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let cfg = ConnectConfig { + address: "example.com".to_string(), + nickname: "Tester".to_string(), + password: None, + identity: None, + ready_timeout: Duration::from_secs(1), + resolved_address: Some(addr), + }; + + assert_eq!(cfg.resolved_address, Some(addr)); + } + + #[test] + fn server_socket_from_config_prefers_prefetched_address() { + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let cfg = ConnectConfig { + address: "example.com".to_string(), + resolved_address: Some(addr), + nickname: "Tester".to_string(), + password: None, + identity: None, + ready_timeout: Duration::from_secs(1), + }; + + assert_eq!(server_socket_from_config(&cfg), Some(addr)); + } + #[test] fn channel_sort_linked_list_under_one_parent() { // Server emits four root-level channels in arbitrary HashMap @@ -1589,4 +1902,32 @@ mod tests { assert!(ids.contains(&1)); assert!(ids.contains(&2)); } + + #[tokio::test] + async fn send_with_timeout_waits_for_capacity_instead_of_dropping() { + let (tx, mut rx) = mpsc::channel(1); + tx.send(1_u8).await.expect("seed first item"); + + let drain = tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(10)).await; + assert_eq!(rx.recv().await, Some(1)); + rx.recv().await + }); + + send_with_timeout(&tx, 2_u8, Duration::from_millis(100)) + .await + .expect("second item should enqueue once capacity frees"); + + assert_eq!(drain.await.expect("drain task should succeed"), Some(2)); + } + + #[tokio::test] + async fn send_with_timeout_times_out_when_capacity_stays_full() { + let (tx, _rx) = mpsc::channel(1); + tx.send(1_u8).await.expect("seed first item"); + + let result = send_with_timeout(&tx, 2_u8, Duration::from_millis(10)).await; + + assert_eq!(result, Err(SendTimeoutError::Timeout(2))); + } } diff --git a/crates/chanora_protocol/src/dto.rs b/crates/chanora_protocol/src/dto.rs index 59d6d67..6867c22 100644 --- a/crates/chanora_protocol/src/dto.rs +++ b/crates/chanora_protocol/src/dto.rs @@ -13,7 +13,7 @@ pub struct ChannelId(pub u64); pub struct ClientId(pub u64); /// One channel in the server's tree. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ChannelInfo { /// Stable channel id. pub id: ChannelId, @@ -44,7 +44,7 @@ pub enum MessageTarget { } /// An in-channel text message from a specific client. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ChatMessage { /// The client id of the sender. pub sender_id: ClientId, @@ -57,19 +57,17 @@ pub struct ChatMessage { } /// A server-activity notification derived from TeamSpeak bookkeeping events. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerActivity { /// Human-readable activity line. pub message: String, } /// One connected client on the server. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ClientInfo { /// Stable client id. pub id: ClientId, - /// Stable TeamSpeak unique identifier for cross-session preference keys. - pub uid: String, /// Channel the client is currently in. pub channel: ChannelId, /// Nickname, preserved verbatim per ADR-008. @@ -88,8 +86,64 @@ pub struct ClientInfo { pub talk_power_granted: bool, } +/// Best-effort profile and live connection details for one online +/// client, fetched through the normal TeamSpeak client protocol. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ClientProfile { + /// Stable online client id. + pub id: ClientId, + /// Channel the client is currently in. + pub channel: ChannelId, + /// Nickname, preserved verbatim per ADR-008. + pub name: String, + /// TeamSpeak unique id encoded in the canonical base64 form. + pub unique_id: String, + /// Stable TeamSpeak database id, when visible. + pub database_id: Option, + /// ISO country code, when the server exposes one. + pub country_code: String, + /// User description, when visible. + pub description: String, + /// Client version string, populated by `clientgetvariables`. + pub version: String, + /// Client platform string, populated by `clientgetvariables`. + pub platform: String, + /// Account creation time as Unix seconds. + pub created_unix_seconds: Option, + /// Last connection time as Unix seconds. + pub last_connected_unix_seconds: Option, + /// Total historical connections, when visible. + pub connections_total: Option, + /// Current online duration in seconds, when visible. + pub online_seconds: Option, + /// Current idle time in milliseconds, when visible. + pub idle_milliseconds: Option, + /// Current ping in milliseconds, when visible. + pub ping_milliseconds: Option, + /// Client address. Empty when permission-gated. + pub client_address: String, + /// Resolved server group names for the online client. + pub server_groups: Vec, + /// Resolved channel group name for the online client. + pub channel_group: String, + /// TeamSpeak avatar file path suffix, when an avatar hash is present. + pub avatar_path: String, + /// Downloaded bytes this month. + pub bytes_downloaded_month: Option, + /// Uploaded bytes this month. + pub bytes_uploaded_month: Option, + /// Downloaded bytes across all time. + pub bytes_downloaded_total: Option, + /// Uploaded bytes across all time. + pub bytes_uploaded_total: Option, + /// Client-to-server total packet loss ratio. + pub packet_loss_client_to_server_total: Option, + /// Server-to-client total packet loss ratio. + pub packet_loss_server_to_client_total: Option, +} + /// Snapshot of the server's published state at a moment in time. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSnapshot { /// Server name. pub server_name: String, diff --git a/crates/chanora_protocol/src/lib.rs b/crates/chanora_protocol/src/lib.rs index 9ca0f69..92a004e 100644 --- a/crates/chanora_protocol/src/lib.rs +++ b/crates/chanora_protocol/src/lib.rs @@ -8,7 +8,8 @@ //! //! * [`ConnectConfig`] — typed connection parameters. //! * [`ProtocolClient`] — async handle owning the connection task. -//! * [`ServerSnapshot`], [`ChannelInfo`], [`ClientInfo`] — opaque +//! * [`ServerSnapshot`], [`ChannelInfo`], [`ClientInfo`], +//! [`ClientProfile`] — opaque //! DTOs containing only `String`s and primitives. //! * [`ProtocolError`] — typed error catalogue. //! @@ -37,8 +38,8 @@ mod dto; pub use adapter::{ConnectConfig, DisconnectReason, InboundVoice, ProtocolClient, SnapshotProbe}; pub use dto::{ - ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, MessageTarget, ServerActivity, - ServerSnapshot, + ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget, + ServerActivity, ServerSnapshot, }; // Re-export the upstream voice types so chanora_audio can build outbound diff --git a/crates/chanora_resolver/Cargo.toml b/crates/chanora_resolver/Cargo.toml index 9c7df6c..3526a5f 100644 --- a/crates/chanora_resolver/Cargo.toml +++ b/crates/chanora_resolver/Cargo.toml @@ -2,6 +2,8 @@ name = "chanora_resolver" version = "0.1.0" edition = "2021" +license = "MIT OR Apache-2.0" +publish = false build = "build.rs" [dependencies] diff --git a/crates/chanora_resolver/README.md b/crates/chanora_resolver/README.md index f288f31..4a57fba 100644 --- a/crates/chanora_resolver/README.md +++ b/crates/chanora_resolver/README.md @@ -9,10 +9,11 @@ Use `ChanoraResolver::resolve_client_request` or `resolve_client_address` for ap 1. Normalize raw client input, including `ts3server://host?port=...`. 2. For dotless names such as `6666` or `wwb`, query the myTeamSpeak server-name endpoint first. If it returns a hostname, restart normal resolution with that hostname. If it returns `ip:port`, use that final address. 3. Start plain A/AAAA DNS as the final fallback address. -4. Prefer `_ts3._udp.` SRV. Its target host and port override the user input port, then the target is resolved to a final IP. -5. Try `_tsdns._tcp.` SRV on candidate parent/full hosts and query the returned TSDNS server over TCP. -6. Try direct TSDNS TCP on candidate parent/full hosts at port `41144`. -7. Fall back to the A/AAAA result with the user-supplied port, or default TeamSpeak port `9987`. +4. If the user supplied an explicit port and DNS resolved, return that direct target immediately. +5. Prefer `_ts3._udp.` SRV. Its target host and port override the default port, then the target is resolved to a final IP. +6. Try `_tsdns._tcp.` SRV on candidate parent/full hosts and query the returned TSDNS server over TCP. +7. Try direct TSDNS TCP on candidate parent/full hosts at port `41144`. +8. Fall back to the A/AAAA result with the default TeamSpeak port `9987`. When DNS fallback is already available, TSDNS discovery is capped so missing or filtered TSDNS cannot add multi-second join delays. The older explicit API (`Args { host, service, protocol }`) is still available for diagnostic example use. @@ -101,4 +102,4 @@ cargo run --example cli -- -host voice.teamspeak.com -service ts3 cargo fmt --all -- --check cargo test cargo clippy --all-targets --all-features -- -D warnings -``` \ No newline at end of file +``` diff --git a/crates/chanora_resolver/src/lib.rs b/crates/chanora_resolver/src/lib.rs index b3184b1..fa3e0ec 100644 --- a/crates/chanora_resolver/src/lib.rs +++ b/crates/chanora_resolver/src/lib.rs @@ -18,6 +18,8 @@ const NICK_RESOLVE_URL: &str = "https://named.myteamspeak.com/lookup"; const TSDNS_PORT: u16 = 41144; const TSDNS_TERMINATOR: &[u8] = b"\n\r\r\r\n"; const TSDNS_TIMEOUT: Duration = Duration::from_secs(3); +const TS3_SRV_FALLBACK_DISCOVERY_BUDGET: Duration = Duration::from_millis(900); +const TSDNS_FALLBACK_DISCOVERY_BUDGET: Duration = Duration::from_millis(900); pub const DEFAULT_TEAMSPEAK_PORT: u16 = 9987; #[derive(Debug, Clone, PartialEq, Eq)] @@ -262,7 +264,7 @@ impl ChanoraResolver { )); } - let direct = if is_bare_numeric_name(&host) { + let mut direct = if is_bare_numeric_name(&host) { errors.push("dns: skipped bare numeric server name".to_string()); None } else { @@ -275,53 +277,80 @@ impl ChanoraResolver { } }; - match self.resolve_ts3(&host, "").await { - Ok(resolution) => { - let selected = match &resolution { - Resolution::Srv { selected, .. } => selected.clone(), - _ => unreachable!("ts3 resolution must be an srv result"), - }; - return self - .client_srv_resolution( - input, - ClientResolutionMethod::Ts3Srv, - &selected, - resolution, - ) - .await; + if should_return_direct_dns_before_discovery(port) { + if let Some(resolution) = direct.take() { + return Ok(direct_client_resolution( + input, + from_server_name, + fallback_port, + resolution, + )); } - Err(err) => errors.push(format!("ts3 srv: {err}")), } - match self - .resolve_tsdns_srv_candidates(input, &host, fallback_port) - .await - { - Ok(resolution) => return Ok(resolution), - Err(err) => errors.push(format!("tsdns srv: {err}")), + let ts3 = self.resolve_ts3(&host, ""); + match ts3_srv_discovery_budget(direct.is_some()) { + Some(budget) => match timeout(budget, ts3).await { + Ok(Ok(resolution)) => { + let selected = match &resolution { + Resolution::Srv { selected, .. } => selected.clone(), + _ => unreachable!("ts3 resolution must be an srv result"), + }; + return self + .client_srv_resolution( + input, + ClientResolutionMethod::Ts3Srv, + &selected, + resolution, + ) + .await; + } + Ok(Err(err)) => errors.push(format!("ts3 srv: {err}")), + Err(_) => errors.push(format!( + "ts3 srv: discovery timed out after {}ms", + budget.as_millis() + )), + }, + None => match ts3.await { + Ok(resolution) => { + let selected = match &resolution { + Resolution::Srv { selected, .. } => selected.clone(), + _ => unreachable!("ts3 resolution must be an srv result"), + }; + return self + .client_srv_resolution( + input, + ClientResolutionMethod::Ts3Srv, + &selected, + resolution, + ) + .await; + } + Err(err) => errors.push(format!("ts3 srv: {err}")), + }, } - match self - .resolve_tsdns_tcp_candidates(input, &host, fallback_port) - .await - { - Ok(resolution) => return Ok(resolution), - Err(err) => errors.push(format!("tsdns tcp: {err}")), + let tsdns = self.resolve_tsdns_candidates(input, &host, fallback_port); + match tsdns_discovery_budget(direct.is_some()) { + Some(budget) => match timeout(budget, tsdns).await { + Ok(Ok(resolution)) => return Ok(resolution), + Ok(Err(err)) => errors.push(format!("tsdns: {err}")), + Err(_) => errors.push(format!( + "tsdns: discovery timed out after {}ms", + budget.as_millis() + )), + }, + None => match tsdns.await { + Ok(resolution) => return Ok(resolution), + Err(err) => errors.push(format!("tsdns: {err}")), + }, } if let Some(resolution) = direct { - let selected = match &resolution { - Resolution::Dns { selected, .. } => *selected, - _ => unreachable!("direct fallback must be a dns resolution"), - }; - return Ok(client_resolution_with_address( + return Ok(direct_client_resolution( input, - if from_server_name { - ClientResolutionMethod::Nick - } else { - ClientResolutionMethod::Dns - }, - format_host_port(&selected.to_string(), fallback_port), + from_server_name, + fallback_port, resolution, )); } @@ -332,6 +361,32 @@ impl ChanoraResolver { ) } + async fn resolve_tsdns_candidates( + &self, + input: &str, + query_host: &str, + fallback_port: u16, + ) -> Result { + let mut errors = Vec::new(); + match self + .resolve_tsdns_srv_candidates(input, query_host, fallback_port) + .await + { + Ok(resolution) => return Ok(resolution), + Err(err) => errors.push(format!("tsdns srv: {err}")), + } + + match self + .resolve_tsdns_tcp_candidates(input, query_host, fallback_port) + .await + { + Ok(resolution) => return Ok(resolution), + Err(err) => errors.push(format!("tsdns tcp: {err}")), + } + + bail!("{}", errors.join("; ")) + } + async fn client_srv_resolution( &self, input: &str, @@ -716,17 +771,6 @@ impl ChanoraResolver { } } -#[cfg(target_os = "android")] -fn srv_resolver_builder() -> Result> { - use hickory_resolver::config::{ResolverConfig, CLOUDFLARE}; - - Ok(TokioResolver::builder_with_config( - ResolverConfig::udp_and_tcp(&CLOUDFLARE), - TokioRuntimeProvider::default(), - )) -} - -#[cfg(not(target_os = "android"))] fn srv_resolver_builder() -> Result> { TokioResolver::builder_tokio().context("failed to initialize DNS resolver") } @@ -801,6 +845,40 @@ fn format_host_port(host: &str, port: u16) -> String { } } +fn should_return_direct_dns_before_discovery(port: Option) -> bool { + port.is_some() +} + +fn tsdns_discovery_budget(has_dns_fallback: bool) -> Option { + has_dns_fallback.then_some(TSDNS_FALLBACK_DISCOVERY_BUDGET) +} + +fn ts3_srv_discovery_budget(has_dns_fallback: bool) -> Option { + has_dns_fallback.then_some(TS3_SRV_FALLBACK_DISCOVERY_BUDGET) +} + +fn direct_client_resolution( + input: &str, + from_server_name: bool, + fallback_port: u16, + resolution: Resolution, +) -> ClientResolution { + let selected = match &resolution { + Resolution::Dns { selected, .. } => *selected, + _ => unreachable!("direct fallback must be a dns resolution"), + }; + client_resolution_with_address( + input, + if from_server_name { + ClientResolutionMethod::Nick + } else { + ClientResolutionMethod::Dns + }, + format_host_port(&selected.to_string(), fallback_port), + resolution, + ) +} + fn client_resolution_with_address( input: &str, method: ClientResolutionMethod, @@ -1313,4 +1391,44 @@ mod tests { vec!["teamspeak.app"] ); } + + #[test] + fn explicit_ports_use_direct_dns_fast_path() { + assert!(should_return_direct_dns_before_discovery(Some(9987))); + assert!(!should_return_direct_dns_before_discovery(None)); + } + + #[tokio::test] + async fn explicit_port_client_request_uses_dns_result() { + let resolver = ChanoraResolver { + resolver: None, + http: Client::new(), + }; + + let resolved = resolver + .resolve_client_request("localhost:10075") + .await + .unwrap(); + + assert_eq!(resolved.method, ClientResolutionMethod::Dns); + assert!(resolved.address.ends_with(":10075")); + } + + #[test] + fn tsdns_discovery_is_bounded_when_dns_fallback_exists() { + assert_eq!( + tsdns_discovery_budget(true), + Some(TSDNS_FALLBACK_DISCOVERY_BUDGET) + ); + assert_eq!(tsdns_discovery_budget(false), None); + } + + #[test] + fn ts3_srv_discovery_is_bounded_when_dns_fallback_exists() { + assert_eq!( + ts3_srv_discovery_budget(true), + Some(TS3_SRV_FALLBACK_DISCOVERY_BUDGET) + ); + assert_eq!(ts3_srv_discovery_budget(false), None); + } } diff --git a/crates/chanora_server_prefetch/Cargo.toml b/crates/chanora_server_prefetch/Cargo.toml new file mode 100644 index 0000000..c2bf94c --- /dev/null +++ b/crates/chanora_server_prefetch/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "chanora_server_prefetch" +description = "Chanora — server-address prefetch cache and policy built on chanora_resolver." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[dependencies] +chanora_resolver = { path = "../chanora_resolver" } +thiserror.workspace = true +tracing.workspace = true +tokio = { version = "1", features = ["sync", "rt", "macros"] } + +[features] +test-support = [] diff --git a/crates/chanora_server_prefetch/src/lib.rs b/crates/chanora_server_prefetch/src/lib.rs new file mode 100644 index 0000000..8048696 --- /dev/null +++ b/crates/chanora_server_prefetch/src/lib.rs @@ -0,0 +1,312 @@ +//! Server-address prefetch cache and policy for Chanora. +//! +//! This crate owns speculative server-resolution warming. It does not +//! decide whether a connection should use a prefetched address; callers +//! must still apply their own trust boundary before dialing. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use thiserror::Error; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; + +const SERVER_PREFETCH_TTL: Duration = Duration::from_secs(120); + +#[derive(Debug, Error)] +pub enum ServerPrefetchError { + #[error("resolver initialization failed: {0}")] + ResolverInit(String), + #[error("resolution failed: {0}")] + Resolution(String), + #[error("resolver returned invalid socket address '{resolved}': {reason}")] + InvalidSocketAddress { resolved: String, reason: String }, +} + +#[derive(Debug, Clone)] +struct ServerPrefetchEntry { + normalized_host: String, + resolved_address: SocketAddr, + completed_at: Instant, +} + +#[derive(Debug, Default)] +struct ServerPrefetchCache { + latest_generation: u64, + entry: Option, +} + +impl ServerPrefetchCache { + fn begin(&mut self, host: &str) -> u64 { + if normalize_host(host).is_empty() { + return self.latest_generation; + } + self.latest_generation = self.latest_generation.saturating_add(1); + self.latest_generation + } + + fn store_success( + &mut self, + generation: u64, + host: &str, + resolved_address: SocketAddr, + completed_at: Instant, + ) { + if generation != self.latest_generation { + return; + } + self.entry = Some(ServerPrefetchEntry { + normalized_host: normalize_host(host), + resolved_address, + completed_at, + }); + } + + fn fresh_match(&self, host: &str, now: Instant) -> Option { + let normalized = normalize_host(host); + let entry = self.entry.as_ref()?; + if entry.normalized_host != normalized { + return None; + } + if now.duration_since(entry.completed_at) > SERVER_PREFETCH_TTL { + return None; + } + // Generation is not checked here: a fresh entry remains usable while + // a newer prefetch is in flight. Stale async completions are still + // rejected in store_success via the generation guard. + Some(entry.resolved_address) + } +} + +#[derive(Debug, Clone, Default)] +pub struct ServerPrefetcher { + cache: Arc>, + #[cfg(any(test, feature = "test-support"))] + setup_error_for_test: Arc>>, +} + +impl ServerPrefetcher { + pub fn new() -> Self { + Self::default() + } + + /// Schedules a fire-and-forget prefetch for `host`. + /// + /// The result only reports synchronous setup failures before scheduling, + /// such as resolver initialization. DNS/resolution failures after the task + /// is spawned are logged and do not complete this returned `Result`. + pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError> { + let normalized = normalize_host(&host); + if normalized.is_empty() { + return Ok(()); + } + + #[cfg(any(test, feature = "test-support"))] + if let Some(err) = self.setup_error_for_test.lock().await.take() { + return Err(ServerPrefetchError::ResolverInit(err)); + } + + let resolver = chanora_resolver::ChanoraResolver::new() + .map_err(|err| ServerPrefetchError::ResolverInit(err.to_string()))?; + + let generation = { + let mut cache = self.cache.lock().await; + cache.begin(&normalized) + }; + let cache = self.cache.clone(); + tokio::spawn(async move { + info!(target: "chanora_server_prefetch", host = %normalized, "resolution prefetch started"); + let result = resolve_socket(resolver, &normalized).await; + match result { + Ok(addr) => { + info!( + target: "chanora_server_prefetch", + host = %normalized, + resolved = %addr, + "resolution prefetch result" + ); + let mut guard = cache.lock().await; + guard.store_success(generation, &normalized, addr, Instant::now()); + } + Err(err) => { + warn!( + target: "chanora_server_prefetch", + host = %normalized, + error = %err, + "resolution prefetch failed" + ); + } + } + }); + Ok(()) + } + + pub async fn fresh_match(&self, host: &str) -> Option { + let resolved = { + let cache = self.cache.lock().await; + cache.fresh_match(host, Instant::now()) + }; + match resolved { + Some(addr) => { + info!( + target: "chanora_server_prefetch", + host = %host, + resolved = %addr, + "connect using prefetched resolution" + ); + Some(addr) + } + None => { + debug!(target: "chanora_server_prefetch", host = %host, "connect prefetch miss or stale"); + None + } + } + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn begin_for_test(&self, host: &str) -> u64 { + let mut cache = self.cache.lock().await; + cache.begin(host) + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn store_success_for_test( + &self, + generation: u64, + host: &str, + resolved_address: SocketAddr, + completed_at: Instant, + ) { + let mut cache = self.cache.lock().await; + cache.store_success(generation, host, resolved_address, completed_at); + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn latest_generation_for_test(&self) -> u64 { + let cache = self.cache.lock().await; + cache.latest_generation + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn fail_next_prefetch_setup_for_test(&self, error: impl Into) { + *self.setup_error_for_test.lock().await = Some(error.into()); + } +} + +fn normalize_host(host: &str) -> String { + host.trim().to_lowercase() +} + +async fn resolve_socket( + resolver: chanora_resolver::ChanoraResolver, + host: &str, +) -> Result { + let resolved = resolver + .resolve_client_address(host) + .await + .map_err(|err| ServerPrefetchError::Resolution(err.to_string()))?; + resolved + .parse::() + .map_err(|err| ServerPrefetchError::InvalidSocketAddress { + resolved, + reason: err.to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn fresh_exact_match_returns_socket_address() { + let prefetcher = ServerPrefetcher::new(); + let generation = prefetcher.begin_for_test(" Example.COM ").await; + let addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(generation, "example.com", addr, Instant::now()) + .await; + + assert_eq!(prefetcher.fresh_match("example.com").await, Some(addr)); + assert_eq!(prefetcher.fresh_match(" EXAMPLE.com ").await, Some(addr)); + } + + #[tokio::test] + async fn stale_entries_are_ignored() { + let prefetcher = ServerPrefetcher::new(); + let generation = prefetcher.begin_for_test("example.com").await; + let addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test( + generation, + "example.com", + addr, + Instant::now() - SERVER_PREFETCH_TTL - Duration::from_secs(1), + ) + .await; + + assert_eq!(prefetcher.fresh_match("example.com").await, None); + } + + #[tokio::test] + async fn different_hosts_are_ignored() { + let prefetcher = ServerPrefetcher::new(); + let generation = prefetcher.begin_for_test("example.com").await; + let addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(generation, "example.com", addr, Instant::now()) + .await; + + assert_eq!(prefetcher.fresh_match("other.example.com").await, None); + } + + #[tokio::test] + async fn stale_generation_completions_are_ignored() { + let prefetcher = ServerPrefetcher::new(); + let old_generation = prefetcher.begin_for_test("old.example.com").await; + let _new_generation = prefetcher.begin_for_test("new.example.com").await; + let old_addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(old_generation, "old.example.com", old_addr, Instant::now()) + .await; + + assert_eq!(prefetcher.fresh_match("old.example.com").await, None); + } + + #[tokio::test] + async fn fresh_entry_usable_during_warming() { + let prefetcher = ServerPrefetcher::new(); + let first_generation = prefetcher.begin_for_test("example.com").await; + let cached_addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(first_generation, "example.com", cached_addr, Instant::now()) + .await; + + let _pending_generation = prefetcher.begin_for_test("example.com").await; + + assert_eq!( + prefetcher.fresh_match("example.com").await, + Some(cached_addr) + ); + + let stale_addr = "127.0.0.2:9987".parse().unwrap(); + prefetcher + .store_success_for_test(first_generation, "example.com", stale_addr, Instant::now()) + .await; + + assert_eq!( + prefetcher.fresh_match("example.com").await, + Some(cached_addr) + ); + } + + #[tokio::test] + async fn blank_host_skips_prefetch() { + let prefetcher = ServerPrefetcher::new(); + let before = prefetcher.latest_generation_for_test().await; + + prefetcher.prefetch(" ".to_string()).await.unwrap(); + + assert_eq!(prefetcher.latest_generation_for_test().await, before); + } +} diff --git a/crates/chanora_state/Cargo.toml b/crates/chanora_state/Cargo.toml index e8e78f3..f366fbc 100644 --- a/crates/chanora_state/Cargo.toml +++ b/crates/chanora_state/Cargo.toml @@ -11,3 +11,4 @@ publish.workspace = true [dependencies] thiserror.workspace = true +chanora_protocol = { path = "../chanora_protocol" } diff --git a/crates/chanora_state/src/lib.rs b/crates/chanora_state/src/lib.rs index 6b5c8c2..978284f 100644 --- a/crates/chanora_state/src/lib.rs +++ b/crates/chanora_state/src/lib.rs @@ -4,17 +4,26 @@ //! channel tree, client list, our own connection state. Owns the //! reducers that fold protocol events into a snapshot, and the //! deltas that the bridge publishes to Flutter view-models -//! (per SAD §7.2 and SDD §5). +//! (per SAD 7.2 and SDD 5). //! -//! ## Status +//! ## Reducer contract //! -//! Scaffold only. +//! Reducers mutate the caller-owned state passed by `&mut` and return a +//! `Reduction` containing only the emitted `Delta` values. The caller +//! (typically `chanora_core`) owns state storage, publishes deltas to the +//! bridge, and executes any side effects. +//! +//! This design satisfies SRS-056 (deterministic deltas), SRS-057 +//! (per-connection ordering), and SRS-058 (reducer functions). #![forbid(unsafe_code)] #![warn(missing_docs)] pub mod channel_join; +use std::collections::HashMap; + +use chanora_protocol::{ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ServerSnapshot}; use thiserror::Error; /// Errors raised while reducing protocol events into state or @@ -24,8 +33,7 @@ pub enum StateError { /// Reducer received an event referencing an unknown entity. #[error("unknown entity: {0}")] Unknown(&'static str), - /// A reducer invariant was violated (e.g. two clients claiming - /// the same id). + /// A reducer invariant was violated (e.g. two clients claiming the same id). #[error("state invariant: {0}")] Invariant(&'static str), } @@ -39,16 +47,773 @@ pub enum ConnectionState { Connecting, /// Initial state snapshot has been received; ready for use. Ready, + /// Connection lost; reconnecting. + Reconnecting, /// Connection lost; will not auto-reconnect at this stage. Lost, } +/// Authoritative mirror of a connected server's published state. +/// +/// Constructed from a [`ServerSnapshot`] and updated through delta +/// events. Owns the channel tree, client list, and server metadata. +/// Satisfies SRS-054. +#[derive(Debug, Clone)] +pub struct ServerState { + /// Retained connection lifecycle state. + pub connection_state: ConnectionState, + /// Server name from the most recent snapshot. + pub server_name: String, + /// Welcome message / banner (server-provided markup). + pub welcome_message: String, + /// Server platform string. + pub platform: String, + /// Server version string. + pub version: String, + channels: HashMap, + clients: HashMap, + channel_order: Vec, + client_order: Vec, + /// Our own client id, as the server reported it. + pub own_client_id: u64, +} + +impl ServerState { + /// Build state from the initial full snapshot. Satisfies SRS-055. + pub fn from_snapshot(snapshot: ServerSnapshot) -> Self { + let snapshot = normalize_snapshot(snapshot); + let channels = snapshot + .channels + .iter() + .map(|c| (c.id.0, c.clone())) + .collect(); + let clients = snapshot + .clients + .iter() + .map(|c| (c.id.0, c.clone())) + .collect(); + let channel_order: Vec = snapshot.channels.iter().map(|c| c.id.0).collect(); + let mut client_order: Vec = snapshot.clients.iter().map(|c| c.id.0).collect(); + client_order.sort_unstable(); + client_order.dedup(); + Self { + connection_state: ConnectionState::Ready, + server_name: snapshot.server_name, + welcome_message: snapshot.welcome_message, + platform: snapshot.platform, + version: snapshot.version, + channels, + clients, + channel_order, + client_order, + own_client_id: snapshot.own_client_id, + } + } + + /// Replace state with a fresh snapshot (post-reconnect). Satisfies SRS-059. + pub fn replace_from_snapshot(&mut self, snapshot: ServerSnapshot) { + *self = Self::from_snapshot(snapshot); + } + + /// Look up a channel by id. + pub fn channel(&self, id: ChannelId) -> Option<&ChannelInfo> { + self.channels.get(&id.0) + } + + /// Look up a client by id. + pub fn client(&self, id: ClientId) -> Option<&ClientInfo> { + self.clients.get(&id.0) + } + + /// All channels in protocol snapshot order. Live inserts are appended + /// until the next full snapshot re-normalizes order. + pub fn channels(&self) -> impl Iterator { + self.channel_order + .iter() + .filter_map(|id| self.channels.get(id)) + } + + /// All clients in stable reducer order. + /// + /// Full snapshots normalize clients by id to avoid inheriting upstream + /// HashMap iteration order. Live inserts are appended in arrival order + /// until the next full snapshot re-normalizes the list. + pub fn clients(&self) -> impl Iterator { + self.client_order + .iter() + .filter_map(|id| self.clients.get(id)) + } + + /// Number of channels. + pub fn channel_count(&self) -> usize { + self.channels.len() + } + + /// Number of clients. + pub fn client_count(&self) -> usize { + self.clients.len() + } + + /// The channel our own client is currently in. + pub fn own_channel(&self) -> Option<&ChannelInfo> { + self.client(ClientId(self.own_client_id)) + .and_then(|c| self.channel(c.channel)) + } + + /// Clients in a given channel. + pub fn clients_in_channel(&self, channel_id: ChannelId) -> impl Iterator { + self.client_order + .iter() + .filter_map(|id| self.clients.get(id)) + .filter(move |c| c.channel == channel_id) + } +} + +fn normalize_snapshot(snapshot: ServerSnapshot) -> ServerSnapshot { + let mut channels = HashMap::new(); + let mut channel_order = Vec::new(); + for channel in snapshot.channels { + if !channels.contains_key(&channel.id.0) { + channel_order.push(channel.id.0); + } + channels.insert(channel.id.0, channel); + } + let mut clients = HashMap::new(); + for client in snapshot.clients { + clients.insert(client.id.0, client); + } + let mut client_order: Vec = clients.keys().copied().collect(); + client_order.sort_unstable(); + + ServerSnapshot { + server_name: snapshot.server_name, + welcome_message: snapshot.welcome_message, + platform: snapshot.platform, + version: snapshot.version, + channels: channel_order + .into_iter() + .filter_map(|id| channels.remove(&id)) + .collect(), + clients: client_order + .into_iter() + .filter_map(|id| clients.remove(&id)) + .collect(), + own_client_id: snapshot.own_client_id, + } +} + +/// A change to the server state that the bridge should publish to +/// Flutter. Deltas are cheap to construct and carry only the +/// information that changed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Delta { + /// A full snapshot was applied; the entire UI should refresh. + SnapshotApplied(ServerSnapshot), + /// A channel was added or updated. + ChannelUpserted(ChannelInfo), + /// A channel was removed. + ChannelRemoved(ChannelId), + /// A client joined or was updated (includes moves). + ClientUpserted(ClientInfo), + /// A client left. + ClientRemoved(ClientId), + /// A chat message arrived. + ChatMessage(ChatMessage), + /// A client's speaking state changed. + VoiceActivityChanged { + /// Client whose voice activity changed. + client_id: ClientId, + /// Whether voice activity is currently observed. + is_speaking: bool, + }, + /// The connection state changed. + ConnectionStateChanged(ConnectionState), +} + +/// Events that flow from the protocol layer into the state reducer. +/// Each variant carries all data the reducer needs; no external +/// lookups are required. +#[derive(Debug, Clone)] +pub enum StateEvent { + /// Initial or post-reconnect snapshot arrived. + Snapshot(ServerSnapshot), + /// A new channel appeared or an existing channel was updated. + ChannelChanged(ChannelInfo), + /// A channel was deleted. + ChannelDeleted(ChannelId), + /// A client joined or was updated (including channel moves and + /// mute/speaking state changes). + ClientChanged(ClientInfo), + /// A client left the server. + ClientLeft(ClientId), + /// A text message was received. + ChatReceived(ChatMessage), + /// A client's speaking state changed. + VoiceActivityChanged { + /// Client whose voice activity changed. + client_id: ClientId, + /// Whether voice activity is currently observed. + is_speaking: bool, + }, + /// Connection lifecycle transition. + ConnectionChanged(ConnectionState), + /// Reconnect started; stale state must be discarded. + ReconnectStarted, +} + +/// Result of applying a single event through the reducer. +#[derive(Debug, Clone)] +pub struct Reduction { + /// Deltas describing what changed, in application order. + pub deltas: Vec, +} + +/// Apply a [`StateEvent`] to optional [`ServerState`], returning +/// the resulting [`Reduction`]. +/// +/// The state is taken by `&mut` so the caller retains ownership. +/// When `state` is `None` (no active connection), most events are +/// ignored except `ConnectionChanged` and `Snapshot` (which creates +/// the state). +/// +/// Deterministic: the same `(state, event)` pair always produces the +/// same `Reduction`. Satisfies SRS-056. +pub fn reduce(state: &mut Option, event: StateEvent) -> Reduction { + match event { + StateEvent::ConnectionChanged(cs) => { + if let Some(s) = state { + s.connection_state = cs; + } + Reduction { + deltas: vec![Delta::ConnectionStateChanged(cs)], + } + } + + StateEvent::ReconnectStarted => { + *state = None; + Reduction { + deltas: vec![Delta::ConnectionStateChanged(ConnectionState::Reconnecting)], + } + } + + StateEvent::Snapshot(snap) => { + let normalized = normalize_snapshot(snap); + *state = Some(ServerState::from_snapshot(normalized.clone())); + Reduction { + deltas: vec![ + Delta::ConnectionStateChanged(ConnectionState::Ready), + Delta::SnapshotApplied(normalized), + ], + } + } + + StateEvent::ChannelChanged(info) => match state { + Some(s) if s.connection_state == ConnectionState::Ready => { + if !s.channels.contains_key(&info.id.0) { + s.channel_order.push(info.id.0); + } + s.channels.insert(info.id.0, info.clone()); + Reduction { + deltas: vec![Delta::ChannelUpserted(info)], + } + } + _ => Reduction { deltas: vec![] }, + }, + + StateEvent::ChannelDeleted(id) => match state { + Some(s) if s.connection_state == ConnectionState::Ready => { + let removed_clients: Vec = s + .client_order + .iter() + .copied() + .filter(|client_id| { + s.clients + .get(client_id) + .is_some_and(|client| client.channel == id) + }) + .collect(); + for client_id in &removed_clients { + s.clients.remove(client_id); + } + s.client_order + .retain(|existing| !removed_clients.contains(existing)); + let removed_channel = s.channels.remove(&id.0).is_some(); + s.channel_order.retain(|existing| *existing != id.0); + let mut deltas: Vec = removed_clients + .into_iter() + .map(|client_id| Delta::ClientRemoved(ClientId(client_id))) + .collect(); + if removed_channel { + deltas.push(Delta::ChannelRemoved(id)); + } + Reduction { + deltas, + } + } + _ => Reduction { deltas: vec![] }, + }, + + StateEvent::ClientChanged(info) => match state { + Some(s) if s.connection_state == ConnectionState::Ready => { + if !s.clients.contains_key(&info.id.0) { + s.client_order.push(info.id.0); + } + s.clients.insert(info.id.0, info.clone()); + Reduction { + deltas: vec![Delta::ClientUpserted(info)], + } + } + _ => Reduction { deltas: vec![] }, + }, + + StateEvent::ClientLeft(id) => match state { + Some(s) if s.connection_state == ConnectionState::Ready => { + s.clients.remove(&id.0); + s.client_order.retain(|existing| *existing != id.0); + Reduction { + deltas: vec![Delta::ClientRemoved(id)], + } + } + _ => Reduction { deltas: vec![] }, + }, + + StateEvent::ChatReceived(msg) => match state { + Some(s) if s.connection_state == ConnectionState::Ready => Reduction { + deltas: vec![Delta::ChatMessage(msg)], + }, + _ => Reduction { deltas: vec![] }, + }, + + StateEvent::VoiceActivityChanged { + client_id, + is_speaking, + } => match state { + Some(s) if s.connection_state == ConnectionState::Ready => { + if let Some(client) = s.clients.get_mut(&client_id.0) { + client.is_speaking = is_speaking; + Reduction { + deltas: vec![Delta::VoiceActivityChanged { + client_id, + is_speaking, + }], + } + } else { + Reduction { deltas: vec![] } + } + } + _ => Reduction { deltas: vec![] }, + }, + } +} + +/// Replace server state with a fresh snapshot after a reconnect. +/// Discards all previous state unconditionally (SRS-059). +pub fn reduce_reconnect_snapshot( + state: &mut Option, + snap: ServerSnapshot, +) -> Reduction { + let normalized = normalize_snapshot(snap); + *state = Some(ServerState::from_snapshot(normalized.clone())); + Reduction { + deltas: vec![ + Delta::ConnectionStateChanged(ConnectionState::Ready), + Delta::SnapshotApplied(normalized), + ], + } +} + #[cfg(test)] mod tests { use super::*; + use chanora_protocol::MessageTarget; + + fn sample_channel(id: u64) -> ChannelInfo { + ChannelInfo { + id: ChannelId(id), + parent: ChannelId(0), + name: format!("channel-{id}"), + order: 0, + has_password: false, + needed_talk_power: None, + } + } + + fn sample_client(id: u64, channel: u64) -> ClientInfo { + ClientInfo { + id: ClientId(id), + channel: ChannelId(channel), + name: format!("client-{id}"), + input_muted: false, + output_muted: false, + is_speaking: false, + is_server_query: false, + talk_power: 0, + talk_power_granted: false, + } + } + + fn sample_snapshot() -> ServerSnapshot { + ServerSnapshot { + server_name: "Test Server".into(), + welcome_message: String::new(), + platform: "Linux".into(), + version: "3.13".into(), + channels: vec![sample_channel(1), sample_channel(2)], + clients: vec![sample_client(10, 1)], + own_client_id: 10, + } + } #[test] - fn state_transitions_compile() { - let _ = ConnectionState::Idle; + fn snapshot_creates_state() { + let mut state = None; + let snapshot = sample_snapshot(); + let reduction = reduce(&mut state, StateEvent::Snapshot(snapshot.clone())); + assert!(state.is_some()); + let s = state.as_ref().unwrap(); + assert_eq!(s.connection_state, ConnectionState::Ready); + assert_eq!(s.channel_count(), 2); + assert_eq!(s.client_count(), 1); + assert_eq!(s.own_client_id, 10); + assert_eq!( + reduction.deltas, + vec![ + Delta::ConnectionStateChanged(ConnectionState::Ready), + Delta::SnapshotApplied(snapshot), + ] + ); + } + + #[test] + fn channel_upsert_adds_and_updates() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let ch = ChannelInfo { + id: ChannelId(3), + parent: ChannelId(1), + name: "new-channel".into(), + order: 1, + has_password: true, + needed_talk_power: Some(50), + }; + let reduction = reduce(&mut state, StateEvent::ChannelChanged(ch.clone())); + let s = state.as_ref().unwrap(); + assert_eq!(s.channel_count(), 3); + assert!(s.channel(ChannelId(3)).is_some()); + assert!(matches!(&reduction.deltas[..], [Delta::ChannelUpserted(_)])); + let updated = ChannelInfo { + name: "renamed".into(), + ..ch + }; + reduce(&mut state, StateEvent::ChannelChanged(updated)); + assert_eq!( + state.as_ref().unwrap().channel(ChannelId(3)).unwrap().name, + "renamed" + ); + assert_eq!(state.as_ref().unwrap().channel_count(), 3); + } + + #[test] + fn channel_delete_removes() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce(&mut state, StateEvent::ChannelDeleted(ChannelId(2))); + let s = state.as_ref().unwrap(); + assert_eq!(s.channel_count(), 1); + assert!(s.channel(ChannelId(2)).is_none()); + assert!(matches!(&reduction.deltas[..], [Delta::ChannelRemoved(_)])); + } + + #[test] + fn channel_delete_removes_clients_in_deleted_channel() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + reduce(&mut state, StateEvent::ClientChanged(sample_client(20, 2))); + reduce(&mut state, StateEvent::ClientChanged(sample_client(30, 2))); + + let reduction = reduce(&mut state, StateEvent::ChannelDeleted(ChannelId(2))); + let s = state.as_ref().unwrap(); + + assert!(s.channel(ChannelId(2)).is_none()); + assert!(s.client(ClientId(20)).is_none()); + assert!(s.client(ClientId(30)).is_none()); + assert_eq!(s.client_count(), 1); + assert_eq!(s.clients_in_channel(ChannelId(2)).count(), 0); + assert_eq!( + reduction.deltas, + vec![ + Delta::ClientRemoved(ClientId(20)), + Delta::ClientRemoved(ClientId(30)), + Delta::ChannelRemoved(ChannelId(2)), + ] + ); + } + + #[test] + fn client_upsert_adds_and_moves() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let new_client = sample_client(20, 2); + let reduction = reduce(&mut state, StateEvent::ClientChanged(new_client)); + let s = state.as_ref().unwrap(); + assert_eq!(s.client_count(), 2); + assert!(matches!(&reduction.deltas[..], [Delta::ClientUpserted(_)])); + let moved = ClientInfo { + channel: ChannelId(2), + ..sample_client(10, 2) + }; + reduce(&mut state, StateEvent::ClientChanged(moved)); + assert_eq!( + state + .as_ref() + .unwrap() + .client(ClientId(10)) + .unwrap() + .channel, + ChannelId(2) + ); + } + + #[test] + fn client_leave_removes() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce(&mut state, StateEvent::ClientLeft(ClientId(10))); + assert_eq!(state.as_ref().unwrap().client_count(), 0); + assert!(matches!(&reduction.deltas[..], [Delta::ClientRemoved(_)])); + } + + #[test] + fn reconnect_discards_stale_state() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + assert_eq!(state.as_ref().unwrap().channel_count(), 2); + let reduction = reduce(&mut state, StateEvent::ReconnectStarted); + assert!(state.is_none()); + assert!(matches!( + &reduction.deltas[..], + [Delta::ConnectionStateChanged(ConnectionState::Reconnecting)] + )); + let snap2 = ServerSnapshot { + server_name: "New Server".into(), + channels: vec![sample_channel(100)], + clients: vec![sample_client(200, 100)], + ..sample_snapshot() + }; + reduce_reconnect_snapshot(&mut state, snap2); + let s = state.as_ref().unwrap(); + assert_eq!(s.server_name, "New Server"); + assert_eq!(s.channel_count(), 1); + assert_eq!(s.client_count(), 1); + } + + #[test] + fn deltas_ignored_when_disconnected() { + let mut state: Option = None; + let r1 = reduce(&mut state, StateEvent::ChannelChanged(sample_channel(1))); + assert!(r1.deltas.is_empty()); + let r2 = reduce(&mut state, StateEvent::ClientChanged(sample_client(1, 1))); + assert!(r2.deltas.is_empty()); + } + + #[test] + fn chat_message_requires_connected_state() { + let mut state: Option = None; + let msg = ChatMessage { + sender_id: ClientId(10), + sender_name: "Alice".into(), + message: "hello".into(), + target: MessageTarget::Channel, + }; + let disconnected = reduce(&mut state, StateEvent::ChatReceived(msg.clone())); + assert!(disconnected.deltas.is_empty()); + + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + reduce( + &mut state, + StateEvent::ConnectionChanged(ConnectionState::Lost), + ); + let lost = reduce(&mut state, StateEvent::ChatReceived(msg.clone())); + assert!(lost.deltas.is_empty()); + + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce(&mut state, StateEvent::ChatReceived(msg)); + assert!(matches!(&reduction.deltas[..], [Delta::ChatMessage(_)])); + } + + #[test] + fn connection_state_change_produces_delta() { + let mut state: Option = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce( + &mut state, + StateEvent::ConnectionChanged(ConnectionState::Connecting), + ); + assert_eq!( + state.as_ref().unwrap().connection_state, + ConnectionState::Connecting + ); + assert!(matches!( + &reduction.deltas[..], + [Delta::ConnectionStateChanged(ConnectionState::Connecting)] + )); + } + + #[test] + fn snapshot_order_is_deterministic_and_live_inserts_append() { + let mut state = None; + let snapshot = ServerSnapshot { + channels: vec![sample_channel(20), sample_channel(10)], + clients: vec![sample_client(30, 20), sample_client(10, 10)], + ..sample_snapshot() + }; + reduce(&mut state, StateEvent::Snapshot(snapshot)); + reduce(&mut state, StateEvent::ChannelChanged(sample_channel(15))); + reduce(&mut state, StateEvent::ClientChanged(sample_client(20, 2))); + let s = state.as_ref().unwrap(); + let channel_ids: Vec = s.channels().map(|c| c.id.0).collect(); + let client_ids: Vec = s.clients().map(|c| c.id.0).collect(); + assert_eq!(channel_ids, vec![20, 10, 15]); + assert_eq!(client_ids, vec![10, 30, 20]); + } + + #[test] + fn malformed_snapshot_duplicate_ids_are_deduplicated_in_order_vectors() { + let mut snapshot = sample_snapshot(); + snapshot.channels.push(ChannelInfo { + name: "duplicate".into(), + ..sample_channel(1) + }); + snapshot.clients.push(ClientInfo { + name: "duplicate".into(), + ..sample_client(10, 2) + }); + + let mut state = None; + let reduction = reduce(&mut state, StateEvent::Snapshot(snapshot)); + let s = state.as_ref().unwrap(); + + assert_eq!(s.channel_count(), 2); + assert_eq!(s.channels().count(), 2); + assert_eq!(s.client_count(), 1); + assert_eq!(s.clients().count(), 1); + assert_eq!(s.channel(ChannelId(1)).unwrap().name, "duplicate"); + assert_eq!(s.client(ClientId(10)).unwrap().channel, ChannelId(2)); + let Delta::SnapshotApplied(emitted) = &reduction.deltas[1] else { + panic!("expected snapshot delta"); + }; + assert_eq!(emitted.channels.len(), 2); + assert_eq!(emitted.clients.len(), 1); + } + + #[test] + fn voice_activity_updates_existing_client_and_emits_delta() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce( + &mut state, + StateEvent::VoiceActivityChanged { + client_id: ClientId(10), + is_speaking: true, + }, + ); + assert!( + state + .as_ref() + .unwrap() + .client(ClientId(10)) + .unwrap() + .is_speaking + ); + assert_eq!( + reduction.deltas, + vec![Delta::VoiceActivityChanged { + client_id: ClientId(10), + is_speaking: true, + }] + ); + } + + #[test] + fn voice_activity_for_unknown_client_is_ignored() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce( + &mut state, + StateEvent::VoiceActivityChanged { + client_id: ClientId(999), + is_speaking: true, + }, + ); + assert!(reduction.deltas.is_empty()); + } + + #[test] + fn own_channel_returns_correct_channel() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let own = state.as_ref().unwrap().own_channel(); + assert!(own.is_some()); + assert_eq!(own.unwrap().id, ChannelId(1)); + } + + #[test] + fn clients_in_channel_filters_correctly() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + reduce(&mut state, StateEvent::ClientChanged(sample_client(20, 1))); + reduce(&mut state, StateEvent::ClientChanged(sample_client(30, 2))); + let s = state.as_ref().unwrap(); + assert_eq!(s.clients_in_channel(ChannelId(1)).count(), 2); + assert_eq!(s.clients_in_channel(ChannelId(2)).count(), 1); + } + + #[test] + fn malformed_delete_is_tolerated() { + let mut state = None; + reduce(&mut state, StateEvent::Snapshot(sample_snapshot())); + let reduction = reduce(&mut state, StateEvent::ChannelDeleted(ChannelId(999))); + assert!(reduction.deltas.is_empty()); + assert_eq!(state.as_ref().unwrap().channel_count(), 2); + } + + #[test] + fn same_event_sequence_produces_same_state() { + let mut a: Option = None; + let mut b: Option = None; + let events: Vec = vec![ + StateEvent::Snapshot(sample_snapshot()), + StateEvent::ClientChanged(sample_client(20, 2)), + StateEvent::ChannelChanged(ChannelInfo { + id: ChannelId(3), + parent: ChannelId(1), + name: "x".into(), + order: 0, + has_password: false, + needed_talk_power: None, + }), + StateEvent::ClientLeft(ClientId(20)), + ]; + let mut deltas_a = Vec::new(); + let mut deltas_b = Vec::new(); + for e in &events { + deltas_a.extend(reduce(&mut a, e.clone()).deltas); + } + for e in &events { + deltas_b.extend(reduce(&mut b, e.clone()).deltas); + } + assert_eq!( + a.as_ref().unwrap().channel_count(), + b.as_ref().unwrap().channel_count() + ); + assert_eq!( + a.as_ref().unwrap().client_count(), + b.as_ref().unwrap().client_count() + ); + assert_eq!( + a.as_ref().unwrap().own_client_id, + b.as_ref().unwrap().own_client_id + ); + assert_eq!(deltas_a, deltas_b); } } diff --git a/deny.toml b/deny.toml index 54b38f2..3336c2d 100644 --- a/deny.toml +++ b/deny.toml @@ -37,6 +37,7 @@ allow = [ "BSL-1.0", "Unicode-3.0", "CC0-1.0", + "CDLA-Permissive-2.0", ] confidence-threshold = 0.93 @@ -61,7 +62,13 @@ yanked = "warn" # Empty for v1.0.0-rc.1; if a future advisory blocks the release # without a workspace fix, add it here with a tracking issue link # and a date. -ignore = [] +ignore = [ + # RUSTSEC-2026-0150: audiopus_sys is unmaintained and references + # old CMake behavior. Chanora currently patches the `cmake` crate + # at workspace level to keep builds working while Opus dependency + # replacement is tracked as a release/supply-chain limitation. + "RUSTSEC-2026-0150", +] # ---------- Bans (versions / packages) ---------- @@ -83,6 +90,7 @@ unknown-git = "warn" # `Cargo.toml`. Allow the upstream and its workspace members. allow-git = [ "https://github.com/ReSpeak/tsclientlib.git", + "https://github.com/EdisonJwa/oboe-rs", ] [sources.allow-org] diff --git a/docs/00_AGENT_ROUTER.md b/docs/00_AGENT_ROUTER.md deleted file mode 100644 index 2df47a6..0000000 --- a/docs/00_AGENT_ROUTER.md +++ /dev/null @@ -1,31 +0,0 @@ -# AI Agent Router — Chanora Minimal Spec - -Purpose: keep AI context small. Do not load the old full bundle unless explicitly asked for audit reconstruction. - -## Load Order -1. Always load this file first. -2. Load exactly one phase file: `P0_MVP_AGENT_SPEC.md`, `P1_BETA_AGENT_SPEC.md`, or `P2_PRODUCTION_AGENT_SPEC.md`. -3. Load `SOP_AGENT_OPERATIONS.md` only when changing architecture, release, security, privacy, diagnostics, build, or test gates. -4. Use `phase_index.json` for machine lookup. Do not paste full source docs into context. - -## Phase Meaning -- P0 = MVP buildable product. Default implementation target. -- P1 = beta hardening and extended quality. Do not implement unless asked. -- P2 = production/public-release readiness. Do not implement until P0 and P1 gates pass. -- SOP = standing operating rules for agents, reviews, testing, release, traceability, and refusal conditions. - -## Non-Negotiable Product Contract -- Product: Chanora, a cross-platform TeamSpeak 3-compatible client application. -- Stack: Flutter UI + Rust core + typed Flutter/Rust bridge + `tsclientlib` adapter. -- Platforms: Windows, macOS, Linux, Android, iOS. -- MVP server model: external TS3-compatible servers only; no Chanora-operated backend. -- MVP connection model: one active server connection per client instance. -- Privacy: no telemetry, no automatic diagnostic upload, no crash reporting unless later approved. -- Diagnostics: local, user-initiated export only. -- Storage: secrets in platform secure storage; non-secrets in bundled SQLite via `rusqlite`. -- Localization: English + Chinese Simplified at MVP; preserve server content verbatim. -- Design: Material 3 + Chanora Design System. -- License: Apache-2.0 OR MIT. - -## Hard Stop Rules -Stop and ask for explicit approval before adding: telemetry, crash reporting, automatic upload, multiple active server connections, a Chanora backend, unsupported platform targets, unapproved hotkey/audio packages, or legal/trademark wording changes. diff --git a/docs/P0_MVP_AGENT_SPEC.md b/docs/P0_MVP_AGENT_SPEC.md deleted file mode 100644 index d2c8a37..0000000 --- a/docs/P0_MVP_AGENT_SPEC.md +++ /dev/null @@ -1,136 +0,0 @@ -# P0 MVP Agent Spec — Build This First - -## Objective -Create the smallest complete Chanora MVP that can connect to one TS3-compatible server, render channel/user state, support basic voice workflow, persist local non-secret settings, protect secrets, and pass platform P0 acceptance gates. - -## Scope -Implement only P0/MVP. Treat P1/P2 items as backlog unless the user explicitly requests them. - -## Accepted MVP Decisions -- DEC-001: Release type = Internal Alpha first, then External Beta, then MVP Public / Store Release -- DEC-002: MVP platform scope = MVP target remains Windows, macOS, Linux, Android, and iOS; first release may be staged by channel/platform -- DEC-003: Minimum iOS version = iOS 13 minimum for Flutter support baseline; test latest iOS release separately -- DEC-004: Minimum Android version = **Android API 28 (Android 9.0)** minimum, raised from the original recommendation of API 24 by explicit owner ruling on 2026-05-14. Rationale: simplifies the audio path (AAudio is unconditionally available from API 26+ and stable from API 28), narrows the TLS / privacy / scoped-storage compatibility surface, and matches typical 2026 Android baselines. The original cpal-on-Oboe Android spike was built with `minSdk = 24` and `cargo-ndk -P 26`; product code in `apps/chanora_flutter` uses `minSdk = 28` and the Android product audio backend is now direct Oboe, so Chanora no longer carries the API 24/25 OpenSL ES fallback branch. -- DEC-005: Android target SDK = Target the Google Play-required API level on the upload date; current release gate uses API 35+ unless newer Google policy applies -- DEC-006: Multiple server connections in MVP = Not in MVP; support one active server connection per client instance -- DEC-007: AEC default state = Enabled by default on platforms/audio backends where supported and stable -- DEC-008: AGC default state = Enabled by default, with user setting to disable -- DEC-009: Noise suppression default state = Enabled by default, with user setting to disable -- DEC-010: High-pass filter default state = Enabled by default -- DEC-011: Audio processing implementation path = Use platform-native audio processing first where available; use Rust/WebRTC-style processing as controlled fallback or later architecture option -- DEC-011.1: Audio crate choice = `cpal` for desktop (empirically verified on Linux/PipeWire by `poc/audio-capture-playback-spike` on 2026-05-13) and direct `oboe-rs` for Android product code. The earlier cpal-on-Oboe spike remains evidence that Rust audio reaches Oboe/AAudio on a Motorola Moto G Stylus 5G (2023) running Android 14 arm64-v8a, but product code chooses direct Oboe because P0 Android needs explicit `VoiceCommunication` input preset, `VoiceCommunication`/`Speech` output attributes, low-latency/exclusive stream requests with shared fallback, audio-session ids for hardware AEC/NS/AGC binding, and diagnostics of requested/achieved stream values. iOS crate TBD pending an iOS spike that requires macOS + Xcode hardware -- DEC-012: Official SDK / trademark / licensing review = Public/store release is blocked until legal confirms TeamSpeak non-affiliation wording, trademark usage, OSS licenses, and `tsclientlib` license posture -- DEC-013: Local database choice = Use SQLite or equivalent embedded local database for non-secret local state; secrets remain in platform secure storage -- DEC-013.1: SQLite crate = `rusqlite` with the `bundled` feature (SQLite statically linked into the binary; no system libsqlite3 dependency); verified by `poc/sqlite-storage-spike` on 2026-05-13 -- DEC-013.2: Linux secure-storage backend policy = Prefer Secret Service (libsecret / gnome-keyring / kwallet / KeePassXC) on Linux; if the default collection is locked or D-Bus is unavailable, fall back to kernel keyutils with a clear user notice. Both backends are "equivalent" per SysRS-053 / SysRS-162; verified by `poc/secure-storage-spike` on 2026-05-13 -- DEC-014: Bridge choice = Use a stable typed Flutter/Rust bridge with generated or schema-controlled DTOs; **`flutter_rust_bridge` 2.x pinned** (empirically verified at 2.12.0 by `poc/flutter_rust_bridge_hello` on 2026-05-13) -- DEC-015: Product language for MVP = **English + Chinese (Simplified) for MVP**, raised from the original recommendation of English-only by explicit owner ruling on 2026-05-14. Rationale: the demonstrated test-server population (verified live against `cn.teamspeak.app`) and broader TS3 audience include substantial Chinese-speaking users; shipping zh-Hans alongside en at MVP avoids a launch-window UX gap. Architecture remains i18n-ready so additional languages can be added later mechanically. Server-provided content is preserved verbatim and never translated (ADR-008 UTF-8 boundary, DEC-015 server-content rule retained). -- DEC-016: Diagnostics upload policy = No automatic upload for MVP; user-initiated local diagnostic export only -- DEC-017: Crash reporting = Disabled for MVP unless explicit opt-in provider and privacy policy are approved -- DEC-018: Public product name = Chanora -- DEC-021: Apple App Store submission SDK = Use Xcode 26 or later and the iOS 26 / iPadOS 26 SDK or later for App Store submission on or after 2026-04-28, unless Apple publishes a newer applicable requirement before upload -- DEC-020: License model = **Dual-licensed under Apache-2.0 OR MIT (recipient's choice)**, the standard Rust-ecosystem permissive license model. Accepted on 2026-05-14. Compatible with every direct dependency in the PoC tree (`tsclientlib` MIT-OR-Apache-2.0, `flutter_rust_bridge` MIT, `cpal` Apache-2.0, `rusqlite` MIT, `keyring` MIT-OR-Apache-2.0, etc.) and with the Flutter framework's BSD-3-Clause. The license texts ship as `LICENSE-APACHE` and `LICENSE-MIT` at the repository root; an aggregator `LICENSE` points to both. `NOTICE` enumerates current direct-dependency attributions. The full OSS legal review (transitive deps, license obligations, OSS notices) remains under DEC-012 and is still required before public release. -- DEC-022: Canonical implementation directory layout = Accept the README's sketch as canonical: `apps/chanora_flutter/`, `core/chanora_core/`, `crates/chanora_protocol/`, `crates/chanora_audio/`, `crates/chanora_state/`, `crates/chanora_storage/`, `crates/chanora_diagnostics/`, `crates/chanora_bridge/`. Matches SAD §7.2 module decomposition -- DEC-023: Windows desktop Global PTT priority = **P0 in MVP.** Resolves PTT-OPEN-001 from `gen2/chanora-desktop-ptt-review-summary-v0.9.2.md`. The Windows backend ladder (Raw Input → low-level keyboard hook → Focused fallback) is mandatory for the first public release; release notes shall not claim Global PTT support on Windows until live measurement confirms a Global level from a non-fallback rung. -- DEC-024: macOS desktop Global PTT priority = **P0 in MVP, with explicit permission UX flow.** Resolves PTT-OPEN-002. The macOS backend shall request the Input Monitoring / Accessibility permission, accept user denial gracefully (continue at `L0Focused` without functional regression), and upgrade to Global asynchronously when the user grants the permission. -- DEC-025: Officially-tested Linux environment for first public release = **GNOME on Wayland (only).** Resolves PTT-OPEN-003. The Linux backend shall use the freedesktop `org.freedesktop.portal.GlobalShortcuts` interface on GNOME-on-Wayland and fall back to Focused PTT on every other Linux environment (X11, sway, KDE, untested compositors). Release notes shall not claim Global PTT support outside the tested compositor. -- DEC-026: Mouse side-button support in first desktop PTT release = **Supported on Windows and macOS; Linux follows whatever the GlobalShortcuts portal exposes.** Resolves PTT-OPEN-004. The Raw Input backend (Windows) and the Event Tap backend (macOS) shall accept Mouse4 / Mouse5 bindings; the Linux portal binding accepts whatever the session exposes and the release notes shall not over-claim. -- DEC-027: PTT diagnostics privacy posture = **Capability and availability state only — no raw key codes ever leave the device.** Resolves PTT-OPEN-005. The diagnostic export shall name only `PttCapabilityLevel`, `backend_id`, `bound_input_class`, and `fallback_exercised`; the user's actual key value (scan code, virtual key, keysym) shall never be logged, persisted, or exported. `PttSanitizer` enforces this at write time. -- DEC-028: Missed-key-up watchdog requirement = **P0.** Resolves PTT-OPEN-006. The audio engine shall include a missed-key-up watchdog that clears `transmit_active` after a configured ceiling (default 30 s) when no key-up event arrives. The watchdog is a P0 release-gate item rather than an implementation-level concern because the failure mode (stuck transmission after the user has released the binding) is user-visible and reputation-relevant. -- DEC-029: Flutter global-hotkey packages rejected for PTT = **Use the native Rust `DesktopPttBackend` trait + per-OS implementations (already specified in SDD-083 / SDD-084 / SDD-085); do not adopt `hotkey_manager`, `super_hot_key`, or any equivalent pub.dev package for PTT.** Rationale: those packages wrap the OS `RegisterHotKey` / `RegisterEventHotKey` semantics — they consume the key (suppressing it from other applications), they do not deliver a key-up event, and they do not support mouse side-buttons. PTT requires the opposite primitive (observe, do not consume, deliver both up and down). -- DEC-030: Voice activity detection deferred to P1 = **`TransmitMode::VoiceActivity` ships as a reserved enum variant with no v1 implementation.** The settings UI shows it as a disabled "coming soon" option. The actual implementation choice is deferred to a future baseline. Rationale: three viable backends were compared (RMS energy threshold — trivial but quality-poor; WebRTC VAD via the `webrtc-vad` crate — frozen-but-stable C++ BSD-3 dep, ~200 KB binary, industry baseline; Silero VAD via ONNX Runtime — best quality but ~8–16 MB binary uplift per platform plus an ONNX-runtime dependency surface). The v1 dependency-surface budget does not have room for the trade-off review required to pick correctly. Choosing too early risks either user-visible quality regression (RMS) or a forced ONNX adoption (Silero) before there is a comparable need for ML inference elsewhere (noise suppression, AEC). -- DEC-001: Product Owner = Release type sequence -- DEC-002: Product Owner / Engineering = MVP platform strategy -- DEC-003: Product Owner / iOS Owner = Minimum iOS version -- DEC-004: Product Owner / Android Owner = Minimum Android version -- DEC-005: Android Owner / Release Manager = Android target SDK -- DEC-006: Product Owner / Software Architect = Multiple server connections in MVP -- DEC-007: Audio Owner / Product Owner = AEC default -- DEC-008: Audio Owner / Product Owner = AGC default -- DEC-009: Audio Owner / Product Owner = Noise suppression default -- DEC-010: Audio Owner = High-pass filter default -- DEC-011: Software Architect / Audio Owner = Audio processing path -- DEC-011.1: Software Architect / Audio Owner = Audio crate (desktop / mobile) -- DEC-012: Legal / Compliance = SDK/trademark/licensing review -- DEC-013: Software Architect / Storage Owner = Local database -- DEC-013.1: Software Architect / Storage Owner = SQLite crate -- DEC-013.2: Software Architect / Storage Owner / Security Reviewer = Linux secure-storage backend policy -- DEC-014: Software Architect = Bridge choice -- DEC-015: Product Owner = Product language for MVP -- DEC-016: Product Owner / Legal / Security = Diagnostics upload policy -- DEC-017: Product Owner / Legal / Security = Crash reporting -- DEC-018: Product Owner = Public product name -- DEC-020: Product Owner / Legal = License model -- DEC-021: iOS Owner / Release Manager = Apple App Store SDK gate -- DEC-022: Software Architect = Canonical implementation directory layout -- DEC-023: Product Owner / Windows Platform Owner = Windows desktop Global PTT priority -- DEC-024: Product Owner / macOS Platform Owner = macOS desktop Global PTT priority -- DEC-025: Product Owner / Linux Platform Owner = Officially-tested Linux environment -- DEC-026: Product Owner / UX Owner = Mouse side-button support -- DEC-027: Security / Privacy Reviewer = PTT diagnostics privacy posture -- DEC-028: Audio Owner / Software Architect = Missed-key-up watchdog -- DEC-029: Software Architect / Audio Owner = Flutter global-hotkey packages rejected for PTT -- DEC-030: Audio Owner / Product Owner = Voice activity detection deferred to P1 - -## Required P0 Capability Groups -1. App shell: routing, startup, theme, localization, adaptive layout, platform-service initialization. -2. UI/design: Material 3, Chanora design tokens, responsive desktop/tablet/mobile layouts, accessible controls, keyboard focus, safe areas/insets. -3. Protocol: TS3-compatible adapter through `tsclientlib`; no direct UI coupling to protocol internals. -4. Connection: one active server connection; visible connecting/connected/reconnecting/disconnected/error states. -5. Channel/user tree: preserve Unicode server content; do not translate server names/messages. -6. Voice basics: capture/playback path, push-to-talk where platform-supported, mute/deafen indicators, native processing defaults. -7. Audio defaults: AEC on where stable, AGC on with user toggle, noise suppression on with user toggle, high-pass filter on. -8. Storage: SQLite for non-secret state; platform secure storage for identities/tokens/password-equivalent secrets. -9. Diagnostics: local export only; redact secrets; machine fields remain language-neutral. -10. Builds: reproducible platform builds for Windows, macOS, Linux, Android, and iOS with staged release allowed. -11. Verification: unit, widget, integration, platform acceptance, and release go/no-go evidence. - -## P0 Architecture Boundaries -- Flutter owns UI, navigation, Material 3 theming, accessibility, and adaptive presentation. -- Rust core owns domain state, protocol/session orchestration, storage, diagnostics, and platform-abstracted services. -- Bridge owns typed DTOs/events/errors only. Do not leak Rust internals into Flutter or Flutter objects into Rust core. -- Platform adapters own OS-specific audio, hotkey, secure storage, build, and permission behavior. - -## P0 Definition of Done -- User can launch app, enter/connect to a TS3-compatible server, see server/channel/user state, and disconnect cleanly. -- App supports the selected P0 voice path without telemetry or automatic upload. -- Secrets are never stored in SQLite/plaintext. -- English and zh-Hans product strings are available; server-originated Unicode remains unchanged. -- All diagnostic exports are local/user-triggered and redacted. -- Platform P0 acceptance docs pass or contain explicit blocker records. -- Every code change cites at least one requirement or decision ID. - -## P0 Requirement Index -- Count: 500 -- ID groups: SAD-001..SAD-096 (71 ids); SDD-001..SDD-121 (98 ids); SRS-001..SRS-219 (178 ids); SysDes-001..SysDes-158 (153 ids) - -## P0 Source Weighting -- `requirements/srs.md`: 178 -- `architecture/sysdes.md`: 153 -- `architecture/sdd.md`: 98 -- `architecture/sad.md`: 71 - -## P0 Allocation Hotspots -- Unallocated: 98 -- Flutter UI: 68 -- Rust Core: 52 -- Verification: 43 -- Audio Subsystem: 42 -- Software: Audio Subsystem: 39 -- System Engineering: 33 -- Protocol Adapter: 31 -- Diagnostics: 28 -- State Sync: 24 -- Design System: 22 -- Bridge: 21 -- Storage: 21 -- Software: Diagnostics: 21 -- Platform: 20 -- Software: Protocol Adapter: 20 -- Platform Adapters: 19 -- Software: Rust Core: 19 -- Audio: 18 -- Operations: 17 - -## Agent Compression Rule -Do not paste all P0 requirement statements into prompt context. Use this file as the execution contract and `phase_index.json` only when an exact ID/source lookup is required. diff --git a/docs/P1_BETA_AGENT_SPEC.md b/docs/P1_BETA_AGENT_SPEC.md deleted file mode 100644 index 1363beb..0000000 --- a/docs/P1_BETA_AGENT_SPEC.md +++ /dev/null @@ -1,58 +0,0 @@ -# P1 Beta Agent Spec — Harden After MVP - -## Objective -Improve reliability, beta readiness, accessibility coverage, diagnostics usefulness, and platform quality after P0 works end-to-end. - -## Entry Gate -Do not start P1 until P0 MVP acceptance is passing or blockers are formally recorded. - -## P1 Scope -- Harden reconnection/error handling beyond MVP happy path. -- Expand integration/system tests and beta acceptance evidence. -- Improve accessibility, keyboard workflows, adaptive layout edge cases, and localization completeness. -- Improve diagnostics quality without changing the privacy model. -- Prepare external beta release notes, support workflows, and known-issue tracking. - -## P1 Non-Goals -- No telemetry by default. -- No automatic diagnostic upload. -- No multiple active server connections unless a new accepted decision overrides DEC-006. -- No production/store launch gates unless explicitly promoted to P2. - -## P1 Definition of Done -- P0 regressions remain passing. -- Beta blockers are triaged and traceable. -- Diagnostic exports are useful for support while still redacted and local. -- Platform-specific beta risks have owner/status records. - -## P1 Requirement Index -- Count: 81 -- ID groups: SAD-035..SAD-066 (18 ids); SDD-051..SDD-109 (20 ids); SRS-028..SRS-191 (38 ids); SysDes-122, SysDes-123, SysDes-124, SysDes-125, SysDes-138 - -## P1 Source Weighting -- `requirements/srs.md`: 38 -- `architecture/sdd.md`: 20 -- `architecture/sad.md`: 18 -- `architecture/sysdes.md`: 5 - -## P1 Allocation Hotspots -- Unallocated: 20 -- Diagnostics: 14 -- Audio Subsystem: 13 -- Flutter UI: 9 -- Platform Adapters: 9 -- Deployment Scripts: 7 -- Software Architecture: 6 -- Application Container: 6 -- Storage: 5 -- Protocol Adapter: 5 -- Localization Service: 5 -- Bridge: 4 -- Flutter App Shell: 4 -- Software Design: 4 -- Platform Adapter: 4 -- Platform Audio Adapter: 3 -- State Sync: 3 -- Rust Core: 3 -- Verification Support: 2 -- Flutter State: 2 diff --git a/docs/P2_PRODUCTION_AGENT_SPEC.md b/docs/P2_PRODUCTION_AGENT_SPEC.md deleted file mode 100644 index adddcdb..0000000 --- a/docs/P2_PRODUCTION_AGENT_SPEC.md +++ /dev/null @@ -1,34 +0,0 @@ -# P2 Production Agent Spec — Public Release Readiness - -## Objective -Prepare Chanora for public/store release only after P0 implementation and P1 beta hardening are complete. - -## Entry Gate -Do not begin P2 until P0 and P1 evidence is available. Public release is blocked by legal/trademark/OSS review and store-policy readiness. - -## P2 Scope -- Final release go/no-go records. -- Store metadata and platform upload compliance. -- Legal review: TeamSpeak non-affiliation wording, trademark usage, OSS license posture, `tsclientlib` license posture. -- Final privacy policy confirmation against actual app behavior. -- Production build signing, artifact policy, and staged rollout policy. - -## P2 Non-Goals -- Do not add telemetry/crash reporting to satisfy production readiness unless privacy/legal/product explicitly approve it. -- Do not expand server/backend scope. -- Do not add new features that destabilize P0/P1 acceptance. - -## P2 Definition of Done -- Release manager has signed go/no-go evidence. -- Legal/compliance blockers are closed. -- Platform upload SDK/target requirements are current on upload date. -- Release artifacts are reproducible, signed where required, and mapped to a version/tag. - -## P2 Requirement Index -- Count: 11 -- ID groups: SAD-045, SAD-046, SAD-047, SAD-048, SAD-049, SAD-050, SAD-055; SDD-063; SRS-117, SRS-154, SRS-175 - -## P2 Source Weighting -- `architecture/sad.md`: 7 -- `requirements/srs.md`: 3 -- `architecture/sdd.md`: 1 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index ef190b5..0000000 --- a/docs/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Chanora AI-Agent Minimal Phased Requirements - -This bundle intentionally removes the large rewritten archive and trace CSV. It is designed for AI agents with limited context. - -Recommended loading: -1. `00_AGENT_ROUTER.md` -2. One phase file: P0, P1, or P2 -3. `SOP_AGENT_OPERATIONS.md` only when process/release/security rules matter -4. `phase_index.json` only for exact ID lookup - -Do not load the old full bundle unless performing audit reconstruction. diff --git a/docs/SOP_AGENT_OPERATIONS.md b/docs/SOP_AGENT_OPERATIONS.md deleted file mode 100644 index 710f0d6..0000000 --- a/docs/SOP_AGENT_OPERATIONS.md +++ /dev/null @@ -1,46 +0,0 @@ -# SOP Agent Operations — Standing Rules - -## Core Agent Rules -1. Default to P0. Do not implement P1/P2 unless explicitly requested. -2. Preserve traceability: commit messages, PR descriptions, tests, and review notes must cite decision/requirement IDs. -3. Make the smallest compliant change. Avoid architecture invention. -4. If requirements conflict, precedence is: Product Decision Register > SysRS > SysDes > SRS > SAD > SDD > Verification/Release/Security docs. -5. If a task requires exact historical detail, inspect `phase_index.json`; do not load the obsolete full bundle. - -## Required Review Gates -- Architecture boundary review for Flutter/Rust/bridge/protocol/storage/audio changes. -- Privacy review for diagnostics, logging, crash handling, analytics, upload, identifiers, or support artifacts. -- Security review for secure storage, redaction, secrets, dependencies, and supply chain. -- Release review for platform build, signing, SDK, store, and artifact policy. -- Localization review for product strings and server-content preservation. - -## Testing SOP -- Unit tests: Rust core, DTO transforms, storage, redaction, decision logic. -- Widget tests: Material 3 components, state tokens, localization, accessibility semantics, adaptive layouts. -- Integration tests: bridge events/errors, protocol adapter state, diagnostics export, storage migration. -- Platform tests: secure storage, audio capture/playback, permissions, hotkeys/PTT, packaging. -- Acceptance tests: use platform P0 acceptance docs before any release promotion. - -## Build SOP -- Keep repo layout canonical: `apps/chanora_flutter/`, `core/chanora_core/`, `crates/chanora_*`. -- Pin `flutter_rust_bridge` 2.x. -- Use bundled `rusqlite` for embedded SQLite. -- Android minSdk = 28; target API must match current Google Play requirement on upload date. -- iOS minimum = 13; App Store upload SDK gate must be checked on actual upload date. - -## Privacy/Security SOP -- No telemetry in MVP. -- No crash reporting unless approved. -- No automatic diagnostic upload. -- Diagnostics are local export only and must redact secrets. -- Product-owned strings may be localized; server-originated content must be preserved verbatim. -- Secrets never enter logs, diagnostics, SQLite, screenshots, or crash-like artifacts. - -## Release SOP -- Release sequence: Internal Alpha -> External Beta -> MVP Public. -- Public/store release requires legal/trademark/OSS license confirmation. -- Staged platform release is allowed when documented. -- Every release must include version, commit/tag, artifact list, known issues, test evidence, and go/no-go result. - -## Agent Refusal / Escalation Conditions -Stop implementation and ask for owner approval when asked to add or change: telemetry, crash reporting, diagnostic uploads, backend service, multi-server active sessions, platform scope, license, trademark/non-affiliation text, secret-storage policy, or target SDK/minimum OS baseline. diff --git a/docs/architecture/desktop-ptt-architecture.md b/docs/architecture/desktop-ptt-architecture.md new file mode 100644 index 0000000..9cd6b92 --- /dev/null +++ b/docs/architecture/desktop-ptt-architecture.md @@ -0,0 +1,43 @@ +# Chanora Desktop Push-to-Talk Architecture + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Related documents:** `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/release/dv-waiver-register.md` + +## 1. Purpose + +This document records the desktop push-to-talk architecture advertised by the README and connects it to the SWE.2/SWE.3 baselines. + +## 2. Architecture Summary + +Desktop PTT is implemented as a platform-capability feature. The application must detect the active backend, expose the resulting `PttCapabilityLevel`, and avoid claiming global PTT support when the runtime falls back to focused-input behavior. + +## 3. Platform Backends + +| Platform | Backend strategy | Release claim rule | +|---|---|---| +| Windows | Raw Input first, low-level keyboard hook fallback, focused fallback if unavailable | Claim only the detected runtime capability | +| macOS | Event Tap where permission and OS policy allow; focused fallback otherwise | Claim Global PTT only with permission/backend evidence | +| Linux | Freedesktop GlobalShortcuts portal where available; focused fallback otherwise | State portal/fallback behavior clearly | + +## 4. Safety Rules + +| Rule | Purpose | +|---|---| +| Missed-key-up watchdog clears transmit after timeout | Prevents stuck transmit when an OS suppresses key-up | +| Capability is surfaced to UI and release record | Prevents over-claiming platform support | +| Mouse side-button support is platform-dependent | Avoids blocking release on Linux portal limitations | +| Focused fallback remains available | Preserves usable PTT when global backends are unavailable | + +## 5. Verification Handoff + +| Evidence | Required result | +|---|---| +| Per-platform smoke | Active backend and fallback behavior recorded | +| UI inspection | PTT capability badge matches runtime backend | +| Release readiness | Release notes mirror actual capability per platform | +| Safety test | Watchdog prevents stuck transmit after missed key-up | + +## 6. DV Conclusion + +The desktop PTT architecture is documented for DV navigation. Public release claims still require per-platform PTT evidence attached to the release-readiness record. diff --git a/docs/architecture/sad.md b/docs/architecture/sad.md new file mode 100644 index 0000000..70a2d1a --- /dev/null +++ b/docs/architecture/sad.md @@ -0,0 +1,186 @@ +# Chanora Software Architecture Description + +**Lifecycle:** SWE.2 Software Architectural Design +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Direct upstream source:** `docs/srs.md` +**Related system allocation:** `docs/sysdes.md` + +## 1. Purpose + +This Software Architecture Description defines Chanora's software architecture for DV review. It bridges SRS software requirements to SWE.3 detailed design and to SWE.5/SWE.6 verification planning. + +This baseline captures the architecture visible in the current repository. It is sufficient for DV traceability review, while deeper per-module algorithms remain in `docs/architecture/sdd.md` and source-level design. + +## 2. Architectural Scope + +Chanora is a Flutter application with a Rust core. Flutter owns the user-facing shell, Material 3 widgets, localization, permission UX, and platform service presentation. Rust owns connection orchestration, protocol isolation, audio processing, storage coordination, diagnostics, server resolution, prefetch policy, and bridge DTOs. + +## 3. Upstream SRS Allocation + +| SRS group | Architectural allocation | +|---|---| +| SRS-003, SRS-008 through SRS-016 | Cross-platform app shell, Flutter UI, Rust core, platform adapters | +| SRS-017 through SRS-030 | Flutter UI, state presentation, connection and voice controls | +| SRS-031 through SRS-035 | Bridge layer and typed DTO boundary | +| SRS-036 through SRS-043 | Rust core connection lifecycle and state behavior | +| SRS-044 through SRS-053 | Protocol adapter and TeamSpeak-compatible server boundary | +| SRS-054 through SRS-061 | State synchronization and replay/reducer verification hooks | +| SRS-062 through SRS-083 | Audio subsystem, DSP, codec, PTT, mute/deaf, metering | +| SRS-084 through SRS-095 | Storage, secure storage, identity, diagnostics-sensitive data | +| SRS-096 through SRS-102 | Diagnostics, export, redaction, troubleshooting hooks | +| SRS-103 through SRS-123 | Platform adapters, packaging, release behavior | +| SRS-124 through SRS-143 | Verification support, analysis requirements, traceability rules | +| SRS-144 through SRS-184 | Material 3, adaptive UI, accessibility, localization, Unicode, app initialization | +| SRS-185 through SRS-218 | Platform baselines, PTT capability, transmit mode, no automatic telemetry, benchmark advisory | + +## 4. Component Architecture + +| Component | Repository location | Responsibility | Direct architectural dependencies | +|---|---|---|---| +| Flutter app shell | `apps/chanora_flutter/lib/main.dart`, services, widgets | App startup, screen composition, user actions, localization, Material 3 UI | Generated Rust bridge, platform plugins, Flutter services | +| Flutter service layer | `apps/chanora_flutter/lib/services/` | Permission flows, lifecycle policy, host prefetch debounce, link trust, state mapping, platform back intent | Flutter app shell, generated bridge APIs, platform plugins | +| Flutter widget layer | `apps/chanora_flutter/lib/widgets/` | Connect UI, channel tree, chat, voice controls, settings, diagnostics surfaces | Flutter services, generated DTOs, design tokens | +| Bridge layer | `crates/chanora_bridge`, `apps/chanora_flutter/lib/src/rust/` | Typed Flutter/Rust boundary and generated bindings | Rust core, Flutter generated code | +| Rust core | `core/chanora_core` | Connection lifecycle, orchestration, reconnect behavior, storage coordination, voice state | Protocol, audio, storage, diagnostics, state, resolver/prefetch | +| Protocol adapter | `crates/chanora_protocol` | Isolate `tsclientlib`, expose typed protocol DTOs/errors | Rust core, external compatible server | +| State sync | `crates/chanora_state` | Snapshot/delta model, channel join helpers, reducer behavior | Rust core, protocol DTOs | +| Audio subsystem | `crates/chanora_audio` | Capture/playback, Opus, DSP, PTT, voice activity reservation, platform units | Rust core, platform APIs, protocol audio path | +| Storage | `crates/chanora_storage` | Bookmarks, identities, encrypted local data, platform keyring integration | Rust core, platform secure storage | +| Diagnostics | `crates/chanora_diagnostics` | Redaction, log sink, export bundle, known-secret registry | Rust core, Flutter diagnostics UI | +| Server resolver | `crates/chanora_resolver` | SRV/TSDNS/DNS fallback resolution | Rust core, prefetch crate | +| Server prefetch | `crates/chanora_server_prefetch`, Flutter `prefetch_debouncer.dart` | Invisible host-field resolution warming, TTL cache, generation safety | Resolver, Flutter connect UI, Rust core | + +## 5. Static Architecture View + +```text +Flutter UI/widgets/services + -> generated Dart bridge API + -> chanora_bridge + -> chanora_core + -> chanora_protocol -> tsclientlib -> external compatible server + -> chanora_state + -> chanora_audio -> platform audio APIs / Opus / DSP + -> chanora_storage -> platform secure storage / SQLite + -> chanora_diagnostics + -> chanora_server_prefetch -> chanora_resolver -> network DNS/TSDNS +``` + +The bridge is the trust and type boundary between Flutter and Rust. Flutter must not directly depend on protocol-library internals. Rust core must not expose platform-specific storage or audio details to UI code except through stable DTOs and capability fields. + +## 6. Runtime Flow Architecture + +### 6.1 Connect Flow + +```text +User enters host/bookmark + -> Flutter connect widgets + -> optional prefetch debounce + -> bridge connect command + -> Rust core supervisor + -> resolver / prefetch cache + -> protocol adapter + -> external compatible server + -> state snapshot/events + -> bridge event stream + -> Flutter state mapper and widgets +``` + +### 6.2 Voice Flow + +```text +Microphone / platform input + -> audio capture unit + -> DSP chain: HPF, NS, AEC, AGC where active + -> PTT/mute/transmit gate + -> Opus encode + -> protocol adapter + -> external compatible server + +External server voice + -> protocol adapter + -> jitter/decode path + -> mixer / per-user controls + -> platform output +``` + +### 6.3 Diagnostics Flow + +```text +Runtime event or error + -> diagnostic log sink / known-secret registry + -> redactor + -> user-initiated export bundle + -> Flutter share/export surface +``` + +## 7. Interface Catalogue + +| Interface | Producer | Consumer | Architectural rule | +|---|---|---|---| +| Bridge command DTOs | Flutter generated API | `chanora_bridge`, Rust core | Stable typed DTOs; no raw protocol-library types cross to Flutter | +| Bridge event DTOs | Rust core / bridge | Flutter services/widgets | User-safe errors and capability fields are explicit | +| Protocol DTOs | `chanora_protocol` | Rust core, state sync | Protocol adapter isolates `tsclientlib` | +| Audio configuration | Flutter settings / Rust core | `chanora_audio` | Voice modes and processing flags are explicit; VAD remains disabled/deferred | +| Storage records | Storage crate | Rust core / Flutter UI via bridge | Secrets stay behind secure-storage abstraction | +| Diagnostic bundles | Diagnostics crate | Flutter diagnostics UI | Redaction runs before export or display | +| Platform capability records | Platform adapters/audio/PTT backends | UI and release record | UI/release wording must not over-claim capability | + +## 8. Dependency Rules + +| Rule | Rationale | +|---|---| +| Flutter UI depends on generated bridge APIs, not Rust internals | Keeps UI stable across Rust implementation changes | +| Rust core orchestrates crates but protocol/audio/storage crates remain separately testable | Supports SWE.4 unit verification and bounded responsibilities | +| Protocol adapter is the only component that owns `tsclientlib` coupling | Protects the app from protocol-library leakage | +| Diagnostics redaction must be reusable by runtime logging and export | Prevents split redaction behavior | +| Platform-specific behavior stays in platform adapters or audio platform units | Keeps cross-platform logic testable and reduces conditional sprawl | +| Release claims consume capability records and release evidence | Prevents over-claiming PTT, signing, packaging, or secure-storage behavior | + +## 9. Non-Functional Allocation + +| Concern | Architectural mechanism | Verification owner | +|---|---|---| +| Real-time audio responsiveness | Rust audio subsystem, benchmark advisory, bounded callback behavior | Audio / Platform QA | +| Privacy and no automatic telemetry | User-initiated diagnostics, no automatic upload policy | Security / Privacy QA | +| Secure secret handling | Platform secure-storage abstraction and encrypted local storage | Security / QA | +| Cross-platform UI | Flutter Material 3, design tokens, responsive widgets | Software QA / UX | +| Protocol compatibility | `tsclientlib` adapter isolation and compatible-server matrix | Protocol / Integration QA | +| Release reproducibility | CI, build scripts, artifact hashes, release-readiness record | Release / Operations QA | + +## 10. Architectural Decisions Captured by This Baseline + +| Decision | Architectural outcome | +|---|---| +| Flutter + Rust split | Flutter owns presentation; Rust owns protocol/audio/storage/diagnostics core behavior | +| `tsclientlib` isolation | Protocol compatibility is behind `chanora_protocol` | +| Secure storage abstraction | Platform storage details do not leak into UI or unrelated crates | +| Advisory audio benchmarks | Performance regressions are surfaced without making CI a hard release gate at this stage | +| PTT capability levels | Platform PTT support is represented as capability data and must match release wording | +| VoiceActivity deferral | `VoiceActivity` remains reserved/disabled until a later baseline allocates implementation | +| No automatic diagnostic upload in MVP | Diagnostics are local and user-initiated unless future approved requirements change policy | + +## 11. Verification Handoff + +| Verification plan | SAD handoff | +|---|---| +| SWE.4 | Component boundaries define unit-test ownership for Flutter services/widgets and Rust crates | +| SWE.5 | Interface catalogue and runtime flows define integration paths | +| SWE.6 | SRS allocation and acceptance flows define software acceptance evidence | +| SYS.4 | Platform capability and external-server boundaries define system integration evidence | + +## 12. Traceability to SRS + +This SAD derives only from `docs/srs.md`. The broad SRS group-to-component allocation in section 3 is the controlling SWE.2 trace for DV. Detailed item-level trace is represented by the SRS coverage matrix and `docs/governance/traceability-matrix.md`. + +## 13. Open Architecture Risks + +| Risk | Impact | Control | +|---|---|---| +| SAD item numbering from historical status references is not reconstructed in this baseline | Existing references such as `SAD-043` and `SAD-046` are not itemized here | Treat this as a DV baseline SAD; add itemized SAD IDs in a follow-up if process requires strict ID-level review | +| Some architecture views are textual rather than C4 diagrams | Reviewers may request visual C4 views | Record as documentation hardening, not a blocker for DV baseline if textual views are accepted | +| Release/platform architecture evidence is incomplete | Public release remains blocked | Controlled by release-readiness and waiver records | + +## 14. DV Conclusion + +This SWE.2 baseline is sufficient to remove the missing-SAD traceability gap for DV review. It does not replace candidate test evidence or final release approval. diff --git a/docs/architecture/sdd.md b/docs/architecture/sdd.md new file mode 100644 index 0000000..c966dd4 --- /dev/null +++ b/docs/architecture/sdd.md @@ -0,0 +1,154 @@ +# Chanora Software Detailed Design + +**Lifecycle:** SWE.3 Software Detailed Design and Unit Construction Handoff +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Direct upstream source:** `docs/architecture/sad.md` +**Related software requirements:** `docs/srs.md` + +## 1. Purpose + +This Software Detailed Design defines the module-level design details needed for SWE.4 unit verification and SWE.5 integration verification. It is based on the current repository layout and the SWE.2 architecture baseline. + +## 2. Module Catalogue + +| SDD module | Source location | Primary responsibility | Upstream SAD component | +|---|---|---|---| +| SDD-MOD-001 Flutter app bootstrap | `apps/chanora_flutter/lib/services/app_bootstrap.dart`, `main.dart` | Initialize Rust bridge, localization, app services, theme/design baseline | Flutter app shell | +| SDD-MOD-002 Connect UI | `apps/chanora_flutter/lib/widgets/connect_widgets.dart` | Host/bookmark inputs, connect actions, pre-request UX | Flutter widget layer | +| SDD-MOD-003 Snapshot and channel UI | `snapshot_view.dart`, `snapshot_state_mapper.dart`, `channel_spacer.dart` | Present channel tree, clients, and mapped state | Flutter widget/service layer | +| SDD-MOD-004 Chat UI | `chat_views.dart`, `bbcode_text.dart` | Channel text rendering and BBCode-safe display | Flutter widget layer | +| SDD-MOD-005 Voice UI | `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart`, `voice_level_meter.dart`, `ptt_capability_badge.dart` | Voice controls, processing settings, metering, PTT capability | Flutter widget layer | +| SDD-MOD-006 Platform services | `android_permissions_service.dart`, `ios_permissions_service.dart`, `audio_lifecycle_service.dart`, `back_intent_*`, `link_trust_service.dart` | Permission, lifecycle, navigation, route/link trust behavior | Flutter service layer | +| SDD-MOD-007 Bridge API | `crates/chanora_bridge/src/api.rs`, generated Dart/Rust bridge files | Typed command/event boundary | Bridge layer | +| SDD-MOD-008 Rust core supervisor | `core/chanora_core/src/lib.rs`, `ptt.rs` | Connection orchestration, reconnect, PTT state, storage coordination | Rust core | +| SDD-MOD-009 Protocol adapter | `crates/chanora_protocol/src/` | `tsclientlib` isolation, DTO/error mapping | Protocol adapter | +| SDD-MOD-010 State sync | `crates/chanora_state/src/lib.rs`, `channel_join.rs` | Snapshot/delta model, reducer, channel join support | State sync | +| SDD-MOD-011 Audio subsystem | `crates/chanora_audio/src/` | Audio capture/playback, DSP, Opus, PTT, mode stack, platform units | Audio subsystem | +| SDD-MOD-012 Storage | `crates/chanora_storage/src/lib.rs` | Bookmarks, identity storage, encrypted local records, keyring abstraction | Storage | +| SDD-MOD-013 Diagnostics | `crates/chanora_diagnostics/src/lib.rs` | Redaction, log sink, known-secret registry, export bundle | Diagnostics | +| SDD-MOD-014 Resolution and prefetch | `crates/chanora_resolver/src/lib.rs`, `crates/chanora_server_prefetch/src/lib.rs`, `prefetch_debouncer.dart` | SRV/TSDNS/DNS fallback and generation-safe resolution warming | Server resolver / prefetch | +| SDD-MOD-015 Build and release hooks | `.github/workflows/`, `tools/`, platform project files | CI, unsigned iOS build, benchmark advisory, platform smoke procedures | Release / platform architecture | + +## 3. Bridge Boundary Design + +The bridge boundary is the only supported Flutter-to-Rust command path. Dart code uses generated APIs under `apps/chanora_flutter/lib/src/rust/`; Rust exposes bridge functions through `crates/chanora_bridge/src/api.rs`. + +Design rules: + +| Rule | Detail | +|---|---| +| DTO stability | DTO fields must be explicit and serializable through Flutter Rust Bridge generation | +| Error safety | Rust errors exposed to Flutter must be user-safe or mapped before display | +| Secret handling | Secrets may cross only as command inputs or protected DTO fields and must be registered for diagnostic redaction where relevant | +| Capability reporting | Platform and PTT capability fields must reflect actual active backend state | +| Regeneration control | Generated bridge files are implementation artifacts and must be regenerated when bridge API signatures change | + +## 4. Connection and State Design + +| Detail | Design | +|---|---| +| Connection lifecycle | Rust core owns connect/disconnect/reconnect decisions and suppresses reconnect after user disconnect | +| Backoff | Reconnect uses exponential backoff as described in implementation status, capped at 60 seconds | +| Server resolution | Resolver performs SRV/TSDNS/DNS fallback; prefetch cache may warm but must not be required for connect success | +| Snapshot mapping | Rust state and bridge DTOs are mapped into Flutter view models by `snapshot_state_mapper.dart` | +| Channel join | Channel join logic and errors are represented through Rust state/protocol handling and Flutter error mapper service | +| Reducers | `chanora_state` owns snapshot/delta reducer design with unit coverage for snapshot, delta, reconnect, duplicate normalization, disconnected/lost suppression, unknown-client voice activity, deterministic ordering, and channel-delete/client cleanup. Current runtime UI refresh still flows through `chanora_core` snapshot/probe paths; full live-event folding through `chanora_state::reduce` is an integration follow-up. | + +## 5. Audio Detailed Design + +| Audio element | Design detail | +|---|---| +| Capture/playback | Platform-specific units handle Android, iOS, desktop/fallback paths behind Rust audio abstractions | +| Codec | Opus encode/decode lives in `opus_voice.rs` and associated audio modules | +| DSP chain | High-pass filter, noise suppression, echo cancellation, and AGC are represented by audio processing modules/backends | +| Transmit control | `TransmitMode` supports `Ptt`, `Continuous`, and reserved `VoiceActivity`; `VoiceActivity` has no active MVP implementation | +| PTT | Desktop/mobile backends expose capability level and active backend; missed-key-up watchdog prevents stuck transmit | +| Release tail | Tail handling prevents abrupt cutoffs after PTT release where configured | +| Benchmarks | Realtime capture, Opus, and resampler benchmarks provide advisory baseline evidence | + +## 6. Storage and Secret Design + +| Storage item | Design detail | +|---|---| +| Bookmarks | Stored locally through the storage crate and surfaced in Flutter connect UI | +| Identity references | Stored through `IdentityFileStore` and platform secure storage where available | +| Passwords/secrets | Encrypted at rest using the current storage design; Android Keystore-backed DEK is deferred and must be disclosed | +| CI keyring behavior | CI disables real keyring access with `CHANORA_DISABLE_KEYRING=1` to avoid headless blocking | +| Fallback behavior | Platform fallback modes must be represented as limitations in release/security evidence | + +## 7. Diagnostics Detailed Design + +| Diagnostic element | Design detail | +|---|---| +| Log sink | Runtime logs can be captured by diagnostic sinks for export | +| Known-secret registry | Runtime secrets are registered for redaction where applicable | +| Redactor | Redacts configured sensitive patterns before export | +| Export bundle | Diagnostic export is JSON-based and user-initiated | +| Upload policy | MVP has no automatic diagnostic, telemetry, or crash upload | + +## 8. Flutter UI Detailed Design + +| UI area | Design detail | +|---|---| +| Design tokens | `chanora_tokens.dart` centralizes product styling over Material 3 | +| Platform capability display | `platform_capabilities.dart` and PTT capability widgets expose platform-specific support honestly | +| Localization | Generated localization files provide English and Simplified Chinese resources | +| Responsive behavior | Current widgets support compact/mobile-oriented layouts; expanded side-pane hardening remains P1/P2 as recorded | +| Accessibility | Critical status should use text/icons/semantics and not color alone; verification remains through UI tests/audit | +| UI settings persistence | `UiPreferencesService` persists host, nickname, permission explanation state, and theme mode through `shared_preferences`; invalid stored theme values fall back to system theme | + +## 9. Build and Release Detailed Design + +| Build/release item | Design detail | +|---|---| +| Rust CI | `.github/workflows/ci.yml` runs cargo check/test and advisory clippy | +| Flutter CI | `.github/workflows/ci.yml` runs Flutter pub get, analyze, and tests | +| Supply chain | CI runs cargo-deny and license inventory checks | +| iOS unsigned build | CI runs `flutter build ios --release --no-codesign` | +| Audio benchmarks | `bench-advisory.yml` runs audio benchmarks and posts advisory evidence | +| Platform packages | Public binary packaging/signing/notarization remains release-gated | + +## 10. Verification Hook Design + +| Module | SWE.4 unit hooks | SWE.5/SWE.6 integration hooks | +|---|---|---| +| Flutter services/widgets | Dart unit/widget tests under `apps/chanora_flutter/test/` | Widget/system demos and candidate device smoke | +| Bridge | API compile/generation checks | Flutter-to-Rust command/event smoke | +| Rust core | Cargo tests | Compatible-server lifecycle demo | +| Protocol | DTO/error mapping tests | Protocol compatibility matrix and server demo | +| State sync | Reducer tests | Snapshot/delta/reconnect integration evidence | +| Audio | DSP/codec/PTT tests and benchmarks | Platform audio loopback/device demo | +| Storage | Repository/encryption/keyring-disabled tests | Platform secure-storage audit | +| Diagnostics | Redaction/export tests | User-initiated export inspection | +| Release hooks | CI workflow validation | Release readiness record and artifact evidence | + +## 11. Traceability to SAD + +| SAD component | SDD modules | +|---|---| +| Flutter app shell | SDD-MOD-001 | +| Flutter service layer | SDD-MOD-006, SDD-MOD-014 | +| Flutter widget layer | SDD-MOD-002 through SDD-MOD-005 | +| Bridge layer | SDD-MOD-007 | +| Rust core | SDD-MOD-008 | +| Protocol adapter | SDD-MOD-009 | +| State sync | SDD-MOD-010 | +| Audio subsystem | SDD-MOD-011 | +| Storage | SDD-MOD-012 | +| Diagnostics | SDD-MOD-013 | +| Server resolver/prefetch | SDD-MOD-014 | +| Release/platform architecture | SDD-MOD-015 | + +## 12. Open Detailed-Design Risks + +| Risk | Impact | Control | +|---|---|---| +| Detailed item IDs from historical SDD references are not reconstructed | Existing references such as `SDD-109` are not itemized in this baseline | Treat this as a DV baseline SDD and add strict item numbering later if required | +| Some module designs are summarized rather than API-by-API | May be insufficient for final process audit | Use this as DV baseline; deepen high-risk modules before final release gate | +| Android Keystore-backed DEK is not implemented | Limits storage/security design claims | Controlled by waiver and release-readiness records | +| Full event replay tooling and live reducer integration evidence are absent | Limits state verification design beyond reducer unit behavior | Controlled as P1 gap and runtime-integration follow-up | + +## 13. DV Conclusion + +This SWE.3 baseline is sufficient to remove the missing-SDD traceability gap for DV review and to feed SWE.4/SWE.5 verification plans. It does not close release evidence gaps or replace source-level tests. diff --git a/docs/architecture/sysdes.md b/docs/architecture/sysdes.md new file mode 100644 index 0000000..7b99ea4 --- /dev/null +++ b/docs/architecture/sysdes.md @@ -0,0 +1,21 @@ +# System Design Specification + +**Document status:** DV entry-point record +**Canonical document:** `../sysdes.md` + +The canonical Chanora System Design Specification currently lives at `docs/sysdes.md`. This file preserves the README-advertised path `docs/architecture/sysdes.md` for DV navigation. + +Reviewers shall use `docs/sysdes.md` as the authoritative SysDes baseline until the repository migration moves the canonical file into this directory. + +## DV Review Summary + +| Topic | Canonical source | +|---|---| +| System element allocation | `docs/sysdes.md` sections 4 through 8 | +| Verification handoff | `docs/sysdes.md` section 12, SysDes-102 through SysDes-107 | +| SysRS-to-SysDes allocation matrix | `docs/sysdes.md` Appendix A | +| Change-control and traceability rules | `docs/sysdes.md` SysDes-108 through SysDes-110 | + +## DV Position + +The SysDes baseline is reviewable for DV. SRS, SAD, SDD, and verification documents derive from or consume this allocation layer. diff --git a/docs/chanora_p1_voice_agent_spec_v1_1.json b/docs/chanora_p1_voice_agent_spec_v1_1.json deleted file mode 100644 index a32eb0a..0000000 --- a/docs/chanora_p1_voice_agent_spec_v1_1.json +++ /dev/null @@ -1,767 +0,0 @@ -{ - "document_id": "CHANORA_P1_VOICE_AGENT_SPEC", - "version": "1.1.0", - "language": "en", - "audience": "ai_agents", - "scope": "P1_voice_only", - "mode": "modify_existing_scaffold_do_not_rewrite", - "based_on": { - "archive": "chanora-product-scaffold-v0.zip", - "previous_spec": "chanora_p1_voice_design_from_current_scaffold.json" - }, - "project_context": { - "ui": "Flutter", - "core": "Rust", - "network": "tsclientlib", - "audio_crate": "crates/chanora_audio", - "bridge_crate": "crates/chanora_bridge", - "flutter_app": "apps/chanora_flutter", - "phase_1_platform": "iOS", - "future_platforms": [ - "Android", - "Windows", - "macOS", - "Linux" - ] - }, - "current_scaffold_facts": { - "ios_audio_backend_exists": true, - "ios_current_backend": "VoiceProcessingIO AudioUnit", - "android_audio_backend_exists": true, - "android_current_backend": "Oboe", - "desktop_audio_backend_exists": true, - "desktop_current_backend": "cpal / SDL depending on platform", - "current_network_frame": { - "sample_rate_hz": 48000, - "channels": 1, - "frame_ms": 20, - "samples": 960, - "codec": "Opus VoIP" - }, - "current_effects_api_exists": true, - "current_effects_are_real_dsp": false, - "current_voice_activity_is_real_vad": false, - "current_voice_activity_behavior": "VoiceActivity behaves like Continuous", - "current_ios_has_platform_voice_processing": true, - "current_ios_platform_processing_likely_includes": [ - "AEC", - "NS", - "AGC" - ], - "current_rust_software_ns_exists": false, - "current_rust_software_aec_exists": false, - "current_rust_software_agc_exists": false - }, - "p1_primary_goals": [ - "Add real VoiceActivity mode using ML VAD.", - "Keep iOS VoiceProcessingIO as default shipping voice-processing path.", - "Add optional iOS Sonora mode for experimental raw Rust AEC3/NS/AGC2/HPF.", - "Make AEC/NS/AGC route-aware and backend-aware.", - "Expose voice processing configuration and stats to Flutter.", - "Preserve current 20ms Opus/tsclientlib transport behavior.", - "Add internal 10ms processing frames without changing network framing." - ], - "non_goals": [ - "Do not rewrite P0.", - "Do not move PCM processing to Flutter.", - "Do not replace tsclientlib.", - "Do not require Android/Desktop implementation in Phase 1.", - "Do not require CoreML conversion for P1.", - "Do not require GPU/NPU execution providers for correctness.", - "Do not ship AI noise suppression as P1 default.", - "Do not use energy VAD as production transmit gate." - ], - "hard_invariants": { - "INV_001": "Flutter MUST NOT process realtime PCM.", - "INV_002": "Rust AudioEngine MUST own voice state, VAD state, processing config, route policy, stats, and diagnostics.", - "INV_003": "Platform audio layer MUST only provide audio I/O, device/session config, and route detection.", - "INV_004": "tsclientlib MUST remain the voice network/session layer.", - "INV_005": "Keep existing 20ms Opus frame/network contract in P1.", - "INV_006": "Add internal 10ms processing frames by splitting 20ms frames into two 10ms frames.", - "INV_007": "Realtime callbacks MUST NOT call Flutter, block on network, perform file I/O, allocate unbounded memory, or run ML inference.", - "INV_008": "Rust panics MUST NOT cross FFI/bridge boundaries.", - "INV_009": "Never enable platform AEC and Rust AEC simultaneously on the same route.", - "INV_010": "Never enable VoiceProcessingIO and Sonora AEC3 simultaneously.", - "INV_011": "Software AEC-capable backends MUST receive both capture and render-reference streams.", - "INV_012": "Render reference MUST be copied from decoded/mixed remote PCM before playout.", - "INV_013": "VAD MUST NOT stop capture or playback; VAD may only gate transmit and update speaking/debug state." - }, - "canonical_audio_model": { - "network_frame": { - "sample_rate_hz": 48000, - "channels": 1, - "frame_ms": 20, - "samples": 960, - "sample_format": "i16_or_f32_internal_conversion", - "status": "keep_existing" - }, - "processing_frame": { - "sample_rate_hz": 48000, - "channels": 1, - "frame_ms": 10, - "samples": 480, - "sample_format": "f32", - "status": "new_internal_contract" - }, - "vad_model_input": { - "sample_rate_hz": 16000, - "channels": 1, - "sample_format": "f32", - "source": "processed_capture", - "resample_from": "48kHz" - } - }, - "target_architecture": { - "layers": [ - "Flutter UI", - "Typed Bridge / FFI", - "Rust Application Core", - "tsclientlib Adapter", - "Rust AudioEngine", - "AudioDevice backend", - "AudioProcessor backend", - "VoiceActivityDetector backend", - "VoiceTransmitStateMachine", - "AudioStats", - "WavDebugRecorder" - ], - "capture_path": [ - "Platform mic input", - "Platform audio callback", - "Minimal realtime-safe push into Rust", - "Canonical conversion to 48kHz mono f32", - "20ms network frame split into 2x10ms processing frames", - "Raw mic debug tap", - "AudioProcessor.process_capture", - "Processed mic debug tap", - "ML VAD worker consumes processed capture", - "VoiceActivityStateMachine updates transmit state", - "If transmitting, collect 10ms frames into 20ms Opus frame", - "tsclientlib / existing send path" - ], - "render_path": [ - "tsclientlib receive/decode/jitter/mix", - "Remote mixer output", - "Copy render_reference before playout", - "If software AEC backend active, call AudioProcessor.process_render on 10ms slices", - "Platform output callback", - "Speaker / receiver / headset" - ] - }, - "modules_to_add": { - "crates/chanora_audio/src/frame.rs": "AudioFrame10ms, AudioFrame20ms, split/merge helpers, i16/f32 conversion, dBFS helpers.", - "crates/chanora_audio/src/processor.rs": "AudioProcessor trait, AudioBackend enum, backend selector.", - "crates/chanora_audio/src/processor/platform.rs": "PlatformVoiceProcessing backend marker/policy.", - "crates/chanora_audio/src/processor/sonora.rs": "Optional Sonora backend: HPF, AEC3, NS, AGC2, limiter hook.", - "crates/chanora_audio/src/processor/noop.rs": "No-op backend for tests/headset/debug.", - "crates/chanora_audio/src/vad/mod.rs": "VoiceActivityDetector trait, VadOutput, VadBackend enum.", - "crates/chanora_audio/src/vad/silero_onnx.rs": "Silero ONNX VAD runtime wrapper.", - "crates/chanora_audio/src/vad/webrtc.rs": "WebRTC VAD fallback.", - "crates/chanora_audio/src/vad/energy_debug.rs": "Debug-only energy VAD.", - "crates/chanora_audio/src/voice_activity.rs": "VoiceActivity state machine.", - "crates/chanora_audio/src/stats.rs": "Expanded AudioStats schema.", - "crates/chanora_audio/src/debug_wav.rs": "Async WAV dump writer.", - "crates/chanora_audio/src/route_policy.rs": "AudioRoute enum and route-to-processing policy.", - "crates/chanora_audio/src/ios_raw_unit.rs": "Optional raw iOS RemoteIO path for Sonora mode." - }, - "modules_to_modify": { - "crates/chanora_audio/src/lib.rs": [ - "Export AudioProcessingConfig, AudioStats, AudioRoute, AudioBackend, VadBackend.", - "Replace no-op effects semantics with backend-aware processing config." - ], - "crates/chanora_audio/src/engine.rs": [ - "Own AudioProcessingConfig.", - "Own AudioProcessor backend.", - "Own VoiceActivityDetector backend.", - "Own VoiceActivityStateMachine.", - "Expose expanded stats.", - "Preserve existing 20ms Opus send/receive behavior." - ], - "crates/chanora_audio/src/transmit_selector.rs": [ - "Remove VoiceActivity == Continuous behavior.", - "Accept VoiceActivityStateMachine result for VoiceActivity transmit mode." - ], - "crates/chanora_audio/src/ios_voice_unit.rs": [ - "Keep as platform VoiceProcessingIO backend.", - "Ensure Rust AEC/NS/AGC disabled when this backend is active.", - "Feed captured audio into Rust VAD/state pipeline.", - "Do not run ML inference in callback.", - "Expose render_reference for debug/future software AEC validation." - ], - "crates/chanora_audio/src/android_voice_unit.rs": [ - "Keep current Oboe backend.", - "Future: adapt to same AudioProcessor/VAD config and stats." - ], - "crates/chanora_bridge/src/api.rs": [ - "Add set_audio_processing_config.", - "Add audio_processing_stats.", - "Add set_vad_model_path or bundled model discovery.", - "Add enable_audio_debug_wav_dump." - ], - "apps/chanora_flutter/ios/Runner/AppDelegate.swift": [ - "Keep AVAudioSession ownership.", - "Add/verify mediaServicesWereReset handling.", - "Send detailed route class to Rust.", - "Do not send PCM to Flutter." - ] - }, - "voice_processing_backends": { - "platform_voice_processing": { - "description": "OS/platform voice processing.", - "ios_implementation": "VoiceProcessingIO AudioUnit", - "ios_default": true, - "owns": [ - "AEC", - "NS", - "AGC" - ], - "rust_aec": "disabled", - "rust_ns": "disabled_by_default", - "rust_agc": "disabled_by_default" - }, - "sonora": { - "description": "Optional Rust-native WebRTC-style voice processor.", - "ios_status": "experimental_optional", - "future_cross_platform_status": "candidate", - "owns": [ - "HPF", - "AEC3", - "NS", - "AGC2", - "limiter_hook" - ], - "requires": [ - "raw_or_near_raw_capture", - "render_reference", - "48kHz mono 10ms frames" - ], - "must_not_run_with": [ - "VoiceProcessingIO", - "platform AEC" - ], - "shipping_default": false - }, - "webrtc_apm": { - "description": "Fallback/candidate WebRTC APM backend.", - "status": "candidate_or_fallback", - "requires": [ - "render_reference_for_AEC" - ] - }, - "noop": { - "description": "No processing.", - "status": "debug_or_headset_policy" - } - }, - "ios_processing_modes": { - "platform_default": { - "mode_id": "ios_platform_voice_processing", - "shipping_default": true, - "audio_io": "VoiceProcessingIO AudioUnit", - "av_audio_session": { - "category": "playAndRecord", - "mode": "voiceChat", - "preferred_sample_rate_hz": 48000, - "preferred_io_buffer_duration_ms": [ - 5, - 10 - ], - "options": [ - "defaultToSpeaker_when_speaker_route", - "allowBluetoothHFP", - "allowBluetoothA2DP_only_for_output_policy" - ] - }, - "processing": { - "aec": "platform", - "ns": "platform", - "agc": "platform", - "hpf": "platform_or_safe_rust_optional", - "limiter": "safe_rust_optional", - "sonora": "disabled" - }, - "vad": { - "backend": "silero_vad_onnx", - "input_source": "platform_processed_capture" - } - }, - "sonora_experimental": { - "mode_id": "ios_raw_sonora", - "shipping_default": false, - "audio_io": "RemoteIO_or_raw_input_output_path", - "av_audio_session": { - "category": "playAndRecord", - "mode": "default_or_measurement_candidate", - "avoid": [ - "voiceChat", - "videoChat", - "VoiceProcessingIO", - "setVoiceProcessingEnabled(true)", - "setPrefersEchoCancelledInput(true)" - ] - }, - "processing": { - "backend": "sonora", - "order": [ - "hpf", - "aec3", - "ns", - "agc2", - "limiter", - "vad_input_tap" - ], - "requires_render_reference": true, - "requires_raw_or_near_raw_capture": true - }, - "vad": { - "backend": "silero_vad_onnx", - "input_source": "sonora_processed_capture" - }, - "fallback": { - "on_backend_failure": "switch_to_ios_platform_voice_processing", - "on_render_reference_missing": "reject_sonora_aec_activation" - } - } - }, - "route_policy": { - "ios": { - "speaker": { - "default": "platform_voice_processing", - "optional": "sonora_experimental", - "aec": "platform_by_default", - "ns": "platform_by_default", - "agc": "platform_by_default", - "vad": "silero_vad_onnx", - "sonora_allowed_if": [ - "raw_io_available", - "render_reference_available" - ] - }, - "earpiece": { - "default": "platform_voice_processing", - "optional": "sonora_experimental", - "vad": "silero_vad_onnx" - }, - "wired_headset": { - "default": "noop_or_conservative", - "aec": "off", - "ns": "conservative_optional", - "agc": "conservative_optional", - "optional": "sonora_ns_agc_without_aec", - "vad": "silero_vad_onnx" - }, - "bluetooth_hfp": { - "default": "route_managed", - "aec": "app_side_off", - "ns": "route_managed_or_conservative", - "agc": "route_managed_or_conservative", - "sonora": "disabled_by_default", - "vad": "silero_vad_onnx" - }, - "bluetooth_a2dp": { - "default": "invalid_for_duplex_voice", - "transmit_allowed": false, - "vad": "disabled" - }, - "unknown": { - "default": "safe_fallback", - "aec": "off_until_classified", - "vad": "silero_vad_onnx" - } - }, - "future_platforms": { - "android": { - "default": "platform_effects_if_available_else_rust_backend", - "audio_io": "Oboe", - "vad": "silero_vad_onnx" - }, - "windows": { - "default": "rust_backend_candidate", - "audio_io": "WASAPI_or_CPAL", - "vad": "silero_vad_onnx" - }, - "macos": { - "default": "platform_or_rust_policy", - "audio_io": "CoreAudio_or_CPAL", - "vad": "silero_vad_onnx" - }, - "linux": { - "default": "rust_backend_candidate", - "audio_io": "CPAL_with_ALSA_PulseAudio_PipeWire", - "vad": "silero_vad_onnx" - } - } - }, - "vad_runtime_strategy": { - "owner": "Rust AudioEngine", - "default_backend": "silero_vad_onnx", - "model_artifact": "silero_vad.onnx", - "primary_runtime": "onnxruntime_c_api", - "rust_binding": "ort_or_onnxruntime_wrapper", - "fallback_backend": "webrtc_vad", - "debug_backend": "energy_vad", - "flutter_onnx_plugins": { - "allowed_in_realtime_voice_path": false, - "blocked_packages_for_core_path": [ - "onnxruntime_v2", - "fonnx" - ], - "reason": "Flutter must not process realtime PCM or own VAD inference." - }, - "coreml": { - "p1_primary": false, - "p1_conversion_required": false, - "p2_candidate": true, - "allowed_p1_use": "CoreML Execution Provider benchmark only, not correctness dependency" - }, - "execution_provider_policy": { - "correctness_provider": "CPU", - "acceleration_providers_optional": true, - "ios_optional": [ - "CoreML_EP_after_benchmark" - ], - "android_optional": [ - "NNAPI_after_benchmark" - ], - "windows_optional": [ - "DirectML_after_benchmark", - "CUDA_after_benchmark" - ], - "macos_optional": [ - "CoreML_EP_after_benchmark" - ], - "linux_optional": [ - "OpenVINO_after_benchmark", - "CUDA_after_benchmark", - "ROCm_after_benchmark" - ] - }, - "runtime_rules": { - "create_session_once": true, - "reuse_session": true, - "run_outside_realtime_callback": true, - "do_not_create_session_per_frame": true, - "fallback_if_model_missing": true, - "fallback_if_inference_lag_exceeds_budget": true, - "fallback_if_memory_budget_exceeded": true - }, - "model_input": { - "sample_rate_hz": 16000, - "channels": 1, - "source": "processed_capture", - "resample_from_hz": 48000 - }, - "defaults": { - "open_threshold": 0.55, - "close_threshold": 0.35, - "open_after_ms": 40, - "hangover_ms": 500, - "pre_roll_ms": 160, - "min_tx_ms": 200, - "max_inference_lag_ms": 60 - }, - "ios_memory_budget": { - "target_extra_rss_mb": 30, - "max_acceptable_extra_rss_mb": 50, - "fail_threshold_extra_rss_mb": 80, - "measurement_required": true, - "measurement_tool": "Xcode Instruments" - } - }, - "noise_suppression_strategy": { - "p1_default": "platform_or_webRTC_style_NS_not_AI_NS", - "ios": { - "default": "VoiceProcessingIO_NS", - "rust_ns": "disabled_by_default", - "reason": "Avoid double noise suppression and voice degradation." - }, - "ios_sonora_optional": { - "default": "Sonora_NS", - "requires": [ - "raw_io", - "sonora_backend_enabled" - ], - "status": "experimental" - }, - "android": { - "default": "Android NoiseSuppressor if available", - "fallback_candidate": "Sonora_NS_or_WebRTC_APM_NS" - }, - "desktop": { - "default_candidate": "Sonora_NS_or_WebRTC_APM_NS" - }, - "not_p1_default": [ - "RNNoise", - "DeepFilterNet", - "AI_noise_suppression_default" - ] - }, - "audio_processing_config_schema": { - "schema_version": "p1.audio_processing.v1", - "sample_rate_hz": 48000, - "network_frame_ms": 20, - "processing_frame_ms": 10, - "route": "speaker|earpiece|wired_headset|bluetooth_hfp|bluetooth_a2dp|unknown", - "ios_mode": "platform_voice_processing|sonora_experimental", - "processing_backend": "platform_voice_processing|sonora|webrtc_apm|noop", - "vad_backend": "silero_vad_onnx|webrtc_vad|energy_debug|disabled", - "aec": "platform|sonora|webrtc_apm|off", - "ns": "platform|sonora|webrtc_apm|off|conservative", - "agc": "platform|sonora|webrtc_apm|off|conservative", - "hpf_enabled": true, - "limiter_enabled": true, - "vad": { - "model": "silero_vad.onnx", - "runtime": "onnxruntime_c_api", - "input_source": "processed_capture", - "model_sample_rate_hz": 16000, - "open_threshold": 0.55, - "close_threshold": 0.35, - "open_after_ms": 40, - "hangover_ms": 500, - "pre_roll_ms": 160, - "min_tx_ms": 200, - "max_inference_lag_ms": 60, - "fallback_backend": "webrtc_vad" - }, - "debug": { - "stats_enabled": true, - "wav_dump_enabled": false - }, - "invalid_combinations": [ - "ios_mode=platform_voice_processing AND processing_backend=sonora", - "aec=platform AND aec=sonora", - "VoiceProcessingIO enabled AND Sonora AEC3 enabled", - "vad_backend=energy_debug in production", - "route=bluetooth_a2dp AND transmit_allowed=true" - ] - }, - "ffi_bridge_api_additions": { - "set_audio_processing_config": { - "crate": "crates/chanora_bridge/src/api.rs", - "input": "BridgeAudioProcessingConfig", - "output": "Result<(), BridgeError>" - }, - "audio_processing_stats": { - "crate": "crates/chanora_bridge/src/api.rs", - "input": "none", - "output": "BridgeAudioProcessingStats" - }, - "set_vad_model_path": { - "crate": "crates/chanora_bridge/src/api.rs", - "input": "String", - "output": "Result<(), BridgeError>", - "note": "May be replaced by bundled model discovery." - }, - "enable_audio_debug_wav_dump": { - "crate": "crates/chanora_bridge/src/api.rs", - "input": "bool", - "output": "Result<(), BridgeError>" - }, - "set_ios_voice_processing_mode": { - "crate": "crates/chanora_bridge/src/api.rs", - "input": "platform_voice_processing|sonora_experimental", - "output": "Result<(), BridgeError>" - } - }, - "runtime_stats_schema": { - "existing_keep": [ - "frames_sent", - "frames_received" - ], - "add": { - "input_dbfs": "float", - "render_dbfs": "float", - "processed_dbfs": "float", - "vad_probability": "float", - "vad_active": "bool", - "speaking": "bool", - "transmitting": "bool", - "vad_backend": "enum", - "vad_fallback_active": "bool", - "vad_inference_lag_ms": "float", - "vad_runtime_memory_estimate_mb": "float_optional", - "processing_backend": "enum", - "ios_voice_processing_mode": "enum_optional", - "audio_route": "enum", - "actual_sample_rate_hz": "uint32", - "actual_io_buffer_frames": "uint32", - "capture_queue_ms": "float", - "render_queue_ms": "float", - "estimated_echo_delay_ms": "float_optional", - "input_overruns": "uint64", - "output_underruns": "uint64", - "callback_xruns": "uint64", - "clipped_samples": "uint64", - "sonora_enabled": "bool", - "platform_voice_processing_enabled": "bool" - } - }, - "debug_wav": { - "enabled_by_default": false, - "streams": { - "raw_mic": "before_audio_processor", - "render_reference": "remote_mixer_output_before_playout", - "processed_mic": "after_audio_processor" - }, - "rules": [ - "Never write WAV from realtime callback.", - "Use bounded queues to async writer.", - "Include route/backend/timestamp in filenames or sidecar metadata." - ] - }, - "failure_modes": { - "silero_model_missing": [ - "fallback_to_webrtc_vad", - "emit_structured_error", - "stats.vad_fallback_active=true" - ], - "onnxruntime_unavailable": [ - "fallback_to_webrtc_vad", - "do_not_crash_audio_engine" - ], - "vad_inference_late": [ - "do_not_block_audio", - "use_latest_valid_probability_if_not_stale", - "fallback_to_webrtc_vad_if_stale" - ], - "onnx_memory_budget_exceeded": [ - "fallback_to_webrtc_vad", - "mark_ml_vad_unavailable", - "emit_structured_error" - ], - "sonora_requested_with_vpio": [ - "reject_config", - "return_invalid_config", - "do_not_start_double_processing" - ], - "sonora_aec_without_render_reference": [ - "reject_sonora_aec_activation", - "allow_sonora_ns_agc_only_if_configured", - "emit_structured_error" - ], - "route_change": [ - "update AudioRoute", - "recompute AudioProcessingConfig", - "reset processor if needed", - "reset AEC delay state if software AEC active" - ], - "ios_interruption": [ - "stop_or_pause_audio_io", - "resume_after_interruption_end", - "update_stats" - ], - "ios_media_services_reset": [ - "rebuild_AVAudioSession", - "rebuild_AudioUnit", - "reapply_config", - "update_stats" - ] - }, - "implementation_phases": { - "P1A_foundation": [ - "Add frame.rs 10ms/20ms types.", - "Add AudioProcessingConfig.", - "Add expanded AudioStats.", - "Add AudioRoute/AudioBackend/VadBackend enums.", - "Keep existing 20ms Opus path passing." - ], - "P1B_voice_activity": [ - "Implement VoiceActivityDetector trait.", - "Implement VoiceActivityStateMachine.", - "Make TransmitMode::VoiceActivity real VAD-gated mode.", - "Preserve PTT and Continuous behavior." - ], - "P1C_silero_onnx_vad": [ - "Choose Rust ONNX Runtime binding.", - "Bundle or locate silero_vad.onnx.", - "Create ONNX session once and reuse it.", - "Implement 48kHz-to-16kHz VAD adapter.", - "Run inference off callback thread.", - "Implement WebRTC VAD fallback." - ], - "P1D_ios_platform_default": [ - "Keep VoiceProcessingIO default.", - "Route VPIO processed capture to Rust VAD pipeline.", - "Expose route/backend/VAD stats.", - "Add mediaServicesWereReset handling if missing." - ], - "P1E_ios_sonora_optional": [ - "Add ios_raw_unit.rs RemoteIO/raw path.", - "Add processor/sonora.rs backend.", - "Wire render_reference to Sonora process_render.", - "Wire raw capture to Sonora process_capture.", - "Reject Sonora when VPIO active.", - "Mark Sonora experimental and disabled by default." - ], - "P1F_diagnostics": [ - "Add raw_mic/render_reference/processed_mic WAV dump.", - "Expose audio_processing_stats to Flutter debug UI.", - "No Flutter PCM." - ], - "P1G_acceptance": [ - "Speaker echo test VPIO default.", - "Speaker echo test Sonora optional.", - "Double-talk test VPIO vs Sonora.", - "VoiceActivity first-syllable test.", - "Quiet-room false-open test.", - "Keyboard-noise false-open test.", - "Route switching test.", - "Interruption recovery test.", - "Media services reset recovery test.", - "30-minute memory/thermal test with ONNX Runtime." - ] - }, - "acceptance_criteria": { - "vad": { - "VAD_001": "VoiceActivity no longer behaves as Continuous.", - "VAD_002": "Silero ONNX VAD active by default in P1 builds where runtime/model are available.", - "VAD_003": "WebRTC VAD fallback works if Silero/ONNX Runtime is unavailable, late, or over memory budget.", - "VAD_004": "First syllable preserved using pre-roll.", - "VAD_005": "Hangover prevents choppy transmit close.", - "VAD_006": "ML inference never runs inside realtime callback." - }, - "ios_platform": { - "IOSP_001": "VoiceProcessingIO remains default iOS shipping backend.", - "IOSP_002": "Platform AEC/NS/AGC active on speaker/earpiece policy.", - "IOSP_003": "Rust/Sonora AEC/NS/AGC disabled by default when VPIO active.", - "IOSP_004": "Route/interruption/media-services-reset handled without app restart." - }, - "ios_sonora": { - "IOSS_001": "Sonora backend exists as optional experimental mode.", - "IOSS_002": "Sonora mode uses raw or near-raw iOS I/O, not VoiceProcessingIO.", - "IOSS_003": "Sonora AEC3 receives render_reference before playout.", - "IOSS_004": "Config rejects VoiceProcessingIO + Sonora AEC3 double-processing.", - "IOSS_005": "Sonora can be compared against VPIO using same stats and WAV dumps." - }, - "noise_suppression": { - "NS_001": "iOS default uses platform NS through VoiceProcessingIO.", - "NS_002": "Rust NS is not double-enabled on iOS platform path.", - "NS_003": "Sonora NS is available only in Sonora experimental mode.", - "NS_004": "AI noise suppression is not P1 default." - }, - "diagnostics": { - "DIAG_001": "Stats expose route, backend, iOS mode, VAD probability, VAD backend, fallback state, sample rate, buffer size, xruns.", - "DIAG_002": "Debug WAV dump supports raw_mic, render_reference, processed_mic.", - "DIAG_003": "WAV writer never runs on realtime callback." - }, - "regression": { - "REG_001": "PTT mode remains functionally unchanged except diagnostics.", - "REG_002": "Continuous mode remains functionally unchanged except diagnostics.", - "REG_003": "Existing voice_join/voice_leave lifecycle remains public lifecycle.", - "REG_004": "Existing tsclientlib receive/decode/mix path remains active." - } - }, - "agent_execution_rules": { - "AGENT_001": "Modify existing scaffold; do not introduce a parallel audio engine.", - "AGENT_002": "Start in crates/chanora_audio; bridge and Flutter changes expose config/stats only.", - "AGENT_003": "Preserve 20ms Opus network framing.", - "AGENT_004": "Introduce 10ms processing frames internally only.", - "AGENT_005": "Do not enable Sonora on iOS default path.", - "AGENT_006": "Do not enable Rust AEC when platform AEC is active.", - "AGENT_007": "Do not run ML inference in realtime callback.", - "AGENT_008": "Do not use Flutter ONNX plugins in the core realtime path.", - "AGENT_009": "Every implementation PR should reference requirement or acceptance IDs.", - "AGENT_010": "If uncertain, preserve P0 lifecycle, tsclientlib transport, and cross-platform AudioEngine boundary." - } -} \ No newline at end of file diff --git a/docs/governance/baseline-approval-record.md b/docs/governance/baseline-approval-record.md new file mode 100644 index 0000000..0c63002 --- /dev/null +++ b/docs/governance/baseline-approval-record.md @@ -0,0 +1,32 @@ +# Chanora Baseline Approval Record + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Approval Scope + +This record tracks approval for documentation baselines. It does not approve public/store release. + +| Baseline | Status | DV disposition | +|---|---|---| +| SysRS | Baseline available | Reviewable | +| SysDes | Baseline available | Reviewable | +| SRS | Baseline available | Reviewable | +| SAD / SWE.2 | Baseline candidate | Reviewable with depth limitation | +| SDD / SWE.3 | Baseline candidate | Reviewable with depth limitation | +| Verification plans | Baseline candidate | Reviewable with evidence limitations | +| Release readiness | Baseline candidate | No-Go for public/store release | + +## 2. Required Approvers + +| Area | Approver role | +|---|---| +| Requirements/design | System Engineering / Software Engineering | +| Verification | Software QA / System QA | +| Release | Product / Release Operations | +| Security/privacy | Security / Privacy owner | +| Legal/trademark/OSS | Legal / Product owner | + +## 3. DV Recommendation + +Approve the document baseline for DV discussion. Do not approve public/store release until release gates close. diff --git a/docs/governance/baseline-candidate-validation-report.md b/docs/governance/baseline-candidate-validation-report.md new file mode 100644 index 0000000..6d4226c --- /dev/null +++ b/docs/governance/baseline-candidate-validation-report.md @@ -0,0 +1,38 @@ +# Chanora Baseline Candidate Validation Report + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Validation Summary + +Validation was performed against the README-advertised document tree and the DV lifecycle chain. + +| Check | Result | +|---|---| +| README document tree represented | Baseline candidate documents exist or path records point to canonical baselines | +| SysRS/SysDes/SRS chain | Available | +| SAD/SDD chain | Baseline candidates added | +| Verification plans | Available | +| Release readiness | Available; public/store release No-Go | +| Waivers | Available | + +## 2. Validation Method + +| Validation step | Evidence | +|---|---| +| README path coverage | Shell `test -f` command over every advertised document path | +| Sentinel-language scan | Review scan over the DV document tree returned no incomplete-marker matches | +| Lifecycle trace | Traceability matrix covers SysRS -> SysDes -> SRS -> SAD -> SDD -> Verification | +| Release posture | Release-readiness record and legal/security docs consistently keep public/store release at No-Go | + +## 3. Known Validation Limits + +| Limit | Control | +|---|---| +| Candidate run IDs not embedded | Release-readiness record requires run IDs before release approval | +| Legal DEC-012 open | Public/store release remains blocked | +| Some SAD/SDD details summarized | Deepen before final process audit if required | + +## 4. Conclusion + +The baseline candidate is suitable for DV meeting review with recorded limitations. diff --git a/docs/governance/decision-impact-assessment.md b/docs/governance/decision-impact-assessment.md new file mode 100644 index 0000000..0537f7b --- /dev/null +++ b/docs/governance/decision-impact-assessment.md @@ -0,0 +1,18 @@ +# Chanora Decision Impact Assessment + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Impact Matrix + +| Decision or gap | Requirements impact | Design impact | Verification impact | Release impact | +|---|---|---|---|---| +| DEC-012 open | Public wording and OSS gates remain constrained | Legal surfaces must avoid over-claiming | Audit evidence required | Public/store release No-Go | +| Android Keystore-backed DEK deferred | Secure-storage claim limited | Storage design carries fallback limitation | Platform audit required | Waiver required | +| Full reducer tests incomplete | State verification incomplete | State design remains valid but evidence partial | SWE.4/SWE.5 partial | Blocks full state-sync claim | +| Desktop/iOS artifacts not release-ready | Platform packaging requirements partial | Release design remains source-build/unsigned | SYS.4 evidence partial | Public binary release No-Go | +| VAD deferred | VoiceActivity not active | UI must show disabled/coming-soon | No VAD pass claim | No VAD marketing claim | + +## 2. Conclusion + +Current impacts are controlled for DV by waivers and release No-Go status. diff --git a/docs/governance/document-index.md b/docs/governance/document-index.md new file mode 100644 index 0000000..224b6ee --- /dev/null +++ b/docs/governance/document-index.md @@ -0,0 +1,35 @@ +# Chanora Document Index + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Purpose + +This index lists the documents required for DV review and identifies their current status. + +## 2. Baseline Documents + +| Area | Document | Status | +|---|---|---| +| Requirements | `docs/sysrs.md` | Canonical SysRS baseline | +| Requirements | `docs/srs.md` | Canonical SRS baseline | +| Requirements path | `docs/requirements/sysrs.md`, `docs/requirements/srs.md` | Path records pointing to canonical root files | +| System design | `docs/sysdes.md` | Canonical SysDes baseline | +| System design path | `docs/architecture/sysdes.md` | Path record pointing to canonical root file | +| Software architecture | `docs/architecture/sad.md` | SWE.2 baseline candidate | +| Software detailed design | `docs/architecture/sdd.md` | SWE.3 baseline candidate | +| Verification | `docs/verification/verification-master-plan.md` | Verification baseline candidate | +| Verification | `docs/verification/swe4-unit-verification-plan.md` | SWE.4 baseline candidate | +| Verification | `docs/verification/swe5-software-integration-verification-plan.md` | SWE.5 baseline candidate | +| Verification | `docs/verification/swe6-software-verification-plan.md` | SWE.6 baseline candidate | +| Verification | `docs/verification/sys4-system-integration-verification-plan.md` | SYS.4 baseline candidate | +| Release | `docs/release/release-readiness-go-nogo-record.md` | No-Go for public/store release | +| Release | `docs/release/platform-release-policy.md` | Baseline candidate | +| Governance | `docs/governance/traceability-matrix.md` | DV baseline candidate | +| Security/privacy/legal | `docs/security/security-privacy-legal-guideline.md` | Baseline candidate | +| Privacy | `docs/privacy/privacy-policy.md` | Engineering baseline candidate | +| Legal | `docs/legal/trademark-and-attribution-review.md` | DEC-012 open | + +## 3. DV Use + +Use this index as the first navigation document in the DV meeting. Release approval remains controlled by the release-readiness record. diff --git a/docs/governance/document-naming-convention.md b/docs/governance/document-naming-convention.md new file mode 100644 index 0000000..659074a --- /dev/null +++ b/docs/governance/document-naming-convention.md @@ -0,0 +1,33 @@ +# Chanora Document Naming Convention + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Rule + +Repository documentation uses lowercase kebab-case file names under topic directories. + +Examples: + +| Document type | Path pattern | +|---|---| +| Requirements | `docs/requirements/.md` or canonical root file during migration | +| Architecture | `docs/architecture/.md` | +| Verification | `docs/verification/-verification-plan.md` | +| Release | `docs/release/.md` | +| Governance | `docs/governance/.md` | + +## 2. Identifier Rules + +| Identifier | Meaning | +|---|---| +| `SysRS-XXX` | System requirement | +| `SysDes-XXX` | System design item | +| `SRS-XXX` | Software requirement | +| `SAD` | Software Architecture Description / SWE.2 | +| `SDD` | Software Detailed Design / SWE.3 | +| `DV-WVR-XXX` | DV waiver | + +## 3. Migration Rule + +When canonical files move, path records must be replaced by the canonical content or by redirects that clearly identify the authoritative source. diff --git a/docs/governance/document-review-report.md b/docs/governance/document-review-report.md new file mode 100644 index 0000000..4e8bb44 --- /dev/null +++ b/docs/governance/document-review-report.md @@ -0,0 +1,37 @@ +# Chanora Document Review Report + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Review Method + +Documents were reviewed for DV navigation, lifecycle coverage, explicit release gating, and absence of unsupported completion claims. + +| Review attribute | Value | +|---|---| +| Review date | 2026-05-29 | +| Review scope | `docs/requirements/`, `docs/architecture/`, `docs/verification/`, `docs/release/`, `docs/security/`, `docs/privacy/`, `docs/legal/`, `docs/ui-ux/`, `docs/i18n/`, `docs/governance/`, `docs/references/` | +| Review criteria | README path coverage, lifecycle traceability, release-status consistency, waiver visibility, security/privacy/legal gate visibility, sentinel-language scan | +| Evidence commands | README path `test -f` check; sentinel-language scan over the DV document tree | + +## 2. Findings + +| Finding | Status | Action | +|---|---|---| +| Previous gap: verification document set was missing | Addressed | Added verification master/SWE.4/SWE.5/SWE.6/SYS.4 plans | +| Previous gap: SAD/SDD were missing from current tree | Addressed | Added SWE.2/SAD and SWE.3/SDD baselines | +| Release blockers needed explicit record | Addressed | Added release-readiness record and waiver register | +| README tree had missing documents | Addressed for DV | Added baseline candidate records and path records | + +## 3. Residual Issues + +| Residual issue | DV handling | +|---|---| +| Candidate commit SHA, tag, artifact hashes, and run IDs are not recorded | Blocks release approval; acceptable for document baseline review | +| SAD/SDD are baseline candidates without historical item-number reconstruction | Accept for DV baseline; deepen before strict item-level audit | +| DEC-012 legal/trademark/OSS review remains open | Public/store release remains No-Go | +| Some implementation status items require revalidation after newer commits | Controlled by waiver register and release-readiness record | + +## 4. Review Conclusion + +The document set is reviewable for DV. Public/store release remains blocked by the release-readiness record. diff --git a/docs/governance/git-commit-message-convention.md b/docs/governance/git-commit-message-convention.md new file mode 100644 index 0000000..0f5fe3e --- /dev/null +++ b/docs/governance/git-commit-message-convention.md @@ -0,0 +1,21 @@ +# Chanora Git Commit Message Convention + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +Chanora uses Conventional Commits style: + +```text +(): +``` + +Examples: + +```text +feat(voice): add push-to-talk state handling +fix(protocol): recover channel tree after reconnect snapshot +sec(diagnostics): redact server password from export bundle +release(android): prepare internal alpha build metadata +``` + +Commits that affect requirements, design, verification, release, security, privacy, or legal gates should update the relevant document in the same change set. diff --git a/docs/governance/path-migration-map.md b/docs/governance/path-migration-map.md new file mode 100644 index 0000000..30cf68a --- /dev/null +++ b/docs/governance/path-migration-map.md @@ -0,0 +1,18 @@ +# Chanora Path Migration Map + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Current Migration State + +| README path | Current canonical or baseline path | Migration state | +|---|---|---| +| `docs/requirements/sysrs.md` | `docs/sysrs.md` | Path record points to canonical root file | +| `docs/requirements/srs.md` | `docs/srs.md` | Path record points to canonical root file | +| `docs/architecture/sysdes.md` | `docs/sysdes.md` | Canonical root file remains in use | +| `docs/architecture/sad.md` | `docs/architecture/sad.md` | Baseline candidate present | +| `docs/architecture/sdd.md` | `docs/architecture/sdd.md` | Baseline candidate present | + +## 2. Migration Rule + +Move canonical files only when references in README, traceability, verification, and governance documents are updated together. diff --git a/docs/governance/product-decision-register.md b/docs/governance/product-decision-register.md new file mode 100644 index 0000000..8c49e2e --- /dev/null +++ b/docs/governance/product-decision-register.md @@ -0,0 +1,22 @@ +# Chanora Product Decision Register + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Purpose + +This register records product and engineering decisions referenced by the DV document set. + +## 2. Decision Summary + +| Decision | State | DV impact | +|---|---|---| +| DEC-012 legal/trademark/OSS review | Open | Blocks public/store release | +| DEC-020 dual license MIT OR Apache-2.0 | Accepted per README | Supports license posture; dependency notices still require review | +| DEC-027 desktop mouse side-button PTT | Accepted by requirements baseline | Verification must not over-claim unsupported platform input classes | +| DEC-030 VAD deferral | Accepted as deferral | `VoiceActivity` remains disabled/coming-soon | +| DEC-032 Android CMake patch exit path | Active tracking | Patched dependency requires reevaluation | + +## 3. DV Rule + +Open decisions that affect release claims must appear in the waiver register or release-readiness record. diff --git a/docs/governance/repo-format-validation-report.md b/docs/governance/repo-format-validation-report.md new file mode 100644 index 0000000..0537b9c --- /dev/null +++ b/docs/governance/repo-format-validation-report.md @@ -0,0 +1,22 @@ +# Chanora Repository Format Validation Report + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Repository Layout Check + +| Area | Status | +|---|---| +| `docs/requirements/` | Path records present | +| `docs/architecture/` | SAD and SDD present | +| `docs/verification/` | Verification plan set present | +| `docs/release/` | Release readiness and policy records present | +| `docs/security/` | Security/privacy/legal guideline and audit records present | +| `docs/privacy/` | Privacy baseline present | +| `docs/legal/` | Trademark/attribution review present | +| `docs/governance/` | Governance records present | +| `docs/references/` | Reference records present | + +## 2. Validation Conclusion + +The repository format is sufficient for DV document navigation. Canonical SysRS/SysDes/SRS files still live at root `docs/` paths during migration. diff --git a/docs/governance/traceability-matrix.md b/docs/governance/traceability-matrix.md new file mode 100644 index 0000000..3e9e3a1 --- /dev/null +++ b/docs/governance/traceability-matrix.md @@ -0,0 +1,73 @@ +# Chanora Traceability Matrix + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Primary upstream documents:** `docs/sysrs.md`, `docs/sysdes.md`, `docs/srs.md`, `docs/architecture/sad.md`, `docs/architecture/sdd.md` + +## 1. Purpose + +This matrix gives DV reviewers a compact trace from system requirements to design allocation, software requirements, SWE.2/SWE.3 design baselines, and verification evidence. The detailed allocation records remain in `docs/sysdes.md`, `docs/srs.md`, `docs/architecture/sad.md`, and `docs/architecture/sdd.md`. + +## 2. Traceability Rule + +Chanora uses this document hierarchy: + +```text +SysRS -> SysDes -> SRS -> SAD -> SDD -> Verification +``` + +The current repository has SysRS, SysDes, SRS, SAD, and SDD baselines. The SAD and SDD are DV baseline candidates: they provide architecture and detailed-design coverage sufficient for review, while strict historical item numbering and deeper API-by-API detail remain follow-up hardening tasks. + +## 3. Baseline Coverage Summary + +| Layer | Coverage statement | Source | +|---|---|---| +| SysRS to SysDes | SysDes Appendix A provides allocation for every SysRS requirement | `docs/sysdes.md` Appendix A | +| SysDes to SRS | SRS reports 110 / 110 SysDes design items covered, with 95 / 110 software-impacting items represented | `docs/srs.md` section 9 | +| SRS to SAD | SAD section 3 allocates SRS groups to software architecture components | `docs/architecture/sad.md` | +| SAD to SDD | SDD section 11 maps SAD components to detailed design modules | `docs/architecture/sdd.md` | +| SDD to verification | Verification plans map SDD modules and SRS acceptance areas to SWE.4/SWE.5/SWE.6/SYS.4 evidence | `docs/verification/` | +| Release decision | Release readiness record maps evidence status to Go/No-Go decision | `docs/release/release-readiness-go-nogo-record.md` | +| Waivers | Waiver register maps known gaps to decision scope and unblock condition | `docs/release/dv-waiver-register.md` | + +## 4. Verification Handoff Trace + +| SysRS / SysDes source | SRS coverage | Verification plan | DV status | +|---|---|---|---| +| SysRS-233 traceability matrix | SRS-005, SRS-141 through SRS-143, SRS-180 through SRS-183 | Verification master plan and this matrix | Passed for DV baseline | +| SysRS-234 protocol compatibility | SRS-045 through SRS-053, SRS-131, SRS-134 | SWE.5, SWE.6, SYS.4 | Requires compatible-server evidence | +| SysRS-235 state synchronization verification | SRS-054 through SRS-061, SRS-124 | SWE.4, SWE.5, SWE.6 | Partial; reducer suite and event replay gaps recorded | +| SysRS-236 audio verification | SRS-062 through SRS-083, SRS-125, SRS-135 | SWE.4, SWE.5, SWE.6, SYS.4 | Partial; platform evidence required | +| SysRS-237 secure storage verification | SRS-090 through SRS-095, SRS-126, SRS-137 | SWE.4, SWE.5, SYS.4 | Partial; Android DEK waiver and platform audit required | +| SysRS-238 diagnostic redaction verification | SRS-093 through SRS-102, SRS-126 | SWE.4, SWE.5, SWE.6 | Evidence required before external tester enablement | +| SysRS-239 release packaging verification | SRS-116 through SRS-123, SRS-127, SRS-138 | SWE.5, SYS.4, release record | Partial; source-build/unsigned limitations recorded | +| SysRS-240 public wording audit | SRS-122, SRS-137 and legal review | SYS.4, legal/trademark review | Blocked by DEC-012 sign-off | +| SysDes-102 protocol verification handoff | SRS-004, SRS-015, SRS-045 through SRS-052, SRS-097, SRS-100, SRS-123, SRS-128, SRS-131, SRS-134 | SWE.5, SWE.6, SYS.4 | Requires attached candidate evidence | +| SysDes-103 state verification handoff | SRS-058 through SRS-061, SRS-097, SRS-098, SRS-124, SRS-128 | SWE.4, SWE.5 | Partial; waiver recorded | +| SysDes-104 audio verification handoff | SRS-062 through SRS-083, SRS-099, SRS-125, SRS-128, SRS-135 | SWE.4, SWE.5, SWE.6, SYS.4 | Partial; platform evidence required | +| SysDes-105 security verification handoff | SRS-090 through SRS-096, SRS-101, SRS-102, SRS-126, SRS-128, SRS-137 | SWE.4, SWE.5, SYS.4 | Partial; audits required | +| SysDes-106 deployment verification handoff | SRS-116 through SRS-123, SRS-127, SRS-128, SRS-138 | SWE.5, SYS.4, release record | Partial; release blockers recorded | +| SysDes-107 MVP acceptance verification | SRS-128 | SWE.6 | Matrix exists; candidate evidence still required | + +## 5. MVP Acceptance Trace + +| SysRS acceptance range | Requirement group | SRS anchor | Verification anchor | +|---|---|---|---| +| SysRS-241 through SysRS-244 | Connection, channel tree, online clients, channel join | SRS-019 through SRS-024, SRS-045 through SRS-051, SRS-128 | SWE.6 MVP acceptance matrix | +| SysRS-245 through SysRS-253 | Send/receive voice, mute/deaf, PTT, audio processing | SRS-062 through SRS-083, SRS-125, SRS-128, SRS-197 through SRS-200 | SWE.4/SWE.5 audio evidence and SWE.6 matrix | +| SysRS-254 | Channel text | SRS-019, SRS-036 through SRS-038, SRS-170 through SRS-174, SRS-128 | SWE.6 matrix | +| SysRS-255 | Bookmarks | SRS-084 through SRS-089, SRS-128 | SWE.4 storage tests and SWE.6 matrix | +| SysRS-256 | Secure storage | SRS-090 through SRS-095, SRS-126, SRS-137 | Security audit and SYS.4 platform matrix | +| SysRS-257 | Redacted diagnostics | SRS-028, SRS-093 through SRS-102, SRS-126, SRS-128 | SWE.6 diagnostics evidence and security review | + +## 6. Traceability Limitations for DV + +| Limitation | Impact | Control | +|---|---|---| +| SAD and SDD are baseline candidates rather than fully item-numbered historical documents | Some prior references such as `SAD-043` and `SDD-109` are not reconstructed as itemized records | Treat the new SAD/SDD as DV baselines; add strict item IDs later if the process owner requires ID-level audit | +| Candidate run IDs are not embedded in the documents | Evidence is not yet auditable to a specific build | Release-readiness record requires run IDs before release approval | +| Some implementation status items may have changed after the status report | Risk of stale blocker statements | Waiver register calls out items that need revalidation | + +## 7. DV Conclusion + +Traceability is sufficient for DV document review if reviewers accept the SAD/SDD baseline-candidate depth limitation and require candidate run evidence before release approval. diff --git a/docs/i18n/localization-architecture.md b/docs/i18n/localization-architecture.md new file mode 100644 index 0000000..d4d70d1 --- /dev/null +++ b/docs/i18n/localization-architecture.md @@ -0,0 +1,44 @@ +# Chanora Localization Architecture + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Implementation Source + +Localization is enabled through Flutter generation in `apps/chanora_flutter/pubspec.yaml` and generated files under `apps/chanora_flutter/lib/l10n/generated/`. + +## 2. Architecture Rules + +| Rule | Requirement | +|---|---| +| Product strings | User-visible product-owned strings should be localizable | +| Server content | Server names, channel names, nicknames, and messages are displayed as content, not translated | +| Unicode | UTF-8/Unicode content must be preserved through protocol, bridge, state, UI, logs, and diagnostics unless intentionally redacted | +| Fallback | Missing localization keys must have deterministic fallback behavior | +| Diagnostics | Machine-readable diagnostic keys remain language-neutral | + +## 3. Current Locale Baseline + +English and Simplified Chinese generated localization files are present in the current Flutter app tree. + +## 4. Ownership and Coverage Criteria + +| Criterion | Required result | +|---|---| +| Product-owned strings | New user-visible product strings are added to localization resources or explicitly justified as non-product content | +| Server-owned content | Server names, channel names, nicknames, and chat messages pass through untranslated | +| Fallback behavior | Missing-key behavior is deterministic and covered by test or generated-localization behavior review | +| Unicode preservation | Multilingual content is preserved across protocol, bridge, state, UI, logs, and diagnostics except where redaction intentionally removes content | +| Diagnostic keys | Machine-readable diagnostic keys remain stable and language-neutral | + +## 5. Evidence Required Before Release + +| Evidence | Purpose | +|---|---| +| Localization generation check | Confirms generated bindings are current | +| UI smoke in supported locales | Confirms critical screens render in English and Simplified Chinese | +| Unicode content test or demo | Confirms server-provided multilingual content is preserved | + +## 6. DV Conclusion + +Localization architecture is documented and reviewable. Additional locale expansion remains outside current release scope. diff --git a/docs/implementation-status-2026-05-28.md b/docs/implementation-status-2026-05-28.md new file mode 100644 index 0000000..1ba862d --- /dev/null +++ b/docs/implementation-status-2026-05-28.md @@ -0,0 +1,160 @@ +# Chanora Implementation Status — 2026-05-28 + +**Workspace version:** `v0.2.0-beta.1` +**CHANGELOG latest:** `v1.0.0-rc.1` +**Build status:** All 9 crates compile cleanly. + +--- + +## P0 / MVP + +### Done + +| Area | Evidence | +|---|---| +| App shell / startup | `main.dart` (2313 lines), `app_bootstrap.dart`, `RustLib.init()` wired | +| Flutter UI | Full widget set: `connect_widgets`, `snapshot_view`, `chat_views`, `voice_bar`, `voice_compact`, `voice_settings`, `voice_settings_controls`, `client_info_sheet`, `input_dialogs`, `bbcode_text` | +| Material 3 + design tokens | `chanora_tokens.dart`, `platform_capabilities.dart` | +| Localization (en + zh-Hans) | `l10n/generated/app_localizations_en.dart` + `app_localizations_zh.dart`, `l10n.yaml` | +| Flutter/Rust bridge | `chanora_bridge` crate (2152-line `api.rs`), generated `frb_generated.rs`, Dart side generated | +| Protocol adapter | `chanora_protocol` — `tsclientlib` isolated behind `ProtocolClient`, typed DTOs, `ProtocolError` catalogue | +| Connection lifecycle | `chanora_core` (2682-line `lib.rs`) — supervisor task, exponential backoff reconnect (1s→60s), user-disconnect suppresses reconnect | +| State sync reducer unit | `chanora_state` — `ConnectionState`, `channel_join`, snapshot/delta reducers, reconnect handling, deterministic ordering, malformed duplicate normalization, channel-delete/client cleanup, and reducer unit tests. Runtime core integration still uses snapshot/probe refresh paths and remains separate validation work. | +| Audio subsystem | `chanora_audio` — Opus encode/decode, HPF/NS/AEC3/AGC2 DSP, PTT backends (Windows/macOS/Linux/focused), iOS VoiceProcessingIO, Android Oboe, jitter buffer via `tsclientlib::audio::AudioHandler`, mixer, mute/deaf gates, release-tail timer, VAD | +| Push-to-talk | Per-platform backends: Windows Raw Input + hook fallback, macOS Event Tap, Linux freedesktop portal, focused fallback; `PttCapabilityLevel` (L0–L3); missed-key-up watchdog | +| Voice controls UI | `voice_bar`, `voice_compact`, `voice_haptics`, `voice_level_meter`, `voice_platform`, `ptt_capability_badge`, `talk_power_warning` | +| Storage (non-secret) | `chanora_storage` — `BookmarkRepository` (SQLite/rusqlite bundled, schema v2), ChaCha20-Poly1305 encrypted passwords | +| Storage (secrets) | `IdentityFileStore` with platform keyring (Linux Secret Service, macOS Keychain, Windows Credential Manager, iOS Keychain); file fallback with 0600 perms | +| Diagnostics | `chanora_diagnostics` — `Redactor` (IP/host/email/token/path/secret scrubbing), `InMemoryLogSink`, `DiagnosticExport` JSON bundle, `KnownSecretRegistry`, panic hook | +| Server address resolution | `chanora_resolver` — SRV/TSDNS/DNS fallback | +| Server prefetch | `chanora_server_prefetch` crate + `prefetch_debouncer.dart` — invisible host-field prefetch, TTL cache, generation-safe | +| Android platform | `android_voice_unit.rs`, `android_permissions_service.dart`, `MODE_IN_COMMUNICATION` routing, foreground service (`flutter_foreground_task`) | +| iOS platform | `ios_voice_unit.rs`, `ios_raw_unit.rs`, `ios_permissions_service.dart`, `AVAudioSession` integration, `audio_session` package | +| Permission UX | `permission_state_banner.dart`, pre-request explainers | +| Bookmark UI | Save/connect/delete in `connect_widgets.dart` | +| Channel join | Tap-to-join with optional password, `channel_join_error_mapper.dart`, `channel_spacer.dart` | +| Chat | `chat_views.dart`, BBCode rendering (`bbcode_text.dart`) | +| Audio settings UI | `voice_settings.dart`, `voice_settings_controls.dart`, `audio_processing_config_state.dart`, `audio_device_list_tile.dart`, `audio_output_tile.dart` | +| Audio debug stats | `audio_debug_stats_panel.dart` | +| TS3 server link | `ts3_server_link.dart` — `ts3server://` URI parsing | +| UI preferences | `ui_preferences_service.dart`, `shared_preferences` | +| Connection phase state | `connection_phase_state.dart` | +| Snapshot state mapper | `snapshot_state_mapper.dart` | +| Back intent (Android) | `back_intent_policy.dart`, `back_intent_service.dart` | +| Audio lifecycle | `audio_lifecycle_service.dart` | +| Link trust | `link_trust_service.dart` | +| About dialog | Non-affiliation statement, dual-license declaration, NOTICE pointer | +| CI | GitHub Actions on every push | +| Workspace compiles | All 9 crates build cleanly | + +### Partial / Scaffold Only + +| Area | Gap | +|---|---| +| Event replay tooling | Reducer tests cover the state-sync contract, but standalone replay-file tooling remains a P1 verification gap. | +| Reducer runtime integration evidence | The standalone reducer is unit-tested, but `chanora_core` still refreshes UI state through snapshot/probe paths rather than folding all live protocol events through `chanora_state::reduce`. | +| Silero VAD | `assets/models/silero_vad.onnx` bundled but DEC-030 defers VAD to P1; `TransmitMode::VoiceActivity` is reserved and disabled in this baseline. | +| macOS build | Not in `v1.0.0-rc.1` release artifacts (source-buildable only per `staged-release-plan.md`). | +| Windows build | Same — source-buildable, not in rc.1 release artifacts. | +| iOS build | Same — source-buildable, not in rc.1 release artifacts. | + +### Not Done (P0 blockers remaining) + +| Item | Status | +|---|---| +| DEC-012 legal/trademark/OSS review | Explicitly open — `v1.0.0-rc.1` is the candidate awaiting sign-off. Public release is blocked. | +| Android Keystore-backed DEK | Deferred to v1.1. Android still uses file-fallback for the Data Encryption Key. | +| iOS `AVAudioSession.Mode.voiceChat` | Implemented in `apps/chanora_flutter/ios/Runner/AppDelegate.swift`; release readiness still requires device audio validation and candidate evidence attachment. | +| Candidate state-sync evidence attachment | Reducer tests exist and pass locally; release readiness still needs candidate CI/run IDs and runtime integration evidence attached before public release approval. | + +--- + +## P1 / Beta + +### Done (promoted from Beta work) + +| Area | Evidence | +|---|---| +| Diagnostics export UI | Diagnostics dialog in `main.dart`, `share_plus` for export | +| Reconnect banner | Referenced in CHANGELOG v0.4 | +| Identity persistence | `IdentityFileStore` shipped | +| Audio loopback/processing test hooks | `compare_baseline.rs`, `emit_baseline.rs` examples; `ptt_privacy.rs`, `linux_portal_smoke.rs` tests | +| Opus codec benchmarks | `benches/opus_codec.rs`, `benches/resampler.rs`, `benches/realtime_capture.rs` | +| Audio processing backend abstraction | `processor/mod.rs` with `sonora`, `webrtc_apm`, `noop` backends | +| Per-user mute | SRS-074 implemented in audio gate | +| Protocol adapter isolation | SRS-053 trait boundary in place | + +### Not Done (P1 backlog) + +| Item | Notes | +|---|---| +| Per-user volume (SRS-075) | Not yet wired to UI/storage | +| Recent servers persistence (SRS-085) | Not confirmed in storage crate | +| UI settings persistence (SRS-087) | Implemented for current P1 scope using `shared_preferences`: host, nickname, permission explanation flag, and theme mode (`system` / `light` / `dark`). SQLite-backed UI settings remain a future hardening option if multi-profile or transactional settings are introduced. | +| Event replay tool (SRS-061, SRS-098) | No replay infrastructure found | +| Network diagnostics (SRS-100) | Not found in diagnostics export | +| Side navigation rail for medium layout (SRS-153) | Not confirmed | +| Keyboard focus traversal (SRS-160) | Not confirmed | +| Android audio focus / BT route changes (SRS-112) | Partial — `MODE_IN_COMMUNICATION` done; full focus/BT handling not confirmed | +| Windows installer packaging (SRS-116) | Not in rc.1 artifacts | +| Linux packaging (AppImage/Flatpak/deb/rpm) (SRS-118) | Not confirmed | +| Android AAB release build pipeline (SDD-109) | Referenced but not confirmed as CI-automated | +| iOS TestFlight/App Store build pipeline (SRS-120) | Deferred | +| Light/dark theme toggle (SAD-043) | Not confirmed in UI | +| Audio device hot-plug recovery (SRS-082) | Noted as follow-up work in `chanora_audio/src/lib.rs` | + +--- + +## P2 / Production + +### Not Done (all P2 items pending) + +| Item | Notes | +|---|---| +| DEC-012 legal sign-off | Hard blocker for public release | +| macOS signed + notarized builds (SRS-117) | Requires macOS build host | +| C4 architecture views in SAD (SAD-046) | Documentation artifact | +| ADRs for significant decisions (SAD-047) | Documentation artifact | +| Architecture glossary (SAD-055, SDD-063) | Documentation artifact | +| Staged rollout plan | `staged-release-plan.md` referenced but not confirmed complete | +| Bidirectional text (SRS-175) | Deferred | +| Expanded layout persistent side panes (SRS-154) | Deferred | + +--- + +## SOP (Standing Operating Procedures) + +### In Place + +- Agent router + phase spec docs (in git history, currently untracked/renamed) +- `phase_index.json` machine-readable requirement index +- Conventional Commits style enforced +- Privacy/security gates documented (`SOP_AGENT_OPERATIONS.md`) +- Traceability chain: SysRS → SysDes → SRS → SAD → SDD +- `SECURITY.md`, `CONTRIBUTING.md`, `NOTICE`, dual-license files present + +### Needs Attention + +- The agent spec docs (`P0_MVP_AGENT_SPEC.md`, `P1_BETA_AGENT_SPEC.md`, etc.) are deleted from the working tree but still in git HEAD. The new `docs/srs.md`, `docs/sysdes.md`, `docs/sysrs.md` are untracked. Docs reorganization in progress — files need to be committed or deletions reverted. + +--- + +## Summary + +``` +P0 / MVP: ~85% done. Core product works end-to-end (connect, voice, chat, + bookmarks, storage, diagnostics). Main blockers: DEC-012 legal + review (hard gate), Android Keystore DEK, iOS voiceChat evidence, + and candidate evidence attachment. + +P1 / Beta: ~40% done. Audio processing backend, diagnostics export, and + loopback tests are in. Per-user volume, event replay, network + diagnostics, packaging pipelines, and several UI hardening items + remain. + +P2 / Prod: ~5% done. Blocked on P0 legal gate. Documentation artifacts + (C4 views, ADRs, glossary) and production signing pipelines + not started. + +SOP: Docs in place but a working-tree reorganization is uncommitted. +``` diff --git a/docs/legal/trademark-and-attribution-review.md b/docs/legal/trademark-and-attribution-review.md new file mode 100644 index 0000000..c7bd928 --- /dev/null +++ b/docs/legal/trademark-and-attribution-review.md @@ -0,0 +1,47 @@ +# Chanora Trademark and Attribution Review + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Decision state:** DEC-012 remains open; public/store release blocked + +## 1. Purpose + +This document records the legal/trademark/attribution checks needed before Chanora can be publicly distributed. It supports DV review by making the open DEC-012 gate explicit. + +## 2. Non-Affiliation Position + +Chanora must present itself as an independent client application. It may describe compatibility with TeamSpeak 3-compatible servers where accurate, but it must not imply that Chanora is official, endorsed, sponsored, or affiliated with TeamSpeak or any trademark owner. + +## 3. Required Wording Controls + +| Surface | Required control | +|---|---| +| App About dialog | Include independent/non-affiliation wording and OSS license pointer | +| README and documentation | Use compatibility wording rather than official-product wording | +| Store metadata | Avoid official affiliation claims and review trademark usage | +| Release notes | Match actual supported capability and platform limitations | +| Website or marketing copy | Require legal review before publication | + +## 4. Attribution and OSS Controls + +| Topic | Required result before public/store release | +|---|---| +| Dependency license inventory | Rust and Flutter inventories generated and reviewed | +| NOTICE file | Required third-party attributions included | +| Dual-license statement | Chanora MIT OR Apache-2.0 licensing remains visible | +| Patched/forked dependencies | Forks and pinned revisions are documented with rationale | +| Trademark references | References are nominative, accurate, and reviewed | + +## 5. Current Review State + +| Item | State | Release impact | +|---|---|---| +| DEC-012 legal/trademark/OSS review | Open | Blocks public/store release | +| About dialog non-affiliation statement | Reported implemented in `docs/implementation-status-2026-05-28.md` | Needs final wording review | +| NOTICE pointer | Reported implemented in About dialog | Needs inventory/legal confirmation | +| Store metadata | Not approved in current record | Blocks store release | +| Public compatibility wording | Not approved in current record | Blocks public release | + +## 6. DV Conclusion + +Legal/trademark/attribution requirements are identified, but DEC-012 is not closed. DV may pass the document baseline only if public/store release remains No-Go. diff --git a/docs/additional_docs/material3-guideline.md b/docs/material3-guideline.md similarity index 100% rename from docs/additional_docs/material3-guideline.md rename to docs/material3-guideline.md diff --git a/docs/phase_index.json b/docs/phase_index.json deleted file mode 100644 index cc27510..0000000 --- a/docs/phase_index.json +++ /dev/null @@ -1,7684 +0,0 @@ -{ - "product": "Chanora", - "load_rule": "Load 00_AGENT_ROUTER.md + one phase file + SOP only if needed.", - "counts": { - "P0": 500, - "P1": 81, - "P2": 11, - "SOP": 310 - }, - "accepted_mvp_defaults": { - "Release sequence": "Internal Alpha → External Beta → MVP Public", - "MVP platforms": "Windows, macOS, Linux, Android, iOS (staged release allowed)", - "Active connections": "One active server connection", - "UI design system": "Material 3 + Chanora Design System", - "Product language": "MODIFIED — English + Chinese (Simplified) at MVP; i18n-ready architecture", - "Server content": "Preserve and display Unicode; do not translate", - "Diagnostics": "Local, user-initiated export only", - "Telemetry": "None", - "Crash reporting": "None unless later approved", - "Secret storage": "Platform secure storage", - "Non-secret local storage": "SQLite (rusqlite bundled)", - "Audio processing": "Platform-native first; fallback strategy documented", - "AEC": "Enabled by default where supported", - "AGC": "Enabled by default (user-toggleable)", - "Noise suppression": "Enabled by default (user-toggleable)", - "High-pass filter": "Enabled by default", - "Android minimum": "MODIFIED — API 28 (Android 9.0), raised from the original recommendation of API 24", - "Android target": "API 35 or newer per current Google Play policy on upload date", - "iOS minimum": "iOS 13", - "Apple App Store SDK gate": "Xcode 26+ / iOS 26 SDK+ for uploads on or after 2026-04-28", - "Audio crate": "cpal (desktop, Android); iOS deferred", - "Bridge": "Stable typed Flutter/Rust bridge; flutter_rust_bridge 2.x pinned", - "Implementation directory layout": "apps/chanora_flutter/, core/chanora_core/, crates/chanora_*", - "License": "Dual-licensed under Apache-2.0 OR MIT, recipient's choice (DEC-020)" - }, - "decisions": [ - { - "id": "DEC-001", - "decision": "Release type", - "accepted_value": "Internal Alpha first, then External Beta, then MVP Public / Store Release", - "status": "Accepted" - }, - { - "id": "DEC-002", - "decision": "MVP platform scope", - "accepted_value": "MVP target remains Windows, macOS, Linux, Android, and iOS; first release may be staged by channel/platform", - "status": "Accepted" - }, - { - "id": "DEC-003", - "decision": "Minimum iOS version", - "accepted_value": "iOS 13 minimum for Flutter support baseline; test latest iOS release separately", - "status": "Accepted" - }, - { - "id": "DEC-004", - "decision": "Minimum Android version", - "accepted_value": "**Android API 28 (Android 9.0)** minimum, raised from the original recommendation of API 24 by explicit owner ruling on 2026-05-14. Rationale: simplifies the audio path (AAudio is unconditionally available from API 26+ and stable from API 28), narrows the TLS / privacy / scoped-storage compatibility surface, and matches typical 2026 Android baselines. The original cpal-on-Oboe Android spike was built with `minSdk = 24` and `cargo-ndk -P 26`; product code in `apps/chanora_flutter` uses `minSdk = 28` and the Android product audio backend is now direct Oboe, so Chanora no longer carries the API 24/25 OpenSL ES fallback branch.", - "status": "Accepted" - }, - { - "id": "DEC-005", - "decision": "Android target SDK", - "accepted_value": "Target the Google Play-required API level on the upload date; current release gate uses API 35+ unless newer Google policy applies", - "status": "Accepted" - }, - { - "id": "DEC-006", - "decision": "Multiple server connections in MVP", - "accepted_value": "Not in MVP; support one active server connection per client instance", - "status": "Accepted" - }, - { - "id": "DEC-007", - "decision": "AEC default state", - "accepted_value": "Enabled by default on platforms/audio backends where supported and stable", - "status": "Accepted" - }, - { - "id": "DEC-008", - "decision": "AGC default state", - "accepted_value": "Enabled by default, with user setting to disable", - "status": "Accepted" - }, - { - "id": "DEC-009", - "decision": "Noise suppression default state", - "accepted_value": "Enabled by default, with user setting to disable", - "status": "Accepted" - }, - { - "id": "DEC-010", - "decision": "High-pass filter default state", - "accepted_value": "Enabled by default", - "status": "Accepted" - }, - { - "id": "DEC-011", - "decision": "Audio processing implementation path", - "accepted_value": "Use platform-native audio processing first where available; use Rust/WebRTC-style processing as controlled fallback or later architecture option", - "status": "Accepted" - }, - { - "id": "DEC-011.1", - "decision": "Audio crate choice", - "accepted_value": "`cpal` for desktop (empirically verified on Linux/PipeWire by `poc/audio-capture-playback-spike` on 2026-05-13) and direct `oboe-rs` for Android product code. The earlier cpal-on-Oboe spike remains evidence that Rust audio reaches Oboe/AAudio on a Motorola Moto G Stylus 5G (2023) running Android 14 arm64-v8a, but product code chooses direct Oboe because P0 Android needs explicit `VoiceCommunication` input preset, `VoiceCommunication`/`Speech` output attributes, low-latency/exclusive stream requests with shared fallback, audio-session ids for hardware AEC/NS/AGC binding, and diagnostics of requested/achieved stream values. iOS crate TBD pending an iOS spike that requires macOS + Xcode hardware", - "status": "Accepted (desktop + Android) / Deferred (iOS)" - }, - { - "id": "DEC-012", - "decision": "Official SDK / trademark / licensing review", - "accepted_value": "Public/store release is blocked until legal confirms TeamSpeak non-affiliation wording, trademark usage, OSS licenses, and `tsclientlib` license posture", - "status": "Accepted (as a release gate)" - }, - { - "id": "DEC-013", - "decision": "Local database choice", - "accepted_value": "Use SQLite or equivalent embedded local database for non-secret local state; secrets remain in platform secure storage", - "status": "Accepted" - }, - { - "id": "DEC-013.1", - "decision": "SQLite crate", - "accepted_value": "`rusqlite` with the `bundled` feature (SQLite statically linked into the binary; no system libsqlite3 dependency); verified by `poc/sqlite-storage-spike` on 2026-05-13", - "status": "Accepted" - }, - { - "id": "DEC-013.2", - "decision": "Linux secure-storage backend policy", - "accepted_value": "Prefer Secret Service (libsecret / gnome-keyring / kwallet / KeePassXC) on Linux; if the default collection is locked or D-Bus is unavailable, fall back to kernel keyutils with a clear user notice. Both backends are \"equivalent\" per SysRS-053 / SysRS-162; verified by `poc/secure-storage-spike` on 2026-05-13", - "status": "Accepted" - }, - { - "id": "DEC-014", - "decision": "Bridge choice", - "accepted_value": "Use a stable typed Flutter/Rust bridge with generated or schema-controlled DTOs; **`flutter_rust_bridge` 2.x pinned** (empirically verified at 2.12.0 by `poc/flutter_rust_bridge_hello` on 2026-05-13)", - "status": "Accepted" - }, - { - "id": "DEC-015", - "decision": "Product language for MVP", - "accepted_value": "**English + Chinese (Simplified) for MVP**, raised from the original recommendation of English-only by explicit owner ruling on 2026-05-14. Rationale: the demonstrated test-server population (verified live against `cn.teamspeak.app`) and broader TS3 audience include substantial Chinese-speaking users; shipping zh-Hans alongside en at MVP avoids a launch-window UX gap. Architecture remains i18n-ready so additional languages can be added later mechanically. Server-provided content is preserved verbatim and never translated (ADR-008 UTF-8 boundary, DEC-015 server-content rule retained).", - "status": "Accepted" - }, - { - "id": "DEC-016", - "decision": "Diagnostics upload policy", - "accepted_value": "No automatic upload for MVP; user-initiated local diagnostic export only", - "status": "Accepted" - }, - { - "id": "DEC-017", - "decision": "Crash reporting", - "accepted_value": "Disabled for MVP unless explicit opt-in provider and privacy policy are approved", - "status": "Accepted" - }, - { - "id": "DEC-018", - "decision": "Public product name", - "accepted_value": "Chanora", - "status": "Accepted" - }, - { - "id": "DEC-019", - "decision": "Public non-affiliation statement", - "accepted_value": "Use legal-approved wording; drafted text accepted as working copy: \"Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.\" Subject to final legal review under DEC-012 before public release.", - "status": "Accepted (drafted wording)" - }, - { - "id": "DEC-021", - "decision": "Apple App Store submission SDK", - "accepted_value": "Use Xcode 26 or later and the iOS 26 / iPadOS 26 SDK or later for App Store submission on or after 2026-04-28, unless Apple publishes a newer applicable requirement before upload", - "status": "Accepted" - }, - { - "id": "DEC-020", - "decision": "License model", - "accepted_value": "**Dual-licensed under Apache-2.0 OR MIT (recipient's choice)**, the standard Rust-ecosystem permissive license model. Accepted on 2026-05-14. Compatible with every direct dependency in the PoC tree (`tsclientlib` MIT-OR-Apache-2.0, `flutter_rust_bridge` MIT, `cpal` Apache-2.0, `rusqlite` MIT, `keyring` MIT-OR-Apache-2.0, etc.) and with the Flutter framework's BSD-3-Clause. The license texts ship as `LICENSE-APACHE` and `LICENSE-MIT` at the repository root; an aggregator `LICENSE` points to both. `NOTICE` enumerates current direct-dependency attributions. The full OSS legal review (transitive deps, license obligations, OSS notices) remains under DEC-012 and is still required before public release.", - "status": "Accepted" - }, - { - "id": "DEC-022", - "decision": "Canonical implementation directory layout", - "accepted_value": "Accept the README's sketch as canonical: `apps/chanora_flutter/`, `core/chanora_core/`, `crates/chanora_protocol/`, `crates/chanora_audio/`, `crates/chanora_state/`, `crates/chanora_storage/`, `crates/chanora_diagnostics/`, `crates/chanora_bridge/`. Matches SAD §7.2 module decomposition", - "status": "Accepted" - }, - { - "id": "DEC-023", - "decision": "Windows desktop Global PTT priority", - "accepted_value": "**P0 in MVP.** Resolves PTT-OPEN-001 from `gen2/chanora-desktop-ptt-review-summary-v0.9.2.md`. The Windows backend ladder (Raw Input → low-level keyboard hook → Focused fallback) is mandatory for the first public release; release notes shall not claim Global PTT support on Windows until live measurement confirms a Global level from a non-fallback rung.", - "status": "Accepted" - }, - { - "id": "DEC-024", - "decision": "macOS desktop Global PTT priority", - "accepted_value": "**P0 in MVP, with explicit permission UX flow.** Resolves PTT-OPEN-002. The macOS backend shall request the Input Monitoring / Accessibility permission, accept user denial gracefully (continue at `L0Focused` without functional regression), and upgrade to Global asynchronously when the user grants the permission.", - "status": "Accepted" - }, - { - "id": "DEC-025", - "decision": "Officially-tested Linux environment for first public release", - "accepted_value": "**GNOME on Wayland (only).** Resolves PTT-OPEN-003. The Linux backend shall use the freedesktop `org.freedesktop.portal.GlobalShortcuts` interface on GNOME-on-Wayland and fall back to Focused PTT on every other Linux environment (X11, sway, KDE, untested compositors). Release notes shall not claim Global PTT support outside the tested compositor.", - "status": "Accepted" - }, - { - "id": "DEC-026", - "decision": "Mouse side-button support in first desktop PTT release", - "accepted_value": "**Supported on Windows and macOS; Linux follows whatever the GlobalShortcuts portal exposes.** Resolves PTT-OPEN-004. The Raw Input backend (Windows) and the Event Tap backend (macOS) shall accept Mouse4 / Mouse5 bindings; the Linux portal binding accepts whatever the session exposes and the release notes shall not over-claim.", - "status": "Accepted" - }, - { - "id": "DEC-027", - "decision": "PTT diagnostics privacy posture", - "accepted_value": "**Capability and availability state only — no raw key codes ever leave the device.** Resolves PTT-OPEN-005. The diagnostic export shall name only `PttCapabilityLevel`, `backend_id`, `bound_input_class`, and `fallback_exercised`; the user's actual key value (scan code, virtual key, keysym) shall never be logged, persisted, or exported. `PttSanitizer` enforces this at write time.", - "status": "Accepted" - }, - { - "id": "DEC-028", - "decision": "Missed-key-up watchdog requirement", - "accepted_value": "**P0.** Resolves PTT-OPEN-006. The audio engine shall include a missed-key-up watchdog that clears `transmit_active` after a configured ceiling (default 30 s) when no key-up event arrives. The watchdog is a P0 release-gate item rather than an implementation-level concern because the failure mode (stuck transmission after the user has released the binding) is user-visible and reputation-relevant.", - "status": "Accepted" - }, - { - "id": "DEC-029", - "decision": "Flutter global-hotkey packages rejected for PTT", - "accepted_value": "**Use the native Rust `DesktopPttBackend` trait + per-OS implementations (already specified in SDD-083 / SDD-084 / SDD-085); do not adopt `hotkey_manager`, `super_hot_key`, or any equivalent pub.dev package for PTT.** Rationale: those packages wrap the OS `RegisterHotKey` / `RegisterEventHotKey` semantics — they consume the key (suppressing it from other applications), they do not deliver a key-up event, and they do not support mouse side-buttons. PTT requires the opposite primitive (observe, do not consume, deliver both up and down).", - "status": "Accepted" - }, - { - "id": "DEC-030", - "decision": "Voice activity detection deferred to P1", - "accepted_value": "**`TransmitMode::VoiceActivity` ships as a reserved enum variant with no v1 implementation.** The settings UI shows it as a disabled \"coming soon\" option. The actual implementation choice is deferred to a future baseline. Rationale: three viable backends were compared (RMS energy threshold — trivial but quality-poor; WebRTC VAD via the `webrtc-vad` crate — frozen-but-stable C++ BSD-3 dep, ~200 KB binary, industry baseline; Silero VAD via ONNX Runtime — best quality but ~8–16 MB binary uplift per platform plus an ONNX-runtime dependency surface). The v1 dependency-surface budget does not have room for the trade-off review required to pick correctly. Choosing too early risks either user-visible quality regression (RMS) or a forced ONNX adoption (Silero) before there is a comparable need for ML inference elsewhere (noise suppression, AEC).", - "status": "Accepted" - }, - { - "id": "DEC-031", - "decision": "Missed-key-up watchdog disabled on P0; redesign deferred to P1", - "accepted_value": "**The `MissedKeyUpWatchdog` is constructed and unit-tested but is NOT spawned by `ChanoraSession::start_audio` in the P0 baseline.** Supersedes DEC-028 for the v1 ship. Rationale: the original 30 s ceiling cut real users off mid-sentence when speaking through PTT for longer than the timeout — Mumble and TeamSpeak do not ship a comparable watchdog, so the protection is stricter than industry baseline while imposing a real UX cost. The watchdog's purpose (catching OS-level key-up loss when the app loses focus / is minimised / hits App Nap) remains valid; the fixed-timeout shape is the wrong implementation. P1 will reintroduce a redesigned variant using one of: (a) raised ceiling (~5 min, owner-tunable) only, (b) active OS-level key-state polling via `GetAsyncKeyState` / `CGEventSourceKeyState` / `XQueryKeymap` so we detect the actual OS desync directly, (c) audio-activity (RMS-silence) fallback once the level meter lands, or (d) a combination. The Rust unit tests for `MissedKeyUpWatchdog::spawn_on_signal` remain in `crates/chanora_audio/src/ptt.rs` so the P1 re-enable is non-breaking.", - "status": "Accepted" - }, - { - "id": "DEC-001", - "decision": "Product Owner", - "accepted_value": "Release type sequence", - "status": "Accepted" - }, - { - "id": "DEC-002", - "decision": "Product Owner / Engineering", - "accepted_value": "MVP platform strategy", - "status": "Accepted" - }, - { - "id": "DEC-003", - "decision": "Product Owner / iOS Owner", - "accepted_value": "Minimum iOS version", - "status": "Accepted" - }, - { - "id": "DEC-004", - "decision": "Product Owner / Android Owner", - "accepted_value": "Minimum Android version", - "status": "Accepted" - }, - { - "id": "DEC-005", - "decision": "Android Owner / Release Manager", - "accepted_value": "Android target SDK", - "status": "Accepted" - }, - { - "id": "DEC-006", - "decision": "Product Owner / Software Architect", - "accepted_value": "Multiple server connections in MVP", - "status": "Accepted" - }, - { - "id": "DEC-007", - "decision": "Audio Owner / Product Owner", - "accepted_value": "AEC default", - "status": "Accepted" - }, - { - "id": "DEC-008", - "decision": "Audio Owner / Product Owner", - "accepted_value": "AGC default", - "status": "Accepted" - }, - { - "id": "DEC-009", - "decision": "Audio Owner / Product Owner", - "accepted_value": "Noise suppression default", - "status": "Accepted" - }, - { - "id": "DEC-010", - "decision": "Audio Owner", - "accepted_value": "High-pass filter default", - "status": "Accepted" - }, - { - "id": "DEC-011", - "decision": "Software Architect / Audio Owner", - "accepted_value": "Audio processing path", - "status": "Accepted" - }, - { - "id": "DEC-011.1", - "decision": "Software Architect / Audio Owner", - "accepted_value": "Audio crate (desktop / mobile)", - "status": "Accepted (desktop: `cpal`; Android product: direct `oboe-rs`) / Deferred (iOS)" - }, - { - "id": "DEC-012", - "decision": "Legal / Compliance", - "accepted_value": "SDK/trademark/licensing review", - "status": "Accepted as a release gate" - }, - { - "id": "DEC-013", - "decision": "Software Architect / Storage Owner", - "accepted_value": "Local database", - "status": "Accepted" - }, - { - "id": "DEC-013.1", - "decision": "Software Architect / Storage Owner", - "accepted_value": "SQLite crate", - "status": "Accepted (`rusqlite` bundled)" - }, - { - "id": "DEC-013.2", - "decision": "Software Architect / Storage Owner / Security Reviewer", - "accepted_value": "Linux secure-storage backend policy", - "status": "Accepted (Secret Service preferred, keyutils fallback)" - }, - { - "id": "DEC-014", - "decision": "Software Architect", - "accepted_value": "Bridge choice", - "status": "Accepted (`flutter_rust_bridge` 2.x pinned)" - }, - { - "id": "DEC-015", - "decision": "Product Owner", - "accepted_value": "Product language for MVP", - "status": "Accepted" - }, - { - "id": "DEC-016", - "decision": "Product Owner / Legal / Security", - "accepted_value": "Diagnostics upload policy", - "status": "Accepted" - }, - { - "id": "DEC-017", - "decision": "Product Owner / Legal / Security", - "accepted_value": "Crash reporting", - "status": "Accepted" - }, - { - "id": "DEC-018", - "decision": "Product Owner", - "accepted_value": "Public product name", - "status": "Accepted" - }, - { - "id": "DEC-019", - "decision": "Legal / Compliance", - "accepted_value": "Public non-affiliation statement", - "status": "Accepted (drafted wording)" - }, - { - "id": "DEC-020", - "decision": "Product Owner / Legal", - "accepted_value": "License model", - "status": "Accepted (Apache-2.0 OR MIT dual-license)" - }, - { - "id": "DEC-021", - "decision": "iOS Owner / Release Manager", - "accepted_value": "Apple App Store SDK gate", - "status": "Accepted" - }, - { - "id": "DEC-022", - "decision": "Software Architect", - "accepted_value": "Canonical implementation directory layout", - "status": "Accepted (README sketch)" - }, - { - "id": "DEC-023", - "decision": "Product Owner / Windows Platform Owner", - "accepted_value": "Windows desktop Global PTT priority", - "status": "Accepted (P0 / MVP)" - }, - { - "id": "DEC-024", - "decision": "Product Owner / macOS Platform Owner", - "accepted_value": "macOS desktop Global PTT priority", - "status": "Accepted (P0 / MVP, permission-UX required)" - }, - { - "id": "DEC-025", - "decision": "Product Owner / Linux Platform Owner", - "accepted_value": "Officially-tested Linux environment", - "status": "Accepted (GNOME on Wayland only)" - }, - { - "id": "DEC-026", - "decision": "Product Owner / UX Owner", - "accepted_value": "Mouse side-button support", - "status": "Accepted (Windows + macOS; Linux portal-dependent)" - }, - { - "id": "DEC-027", - "decision": "Security / Privacy Reviewer", - "accepted_value": "PTT diagnostics privacy posture", - "status": "Accepted (capability + availability only, no key codes)" - }, - { - "id": "DEC-028", - "decision": "Audio Owner / Software Architect", - "accepted_value": "Missed-key-up watchdog", - "status": "Accepted (P0)" - }, - { - "id": "DEC-029", - "decision": "Software Architect / Audio Owner", - "accepted_value": "Flutter global-hotkey packages rejected for PTT", - "status": "Accepted" - }, - { - "id": "DEC-030", - "decision": "Audio Owner / Product Owner", - "accepted_value": "Voice activity detection deferred to P1", - "status": "Accepted (deferred to P1)" - }, - { - "id": "DEC-031", - "decision": "Audio Owner / Product Owner", - "accepted_value": "Missed-key-up watchdog disabled on P0; redesign deferred to P1", - "status": "Accepted (supersedes DEC-028 for P0)" - }, - { - "id": "DEC-032", - "decision": "Android `abiFilters` set during P0 smoke-test cycle", - "accepted_value": "Canonical three-ABI set `{arm64-v8a, armeabi-v7a, x86_64}` (per SDD-073 item 4 / SDD-118 item 3) restored 2026-05-18 after the `audiopus_sys` + `cmake-rs` + NDK toolchain-file `ANDROID_ABI` propagation gap was closed via a workspace `[patch.crates-io]` override pinning `cmake` to a fork carrying cmake-rs PR #257", - "status": "**Resolved**" - }, - { - "id": "DEC-032", - "decision": "Build/Toolchain", - "accepted_value": "Temporary reduction of Android `abiFilters` to `arm64-v8a` only during P0 smoke-test cycle", - "status": "Resolved (2026-05-18)" - } - ], - "requirements_by_phase": { - "P0": [ - { - "id": "SAD-001", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell", - "verification": "Review, Inspection", - "statement": "The architecture shall use a Flutter App Shell as the top-level software composition root." - }, - { - "id": "SAD-002", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Review, Widget Test", - "statement": "The architecture shall use a Chanora Design System package above Material 3." - }, - { - "id": "SAD-003", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Feature Modules, Design System", - "verification": "Static Inspection", - "statement": "The architecture shall isolate feature screens from direct theme-token construction." - }, - { - "id": "SAD-004", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Unit Test", - "statement": "The architecture shall implement connection-state token mapping in the design system." - }, - { - "id": "SAD-005", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Unit Test", - "statement": "The architecture shall implement voice-state token mapping in the design system." - }, - { - "id": "SAD-006", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Unit Test", - "statement": "The architecture shall implement latency, packet-loss, diagnostics, and error token mapping." - }, - { - "id": "SAD-007", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Widget Test", - "statement": "The architecture shall expose tokens through Flutter theming mechanisms." - }, - { - "id": "SAD-008", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell", - "verification": "Widget Test, Integration Test", - "statement": "The architecture shall provide an Adaptive Shell that owns window-class resolution." - }, - { - "id": "SAD-009", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Adaptive Shell", - "verification": "Integration Test", - "statement": "The architecture shall implement a compact layout branch." - }, - { - "id": "SAD-010", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Adaptive Shell", - "verification": "Integration Test", - "statement": "The architecture shall implement a medium layout branch." - }, - { - "id": "SAD-011", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Adaptive Shell", - "verification": "System Test", - "statement": "The architecture shall implement an expanded layout branch." - }, - { - "id": "SAD-012", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Components, Feature Modules", - "verification": "Widget Test", - "statement": "The architecture shall implement a Connection Status UI component." - }, - { - "id": "SAD-013", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Components, Voice Feature", - "verification": "Widget Test, Integration Test", - "statement": "The architecture shall implement a persistent VoiceBar component." - }, - { - "id": "SAD-014", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Components, Localization", - "verification": "Accessibility Test", - "statement": "The architecture shall implement accessible semantics for critical controls." - }, - { - "id": "SAD-015", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "Widget Test, System Test", - "statement": "The architecture shall implement keyboard focus traversal for desktop and tablet keyboard use." - }, - { - "id": "SAD-016", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Adaptive Shell, Components", - "verification": "Accessibility Test", - "statement": "The architecture shall protect critical controls under increased text scaling." - }, - { - "id": "SAD-017", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Platform Services", - "verification": "Platform Test", - "statement": "The architecture shall centralize safe-area and inset handling." - }, - { - "id": "SAD-018", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Platform Services", - "verification": "Platform Test", - "statement": "The architecture shall model Android back behavior through platform services." - }, - { - "id": "SAD-019", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Platform Services", - "verification": "Platform Test", - "statement": "The architecture shall model iOS navigation, keyboard avoidance, safe area, and haptics through platform services." - }, - { - "id": "SAD-020", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Localization Service", - "verification": "Unit Test, Integration Test", - "statement": "The architecture shall implement a Localization Service for product-owned strings." - }, - { - "id": "SAD-021", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Localization Service, Protocol DTOs, Flutter UI", - "verification": "Integration Test", - "statement": "The architecture shall separate product localization from server-provided content." - }, - { - "id": "SAD-022", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Bridge, Rust Core, Flutter UI", - "verification": "Integration Test", - "statement": "The architecture shall preserve Unicode through protocol, bridge, state, and UI paths." - }, - { - "id": "SAD-023", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Platform Adapter, Bridge", - "verification": "Inspection, Integration Test", - "statement": "The architecture shall isolate non-UTF-8 conversion in boundary adapters." - }, - { - "id": "SAD-024", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Storage", - "verification": "Test, Audit", - "statement": "The architecture shall preserve multilingual content in diagnostics except for intentional redaction." - }, - { - "id": "SAD-025", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "Localization Test", - "statement": "The architecture shall support bidirectional text where the platform text engine supports it." - }, - { - "id": "SAD-026", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Localization Service, Diagnostics UI", - "verification": "Localization Test", - "statement": "The architecture shall centralize locale-aware formatting." - }, - { - "id": "SAD-027", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Localization Service", - "verification": "Inspection, Test", - "statement": "The architecture shall keep diagnostic machine fields language-neutral." - }, - { - "id": "SAD-028", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "UX, Software Engineering", - "verification": "Review", - "statement": "The architecture shall maintain downstream UI/UX and design-system documentation." - }, - { - "id": "SAD-029", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Software Architecture", - "verification": "Inspection", - "statement": "The architecture shall enforce SRS-only source traceability for SAD." - }, - { - "id": "SAD-030", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Software Design", - "verification": "Inspection", - "statement": "The architecture shall enforce SAD-only source traceability for SDD." - }, - { - "id": "SAD-031", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell", - "verification": "Integration Test", - "statement": "The architecture shall initialize theme, localization, and platform services before first feature render." - }, - { - "id": "SAD-032", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, Protocol Adapter", - "verification": "Integration Test", - "statement": "The architecture shall isolate TeamSpeak-compatible protocol integration behind Rust protocol components." - }, - { - "id": "SAD-033", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync", - "verification": "System Test", - "statement": "The architecture shall isolate authoritative server state in Rust Core and state synchronization components." - }, - { - "id": "SAD-034", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Audio", - "verification": "Audio Test", - "statement": "The architecture shall isolate real-time audio processing in the audio subsystem." - }, - { - "id": "SAD-052", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Software Architecture", - "verification": "Review, Inspection", - "statement": "The architecture shall maintain an interface catalog for architecturally significant software interfaces." - }, - { - "id": "SAD-056", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Software Architecture, Bridge, Rust Core, Platform Services", - "verification": "Review, Integration Test", - "statement": "The architecture shall define software interface error-handling responsibilities at the architectural level." - }, - { - "id": "SAD-061", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "iOS Build Configuration, Platform Services", - "verification": "Platform Test, Inspection", - "statement": "The software architecture shall isolate iOS runtime minimum version configuration from feature logic." - }, - { - "id": "SAD-062", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Release Pipeline, iOS Build Configuration", - "verification": "Release Inspection", - "statement": "The software architecture shall treat Apple App Store SDK compliance as a release pipeline and build configuration responsibility." - }, - { - "id": "SAD-063", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Android Build Configuration, Platform Services, Release Pipeline", - "verification": "Platform Test, Release Inspection", - "statement": "The software architecture shall isolate Android runtime minimum version and Play target API compliance in Android build configuration and platform services. The Android minimum runtime baseline is API 28 (Android 9.0) per DEC-004 (Accepted " - }, - { - "id": "SAD-064", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, State Sync, Audio Subsystem, Protocol Adapter", - "verification": "System Test, Review", - "statement": "The software architecture shall model one active connection as the MVP runtime invariant across UI, Rust Core, state synchronization, audio routing, and protocol adapter components." - }, - { - "id": "SAD-065", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Audio, Settings UI", - "verification": "Audio Test", - "statement": "The software architecture shall assign audio processing defaults and capability detection to the audio subsystem and settings presentation layer." - }, - { - "id": "SAD-067", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Storage, Platform Secure Storage", - "verification": "Storage Test, Security Audit", - "statement": "The software architecture shall isolate non-secret local persistence in a database repository layer and secret persistence in platform secure storage services." - }, - { - "id": "SAD-068", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Bridge Facade, Flutter State, Rust Core", - "verification": "Integration Test, Architecture Review", - "statement": "The software architecture shall use a stable typed bridge facade with schema-controlled DTO boundaries between Flutter and Rust." - }, - { - "id": "SAD-069", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Privacy, Release Pipeline", - "verification": "Privacy Review, Security Audit", - "statement": "The software architecture shall exclude automatic telemetry, automatic diagnostic upload, and automatic crash reporting from MVP components and release configuration." - }, - { - "id": "SAD-070", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Software Architecture, Documentation Control", - "verification": "Review, Inspection", - "statement": "The software architecture shall record the product decisions affecting platform baselines, connection scope, audio defaults, audio implementation path, storage, bridge, diagnostics, and crash reporting in the ADR and decision baseline." - }, - { - "id": "SAD-071", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio, Platform Input", - "verification": "Architecture Review, Unit Test", - "statement": "The software architecture shall isolate desktop PTT into a `DesktopPttBackend` trait owned by the audio subsystem, with the active implementation selected at runtime by a `platform_input::select` factory. Only the factory and the audio engi" - }, - { - "id": "SAD-072", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio (Windows)", - "verification": "Platform Test (Windows), Architecture Review", - "statement": "The Windows PTT backend shall be a three-level ladder (`WindowsRawInputBackend` -> `WindowsHookBackend` -> `FocusedPttBackend`). The ladder is evaluated once at audio-engine start; the chosen rung is fixed for the lifetime of that engine an" - }, - { - "id": "SAD-073", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio (macOS)", - "verification": "Platform Test (macOS), Architecture Review", - "statement": "The macOS PTT backend shall be a two-level ladder (`MacOSEventTapBackend` -> `FocusedPttBackend`) with explicit permission state surfaced through `PttCapabilityLevel`. The permission query shall not block the audio-engine start path; an und" - }, - { - "id": "SAD-074", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio (Linux)", - "verification": "Platform Test (Linux, GNOME Wayland), Architecture Review", - "statement": "The Linux PTT backend shall be a two-level ladder (`LinuxGnomeWaylandBackend` using the freedesktop `org.freedesktop.portal.GlobalShortcuts` interface, -> `FocusedPttBackend`). Untested display servers, untested compositors, and missing D-B" - }, - { - "id": "SAD-075", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Audio Test", - "statement": "The audio engine shall expose `AudioTransmitGate`, a thin object that owns the `transmit_active` AtomicBool and exposes `set` and `load` operations only. The Opus encoder feed shall consult `AudioTransmitGate::load()` once per outbound fram" - }, - { - "id": "SAD-076", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Bridge, Audio, Diagnostics", - "verification": "Architecture Review, Privacy Review", - "statement": "The bridge shall expose PTT events through the existing `BridgeEvent` stream, adding a `BridgeEvent::PttCapability { level, backend_id }` variant. The bridge shall never carry raw key codes or scan codes across the FFI boundary; the trait s" - }, - { - "id": "SAD-077", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics", - "verification": "Unit Test, Privacy Review", - "statement": "The diagnostics subsystem shall add a `PttSanitizer` decorator over the `RedactingLogLayer` log sink. The decorator shall scan each record's field set for the well-known names `key_code`, `scan_code`, `virtual_key`, `vk`, `keysym`, `keysym_" - }, - { - "id": "SAD-078", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "UI Review, User Acceptance Test", - "statement": "The Flutter UI shall consume `BridgeEvent::PttCapability` and render a small capability badge in the Voice Bar. When the capability resolves to `L0` on a platform that the user reasonably expected to support Global PTT (for example Windows " - }, - { - "id": "SAD-079", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Integration Test", - "statement": "The audio engine shall own the missed-key-up watchdog as a single tokio task that subscribes to `transmit_active` transitions through a `tokio::sync::watch` channel; on a configured timeout (default 30 s, owner-tunable) without a release ev" - }, - { - "id": "SAD-080", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio (Windows / macOS / Linux), Bridge, Flutter UI", - "verification": "Unit Test, Platform Test, UI Review", - "statement": "The software architecture shall expose a mouse-side-button input class as a first-class member of the cross-platform PTT binding type so the Windows and macOS backends can drive `transmit_active` from `Mouse4` / `Mouse5` events while the Li" - }, - { - "id": "SAD-081", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio, Bridge, Flutter UI", - "verification": "Integration Test, UI Review", - "statement": "The software architecture shall remove the `start_audio` and `stop_audio` bridge calls from the public bridge surface and shall replace them with `voice_join(channel_id) -> Result<(), VoiceError>` and `voice_leave() -> Result<(), VoiceError" - }, - { - "id": "SAD-082", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Integration Test", - "statement": "The software architecture shall add a `ReleaseTailTimer` element to the audio subsystem, owned by the `PttController` and acting as an adapter between the `DesktopPttBackend` `transmit` signal and the `AudioTransmitGate::set()` call. On key" - }, - { - "id": "SAD-083", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Integration Test", - "statement": "The software architecture shall add a `TransmitModeSelector` element owned by the audio engine that consults the current `TransmitMode` from the identity store on every state evaluation. In `Ptt` mode the selector defers to the `PttControll" - }, - { - "id": "SAD-084", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Audio Adapter (Android)", - "verification": "Platform Test (Android), Audio Test, Architecture Review", - "statement": "The software architecture shall allocate Android in-call audio-mode management to an `AndroidAudioModeController` element inside the Android platform audio adapter, owned by the audio subsystem and bound to the voice-session lifecycle rathe" - }, - { - "id": "SAD-085", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Platform Services (Android Permission Adapter), Audio Subsystem, Flutter UI, Bridge", - "verification": "Platform Test (Android), Integration Test, Privacy Review", - "statement": "The software architecture shall extend the Android platform permission adapter (within Platform Services) to own the runtime `RECORD_AUDIO` permission flow on behalf of the audio subsystem, and shall recognise listen-only operation (`captur" - }, - { - "id": "SAD-086", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Platform Services (Android), Audio Subsystem", - "verification": "Platform Test (Android), Integration Test", - "statement": "The software architecture shall allocate Android foreground-service behaviour for active voice sessions to an `AndroidVoiceForegroundService` element owned by the Android platform services container, bound to the voice-session lifecycle (st" - }, - { - "id": "SAD-087", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "macOS Build Configuration, Platform Services (macOS), Release Pipeline", - "verification": "Platform Test (macOS), Release Inspection, Architecture Review", - "statement": "The software architecture shall isolate macOS runtime baseline configuration, universal-binary packaging, framework layout, and CocoaPods integration in macOS build configuration and platform services, parallel to SAD-061 (iOS runtime) and " - }, - { - "id": "SAD-088", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem (`chanora_audio` crate, `benches/` directory; extends SAD-034)", - "verification": "Inspection (bench-function presence and metric coverage), Bench Run (criterion executes on the host runner), Architecture Review", - "statement": "The software architecture shall add a `criterion`-based realtime-audio benchmark harness as a first-class architectural element of the `chanora_audio` crate, extending the existing audio-subsystem allocation in SAD-034. The harness lives in" - }, - { - "id": "SAD-089", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem (`chanora_audio` crate, `benches/baselines/` subdirectory; output sink of SAD-088)", - "verification": "Inspection (file path, schema), Bench Run (file is regenerable end-to-end)", - "statement": "The software architecture shall pin the baseline-JSON on-disk path for the SAD-088 harness to `crates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.json`, a single deterministic file per host architecture under the crate's `bench" - }, - { - "id": "SAD-090", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Build, Release, Operations (`.github/workflows/bench-advisory.yml`; extends SAD-037 — CI surface, SE-18 realization)", - "verification": "Inspection (workflow trigger / runner / non-blocking exit), CI Dry Run (synthetic PR exercises markdown rendering end-to-end), Architecture Review", - "statement": "The software architecture shall add an advisory CI workflow file `.github/workflows/bench-advisory.yml` to the build/release/operations allocation (extending SAD-037), realizing the SRS-218 advisory-reporting workflow. Triggers: `pull_reque" - }, - { - "id": "SAD-091", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Build, Release, Operations (`.github/workflows/bench-baseline-update.yml`; extends SAD-037 — CI surface, SE-18 realization; sole writer of SAD-089)", - "verification": "Inspection (trigger is `workflow_dispatch` only; commit destination is SAD-089's path), CI Dry Run (manual invocation regenerates baseline end-to-end), Architecture Review", - "statement": "The software architecture shall add a separate baseline-update CI workflow file `.github/workflows/bench-baseline-update.yml` to the build/release/operations allocation (extending SAD-037), realizing the SRS-218 clause 5 baseline-update-iso" - }, - { - "id": "SAD-092", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync", - "verification": "Architecture Review, Integration Test, System Test", - "statement": "The software architecture shall assign authoritative current-channel membership ownership to Rust Core and the State Synchronization Engine. The authoritative current channel is the last channel membership state confirmed by Rust Core from " - }, - { - "id": "SAD-093", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Flutter State, Bridge Facade, Rust Core", - "verification": "UI Test, Integration Test, System Test", - "statement": "The software architecture shall model a channel-join request as user intent crossing the bridge, not as an immediate local membership mutation. Flutter may expose the requested target channel as a non-authoritative pending target for visual" - }, - { - "id": "SAD-094", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync, Protocol Adapter, Bridge Facade, Diagnostics, Flutter State", - "verification": "Integration Test, System Test, Diagnostics Review", - "statement": "The software architecture shall preserve the previous authoritative current channel on channel-join failure or timeout and shall clear the non-authoritative pending target through Rust Core / State Sync events. Join denial, network/protocol" - }, - { - "id": "SAD-095", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "State Sync, Rust Core, Protocol Adapter, Bridge Facade, Flutter State, Verification Support", - "verification": "Architecture Review, SDD Inspection, Unit Test, Integration Test, System Test", - "statement": "The downstream SDD shall define deterministic State Sync handling for channel-join edge cases while preserving the SAD-092 authoritative ownership boundary. Required SDD coverage includes duplicate join requests, a different target requeste" - }, - { - "id": "SAD-096", - "source": "architecture/sad.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync, Protocol Adapter, Bridge Facade, Diagnostics, Verification Support", - "verification": "Architecture Review, Integration Test, System Test, Diagnostics Review", - "statement": "The software architecture shall expose an explicit Protocol Adapter command seam for leaving the current voice channel / moving the current client out of channel membership; downstream design may name the seam `leave_channel`, `move_out_of_" - }, - { - "id": "SDD-001", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Integration Test", - "statement": "`ChanoraApp` shall initialize theme, localization, platform services, and routing before rendering feature content." - }, - { - "id": "SDD-002", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraThemeFactory` shall create Material 3 ThemeData for light and dark modes." - }, - { - "id": "SDD-003", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraThemeFactory` shall support seed-color replacement without feature-screen changes." - }, - { - "id": "SDD-004", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraSemanticColors` shall define connection state colors independently from base ColorScheme." - }, - { - "id": "SDD-005", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraSemanticColors` shall define voice state colors independently from base ColorScheme." - }, - { - "id": "SDD-006", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraSemanticColors` shall define latency, packet-loss, diagnostics, and error semantic roles." - }, - { - "id": "SDD-007", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraSpacing` shall define reusable spacing values for compact, medium, and expanded layouts." - }, - { - "id": "SDD-008", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraShape` shall define reusable corner and shape values for cards, buttons, panels, badges, and sheets." - }, - { - "id": "SDD-009", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraMotion` shall define standard durations and reduced-motion behavior." - }, - { - "id": "SDD-010", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Static Inspection", - "statement": "Feature widgets shall read tokens through Flutter theme accessors only." - }, - { - "id": "SDD-011", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ChanoraAdaptiveShell` shall classify window width into compact, medium, or expanded." - }, - { - "id": "SDD-012", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ChanoraAdaptiveShell` shall dispatch to `CompactShell`, `MediumShell`, or `ExpandedShell`." - }, - { - "id": "SDD-013", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`CompactShell` shall render single-column content and mobile navigation." - }, - { - "id": "SDD-014", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`MediumShell` shall render side navigation or navigation rail behavior." - }, - { - "id": "SDD-015", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ExpandedShell` shall render persistent pane regions and persistent voice controls." - }, - { - "id": "SDD-016", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`ConnectionStatusPresenter` shall provide non-color-only connection state presentation data." - }, - { - "id": "SDD-017", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ChanoraVoiceBar` shall expose mute, deaf, push-to-talk, input meter, current channel, and latency properties." - }, - { - "id": "SDD-018", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ChanoraVoiceBar` shall remain renderable as a persistent shell slot." - }, - { - "id": "SDD-019", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ChanoraChannelTree` shall support hierarchical channel nodes and client rows." - }, - { - "id": "SDD-020", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ChanoraChannelTree` shall support selected state, expansion state, join intent, and context-menu intent." - }, - { - "id": "SDD-021", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test, Accessibility Test", - "statement": "`ChanoraClientTile` shall display client status with text, icon, and semantic state cues." - }, - { - "id": "SDD-022", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test, Accessibility Test", - "statement": "`ChanoraStatusBadge` shall expose visible text or icon and accessibility semantics." - }, - { - "id": "SDD-023", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "`ChanoraAudioMeter` shall display audio input level and support reduced-motion fallback." - }, - { - "id": "SDD-024", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Accessibility Test", - "statement": "All icon-only buttons shall provide localized semantic labels." - }, - { - "id": "SDD-025", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "Focusable components shall define focus order and visible focus style." - }, - { - "id": "SDD-026", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Accessibility Test", - "statement": "Adaptive shell panes shall support increased text scale without losing critical actions." - }, - { - "id": "SDD-027", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test", - "statement": "`PlatformInsetsService` shall provide safe area, keyboard inset, display cutout, and desktop inset data." - }, - { - "id": "SDD-028", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test, Widget Test", - "statement": "`BackIntentService` shall normalize Android/system back behavior into route-level intents." - }, - { - "id": "SDD-029", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test", - "statement": "`HapticsService` shall expose platform-aware haptic feedback operations." - }, - { - "id": "SDD-030", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test", - "statement": "`KeyboardAvoidancePolicy` shall prevent critical controls from being covered by virtual keyboards." - }, - { - "id": "SDD-031", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`LocalizationService` shall load product string resources for the active locale." - }, - { - "id": "SDD-032", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`LocalizationService` shall fall back to the baseline English locale when a key is missing." - }, - { - "id": "SDD-033", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "`LocalizationService` shall allow new locale resources without changes to protocol, audio, state, or storage modules." - }, - { - "id": "SDD-034", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Integration Test", - "statement": "`ContentTextAdapter` shall pass through server-provided names and messages without translation." - }, - { - "id": "SDD-035", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Integration Test", - "statement": "`UnicodeTextBoundary` shall validate cross-layer text DTOs as UTF-8 compatible strings." - }, - { - "id": "SDD-036", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "Protocol-boundary conversion shall be implemented only in protocol adapter code." - }, - { - "id": "SDD-037", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "Platform-boundary conversion shall be implemented only in platform adapter code." - }, - { - "id": "SDD-038", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Test, Audit", - "statement": "Diagnostics export shall preserve Unicode after redaction." - }, - { - "id": "SDD-039", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Localization Test", - "statement": "Bidi text rendering shall use Flutter text widgets and directionality behavior compatible with platform support." - }, - { - "id": "SDD-040", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "Locale-aware formatting shall be centralized in localization utilities." - }, - { - "id": "SDD-041", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "Diagnostics event keys shall remain stable and language-neutral." - }, - { - "id": "SDD-042", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Review", - "statement": "UI/UX supporting documents shall be versioned with the software architecture baseline." - }, - { - "id": "SDD-043", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "SAD source validation shall reject direct lower-layer source fields." - }, - { - "id": "SDD-044", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "SDD source validation shall reject direct non-SAD source fields." - }, - { - "id": "SDD-045", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Integration Test", - "statement": "Theme/localization/platform initialization shall be tested before first feature render." - }, - { - "id": "SDD-046", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Integration Test", - "statement": "Bridge facade DTOs shall expose only stable UI-safe data structures." - }, - { - "id": "SDD-047", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "Core event mapping shall convert Rust Core events into feature view models." - }, - { - "id": "SDD-048", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "Audio state view models shall separate UI state from real-time audio processing." - }, - { - "id": "SDD-049", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "Storage access shall be isolated from feature widgets behind repository or service APIs." - }, - { - "id": "SDD-050", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Widget Test", - "statement": "Diagnostics UI shall obtain localized display text from diagnostics presentation services." - }, - { - "id": "SDD-066", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection", - "statement": "`SoftwareUnitTraceChecker` shall verify that SDD items link directly to SAD IDs only." - }, - { - "id": "SDD-071", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Inspection, Platform Test", - "statement": "`IOSBuildConfig` shall define the iOS minimum runtime deployment target as iOS 13 unless an approved platform change raises it." - }, - { - "id": "SDD-072", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Release Inspection", - "statement": "`IOSReleaseGate` shall verify that App Store Connect upload builds use Xcode 26 or later and the iOS 26 / iPadOS 26 SDK or later on or after 2026-04-28 unless Apple publishes a newer applicable upload requirement." - }, - { - "id": "SDD-073", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test, Release Inspection, Build Test", - "statement": "`AndroidBuildConfig` shall define Android API 28 (Android 9.0 Pie) as the minimum Android runtime baseline per DEC-004 and shall expose target SDK configuration for release inspection. This supersedes the prior API 24 recommendation and is " - }, - { - "id": "SDD-074", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, System Test", - "statement": "`ConnectionSessionManager` shall enforce a single active server connection in MVP." - }, - { - "id": "SDD-075", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Audio Test", - "statement": "`AudioProcessingSettings` shall default AEC, AGC, Noise Suppression, and High-Pass Filter to enabled where supported and stable." - }, - { - "id": "SDD-077", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Storage Test", - "statement": "`LocalDatabaseRepository` shall persist non-secret local state through SQLite or an equivalent embedded database abstraction." - }, - { - "id": "SDD-078", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Security Audit", - "statement": "`SecretStorageRepository` shall persist secrets only through platform secure storage service interfaces." - }, - { - "id": "SDD-079", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Integration Test, Inspection", - "statement": "`TypedBridgeFacade` shall define generated or schema-controlled DTOs for Flutter/Rust commands, results, and events." - }, - { - "id": "SDD-080", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Privacy Review, Security Audit", - "statement": "`DiagnosticsPrivacyGate` shall disable automatic diagnostic upload, telemetry upload, and crash reporting in MVP release configuration unless an approved decision enables them." - }, - { - "id": "SDD-081", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Inspection", - "statement": "`DesktopPttBackend` shall be a Rust trait in `chanora_audio::ptt` exposing `fn capability(&self) -> PttCapabilityLevel`, `fn backend_id(&self) -> &'static str`, `fn start(&mut self, gate: Arc, binding: PttBinding) -> Resu" - }, - { - "id": "SDD-082", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`PttCapabilityLevel` shall be a plain Rust enum (`L0Focused`, `L1GlobalShortcut`, `L2GlobalHoldToTalk`, `L3GlobalWithMouseButtons`, `L4DeviceAware`) with a public `as_str(&self) -> &'static str` mapping for diagnostics. `L4DeviceAware` is r" - }, - { - "id": "SDD-083", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Windows), Unit Test", - "statement": "`WindowsRawInputBackend` shall use the `RegisterRawInputDevices` Win32 API targeting `HID_USAGE_PAGE_GENERIC` keyboard and mouse usages, run a dedicated message-only window on its own OS thread, and forward only sanitised binding-match resu" - }, - { - "id": "SDD-084", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Windows), Unit Test", - "statement": "`WindowsHookBackend` shall be used only when Raw Input registration fails. It installs `WH_KEYBOARD_LL` and `WH_MOUSE_LL` hooks on its dedicated thread; the hook callbacks compare against the active binding and call `AudioTransmitGate::set`" - }, - { - "id": "SDD-085", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (macOS), User Acceptance Test", - "statement": "`MacOSEventTapBackend` shall create a `CGEventTap` filtered to keyboard and mouse-button events, attached to the main run loop with an explicit retain. The tap callback shall consult the active binding and call `AudioTransmitGate::set` only" - }, - { - "id": "SDD-086", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Linux, GNOME Wayland), Unit Test", - "statement": "`LinuxGnomeWaylandBackend` shall call `CreateSession`, `BindShortcuts`, and listen for `Activated` / `Deactivated` signals on the freedesktop `org.freedesktop.portal.GlobalShortcuts` D-Bus interface. Binding capture shall use the portal's o" - }, - { - "id": "SDD-087", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, UI Review", - "statement": "`FocusedPttBackend` shall integrate with Flutter's keyboard / pointer event stream via the existing `Listener` widget path and the bridge `set_ptt` command. Reports `PttCapabilityLevel::L0Focused` and `backend_id = \"focused\"`." - }, - { - "id": "SDD-088", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Integration Test", - "statement": "`PttController` shall sit in `chanora_core::ptt` and own a `Box`, a clone of `AudioTransmitGate`, a `watch::Sender` for live publication to the bridge, and a `tokio::sync::Mutex` over the active `P" - }, - { - "id": "SDD-089", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test", - "statement": "`AudioTransmitGate` shall be `Arc` wrapping a `tokio::sync::watch::Sender` and an internal `AtomicBool`. `set(v)` updates both atomically; `load()` reads the atomic; `subscribe()` hands out a `watch::Receiver` for t" - }, - { - "id": "SDD-090", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Privacy Review", - "statement": "`PttSanitizer` shall implement `tracing_subscriber::Layer` and decorate the existing `RedactingLogLayer`. Records whose field names match any of the following banned field names shall be dropped before `on_event` reaches the wrapped sink: `" - }, - { - "id": "SDD-091", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "UI Review, User Acceptance Test", - "statement": "`PttCapabilityBadge` shall be a Flutter widget rendered inside the Voice Bar, subscribed to the FRB stream of `BridgeEvent::PttCapability`. The widget shall map each enum value to a short user-facing label and shall render an info icon that" - }, - { - "id": "SDD-092", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Integration Test", - "statement": "`MissedKeyUpWatchdog` shall be a tokio task spawned by the audio engine that holds the `watch::Receiver` from `AudioTransmitGate::subscribe()`. On each transition from `false` to `true` the task records the timestamp; on each `true ->" - }, - { - "id": "SDD-093", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Platform Test (Windows / macOS / Linux), UI Review", - "statement": "`PttInputClass` shall be a Rust enum in `chanora_audio::ptt_backends` with variants `None`, `Keyboard`, and `MouseSideButton`. The enum is the only carrier of the user's bound input class across the audio + bridge + Flutter boundary; its `a" - }, - { - "id": "SDD-094", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Integration Test, UI Review", - "statement": "`chanora_bridge::voice::voice_join(channel_id: ChannelId) -> Result<(), VoiceError>` and `chanora_bridge::voice::voice_leave() -> Result<(), VoiceError>` shall be the only public-bridge voice lifecycle calls. Internally `chanora_audio::engi" - }, - { - "id": "SDD-095", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, UI Review", - "statement": "`chanora_audio::TransmitMode` shall be a `#[repr(u8)]` Rust enum with variants `Ptt = 0`, `Continuous = 1`, `VoiceActivity = 2`. Persistence shall be provided by `chanora_storage::IdentityStore::set_transmit_mode(&self, mode: TransmitMode)`" - }, - { - "id": "SDD-096", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test, Integration Test", - "statement": "`chanora_audio::release_tail::ReleaseTailTimer` shall be a tokio-task-owning struct exposing `arm(&self, gate: AudioTransmitGate, tail_ms: u32)`, `key_down(&self)`, `key_up(&self)`, and `cancel(&self)`. Internally the struct shall hold a `t" - }, - { - "id": "SDD-097", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "UI Review, Widget Test", - "statement": "`apps/chanora_flutter/lib/widgets/voice_bar.dart` shall replace the existing `_AudioControls` widget in `apps/chanora_flutter/lib/main.dart` as the rendering surface for voice state. Its input shall be the `BridgeEvent::VoiceState` stream; " - }, - { - "id": "SDD-098", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test, Release Inspection", - "statement": "`IOSAudioSessionConfig` shall be a Swift unit that configures `AVAudioSession` with category `.playAndRecord`, mode `.default`, and options `.defaultToSpeaker | .allowBluetoothHFP | .allowBluetoothA2DP`. `setCategory` shall be called from `" - }, - { - "id": "SDD-099", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test, Demo", - "statement": "`IOSPermissionRequester` shall call `AVAudioSession.sharedInstance().requestRecordPermission` approximately 1 second after app launch. The permission result shall be logged. A denied permission shall surface as an audio engine failure at `v" - }, - { - "id": "SDD-100", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test TC-9.3", - "statement": "Swift shall observe `AVAudioSession.routeChangeNotification`. On route change where `reason == .oldDeviceUnavailable` or `reason == .newDeviceAvailable`, Swift shall call the Rust bridge method `handleRouteChange()`. The Rust side shall inv" - }, - { - "id": "SDD-101", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test TC-9.1, TC-9.2", - "statement": "Swift shall observe `AVAudioSession.interruptionNotification`. On `.began` the audio shall pause via `VPIO.stop()`. On `.ended` with `shouldResume == true` the audio shall resume via `VPIO.start()`. A phone call shall yield the audio sessio" - }, - { - "id": "SDD-102", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test TC-8", - "statement": "`Info.plist` shall contain `UIBackgroundModes = [audio]`. The VPIO unit shall continue running when the app backgrounds. iOS shall show the red microphone indicator in the status bar while background audio is active. `AVAudioSession` shall " - }, - { - "id": "SDD-103", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test TC-3, TC-12", - "statement": "iOS P0 shall use `FocusedPttBackend` only. There shall be no `DesktopPttBackend` ladder on iOS. `PttCapabilityLevel` shall always be `L0Focused`. The UI shall render an on-screen PTT button. A capability badge shall explain the iOS limitati" - }, - { - "id": "SDD-104", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test, Audio Test", - "statement": "`VoiceProcessingIO` AudioUnit shall provide hardware AEC, AGC, and noise suppression. These shall always be engaged and shall not be user-disableable. The Flutter audio processing settings UI shall display these as hardware-enabled with swi" - }, - { - "id": "SDD-105", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Android), Inspection", - "statement": "`AndroidJniBootstrap` shall own the JNI initialisation surface for `chanora_bridge` on Android, covering native library load, `JNI_OnLoad` `JavaVM*` capture, and `initChanoraContext(Context)` invocation. This item back-fills the existing im" - }, - { - "id": "SDD-106", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Android), Integration Test, UI Review", - "statement": "`AndroidPermissionRequester` shall own the Android runtime acquisition of `RECORD_AUDIO` on behalf of the audio subsystem and shall expose the current permission state to the audio engine and to Flutter through the existing `BridgeEvent` st" - }, - { - "id": "SDD-107", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Android), Integration Test, Release Inspection", - "statement": "`AndroidVoiceForegroundService` shall own the Android foreground service lifecycle for active voice sessions. It shall be started by the audio engine on first `voice_join` and stopped on the last `voice_leave` or on engine `shutdown_if_idle" - }, - { - "id": "SDD-108", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Android), Unit Test, Audio Test", - "statement": "`AndroidAudioModeController` shall own engagement and release of Android's in-call audio mode for the voice subsystem. It shall snapshot the prior `AudioManager.getMode()` on first engage and restore it on last release, shall be reference-c" - }, - { - "id": "SDD-110", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Android), UI Review", - "statement": "`AndroidPttCapability` shall pin Android P0 to `FocusedPttBackend` only. There shall be no `DesktopPttBackend` ladder on Android, no system-wide global key hook, and no global accessibility-service-based PTT. `PttCapabilityLevel` shall alwa" - }, - { - "id": "SDD-111", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test (Rust), Platform Test (Android), Build Verification (cross-platform `cargo build` for android / ios / desktop targets)", - "statement": "`AndroidVoiceAudioBackend` shall be the Android-side implementation of a new platform-abstracted voice-audio capture+playback element. The cross-platform abstraction shall be expressed as a Rust trait — provisionally `MobileVoiceAudioBacken" - }, - { - "id": "SDD-112", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test (Rust, config-validation paths), Platform Test (Android, achieved-value recording)", - "statement": "`AndroidVoiceStreamConfig` shall be the immutable, validated configuration passed by the audio engine into `android_voice_unit::AndroidVoiceUnit::open()` (per SDD-111). It shall encode the AAudio / Oboe stream-construction parameters that S" - }, - { - "id": "SDD-113", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Platform Test (Android, device-matrix from SDD-116), Unit Test (Rust, fallback-path branch coverage)", - "statement": "`AndroidHardwareAudioEffects` shall be the element responsible for engaging the Android platform hardware voice-audio effects — `AcousticEchoCanceler`, `NoiseSuppressor`, `AutomaticGainControl` — on the active microphone capture session, an" - }, - { - "id": "SDD-114", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Manifest Inspection (CI), Platform Test (Android)", - "statement": "`AndroidVoiceManifestRequirements` shall enumerate, at the SDD layer, the Android manifest declarations required for the voice audio backend (SDD-111..SDD-113) and the foreground-service hosting (SDD-107). This unit is a back-fill of declar" - }, - { - "id": "SDD-115", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Integration Test (Rust + Android), Platform Test (Android, device-matrix from SDD-116)", - "statement": "`AndroidVoiceLifecycleSequencing` shall fix the sequencing of voice-session lifecycle operations across the elements introduced in SDD-107 (foreground service), SDD-108 (audio mode controller), SDD-111 (voice audio backend), and SDD-113 (ha" - }, - { - "id": "SDD-116", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Aggregation — this SDD is itself a contract for verification engineers; the per-case verification is owned by SWE4-UV / SWE5-IV / SWE6-SV ranges cross-linked below.", - "statement": "`AndroidVoiceVerificationMatrix` shall specify the minimum Android device matrix that the SDD-111..SDD-115 implementation must be exercised against before the implementation can be declared verified for the purposes of closing risk RISK-AUD" - }, - { - "id": "SDD-118", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Build-system Test (Gradle task wiring, up-to-date semantics), Platform Test (Android — verify per-ABI `.so` present in produced AAB / APK and loadable per SDD-105), Unit Test (preflight failure-mode messages)", - "statement": "`AndroidBridgeBuildAutomation` shall specify how the `chanora_bridge` Rust cdylib is produced and staged for the Android build automatically, without manual `cargo-ndk` invocations and without environment mutation. This unit complements SDD" - }, - { - "id": "SDD-119", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Build-system Test (CocoaPods integration on a clean derived-data state), Platform Test (iOS device build, macOS desktop build, both with `chanora_bridge` symbols resolving at runtime).", - "statement": "`iOSAndMacOSBridgeBuildAutomation` shall record, as a documentation-after-code back-fill, the existing CocoaPods-podspec-driven automation that builds the `chanora_bridge` Rust cdylib for iOS and macOS and embeds the result as an `@rpath`-b" - }, - { - "id": "SDD-121", - "source": "architecture/sdd.md", - "stage": "P0 / MVP", - "allocation": "", - "verification": "Unit Test (reducer transition table), Integration Test (bridge + protocol adapter seams), Widget Test (pending visual state), Fault-Injection Test (timeout/reconnect/stale outcome)", - "statement": "`chanora_state::channel_join` shall own the single deterministic reducer for user-initiated channel-join pending state, authoritative current-channel membership, and reconnect/snapshot reconciliation. Flutter and `chanora_bridge` shall send" - }, - { - "id": "SRS-001", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review", - "statement": "The software requirements specification shall define software requirements derived from the approved Chanora SysDes baseline." - }, - { - "id": "SRS-002", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Inspection", - "statement": "Each software requirement shall have a unique `SRS-XXX` identifier, type, stage, software allocation, source SysDes link, verification method, acceptance criteria, and feasibility analysis." - }, - { - "id": "SRS-003", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review", - "statement": "The software requirements shall be structured by software component and cross-cutting concern to support allocation to the software team." - }, - { - "id": "SRS-004", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Inspection", - "statement": "The software requirements shall be prioritized using P0 / MVP, P1 / Beta, P2 / Production, and P3 / Future stage labels." - }, - { - "id": "SRS-005", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Inspection", - "statement": "The software requirements shall maintain bidirectional traceability to SysDes design items only; upstream system-requirement coverage shall be inherited through the approved SysDes allocation baseline." - }, - { - "id": "SRS-006", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review", - "statement": "The software requirements shall analyze impact on the operating environment for OS services, audio hardware, network connectivity, external compatible servers, secure storage, deployment, and mobile lifecycle constraints." - }, - { - "id": "SRS-007", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review", - "statement": "The agreed SRS baseline shall be communicated to SAD, SDD, implementation, and verification authors." - }, - { - "id": "SRS-008", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Application Container", - "verification": "Review", - "statement": "The software shall be implemented as a cross-platform client application and shall not be implemented or described as an operating system." - }, - { - "id": "SRS-009", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Application Container, Rust Core", - "verification": "Review, System Test", - "statement": "The software shall operate without requiring Chanora-operated backend infrastructure for MVP voice, channel, and text operation." - }, - { - "id": "SRS-010", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Application Container, Protocol Adapter", - "verification": "Inspection", - "statement": "The software shall not implement TeamSpeak-compatible server hosting functionality in MVP." - }, - { - "id": "SRS-011", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "All Software Components", - "verification": "Inspection", - "statement": "The software shall use a layered structure that separates Flutter UI, Flutter State, Bridge, Rust Core, Protocol Adapter, State Sync, Audio, Storage, Diagnostics, and Platform Adapters." - }, - { - "id": "SRS-012", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Audio Subsystem", - "verification": "Inspection, Performance Test", - "statement": "The software shall keep real-time audio processing outside Flutter UI." - }, - { - "id": "SRS-013", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, Platform Adapters", - "verification": "Build Test", - "statement": "The software shall support Windows, macOS, Linux, iOS, and Android through shared Flutter UI, shared Rust Core, and platform-specific adapters." - }, - { - "id": "SRS-014", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters", - "verification": "Inspection, Integration Test", - "statement": "The software shall isolate OS-specific permissions, audio routing, lifecycle, secure storage, notification, and packaging behavior behind platform adapter interfaces." - }, - { - "id": "SRS-015", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, State Sync, Audio, Diagnostics, Platform Adapters", - "verification": "Inspection, Demo", - "statement": "The software shall provide verification hooks for protocol, state, audio, security, packaging, and platform behavior." - }, - { - "id": "SRS-016", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Application Container", - "verification": "Integration Test", - "statement": "The software shall provide an application container that initializes Flutter UI, Bridge, Rust Core, platform adapters, storage, and diagnostics in a deterministic startup sequence." - }, - { - "id": "SRS-017", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "UI Test", - "statement": "The software shall provide a home or entry screen for manual connection, recent servers, and saved bookmarks." - }, - { - "id": "SRS-018", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "UI Test", - "statement": "The software shall provide a manual connection UI that accepts host or IP address, port, nickname, and optional server password." - }, - { - "id": "SRS-019", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Flutter State", - "verification": "UI Test, System Test", - "statement": "The software shall display connection states including Disconnected, Connecting, Synchronizing, Connected, Reconnecting, and Failed or equivalent user-safe states." - }, - { - "id": "SRS-020", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Flutter State", - "verification": "UI Test, System Test", - "statement": "The software shall display a server channel tree derived from the Rust Core snapshot and delta events." - }, - { - "id": "SRS-021", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Flutter State", - "verification": "UI Test, System Test", - "statement": "The software shall display online clients in their channels and update the display after client join, leave, and move events." - }, - { - "id": "SRS-022", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Flutter State", - "verification": "UI Test", - "statement": "The software shall visually identify the current user and current channel when the data is available from Rust Core." - }, - { - "id": "SRS-023", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Bridge, Rust Core", - "verification": "System Test", - "statement": "The software shall provide a channel join interaction that sends a join command through the Bridge and waits for Rust Core or protocol confirmation before treating the join as authoritative." - }, - { - "id": "SRS-024", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Bridge, Rust Core", - "verification": "UI Test, System Test", - "statement": "The software shall provide channel text chat UI for message display and message input." - }, - { - "id": "SRS-025", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Audio Subsystem", - "verification": "UI Test, System Test", - "statement": "The software shall provide a persistent voice control surface for microphone mute, output deaf, push-to-talk state, input level, and current channel." - }, - { - "id": "SRS-026", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Audio Subsystem, Storage", - "verification": "UI Test", - "statement": "The software shall provide an audio settings UI for input device, output device, Echo Canceller, Automatic Gain Control, Noise Suppression, High-Pass Filter, and related defaults." - }, - { - "id": "SRS-027", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Platform Adapters", - "verification": "UI Test, Privacy Review", - "statement": "The software shall provide permission explanation UI before requesting microphone, notification, or mobile foreground-service related permissions where platform guidelines allow." - }, - { - "id": "SRS-029", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Flutter State", - "verification": "Inspection", - "statement": "The Flutter UI shall not directly invoke `tsclientlib`, access raw protocol types, or own authoritative server state." - }, - { - "id": "SRS-030", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter State", - "verification": "Inspection, UI Test", - "statement": "The Flutter UI shall subscribe to UI-safe core events and reduce them into presentation state only." - }, - { - "id": "SRS-031", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge Layer", - "verification": "Integration Test", - "statement": "The software shall provide a typed Flutter-Rust Bridge API for connection, channel, chat, voice, settings, storage, and diagnostics commands." - }, - { - "id": "SRS-032", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge Layer, Rust Core", - "verification": "Integration Test", - "statement": "The Bridge shall expose long-lived event streams from Rust Core to Flutter for connection, snapshot, delta, chat, voice, audio device, error, and diagnostic events." - }, - { - "id": "SRS-033", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge Layer", - "verification": "Inspection, Integration Test", - "statement": "The Bridge shall serialize and deserialize only bridge-safe DTOs and shall not expose Rust internal references or raw protocol library objects." - }, - { - "id": "SRS-034", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge Layer, Rust Core", - "verification": "Integration Test", - "statement": "The Bridge shall map Rust Core errors into user-safe Flutter error DTOs." - }, - { - "id": "SRS-035", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge Layer, Rust Core", - "verification": "Performance Test", - "statement": "The Bridge shall execute commands asynchronously and shall not block the Flutter UI thread during network, storage, or audio operations." - }, - { - "id": "SRS-036", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core", - "verification": "Unit Test, Integration Test", - "statement": "The Rust Core shall own authoritative connection lifecycle and server state for each active connection." - }, - { - "id": "SRS-037", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core", - "verification": "Inspection, Unit Test", - "statement": "The Rust Core shall manage connection actors or equivalent isolated execution contexts for connection-specific commands and events." - }, - { - "id": "SRS-038", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, Bridge", - "verification": "Integration Test", - "statement": "The Rust Core shall expose connect, disconnect, reconnect, join channel, send message, audio control, settings, bookmark, identity, and diagnostics commands to the Bridge." - }, - { - "id": "SRS-039", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core", - "verification": "Inspection", - "statement": "The Rust Core shall coordinate protocol, state synchronization, audio, storage, diagnostics, and platform adapter interfaces without leaking implementation details to Flutter." - }, - { - "id": "SRS-040", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync", - "verification": "Unit Test, System Test", - "statement": "The Rust Core shall emit connection status events for Connecting, Synchronizing, Connected, Reconnecting, Disconnected, and failure states." - }, - { - "id": "SRS-041", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core", - "verification": "System Test", - "statement": "The Rust Core shall suppress automatic reconnect after user-triggered disconnect." - }, - { - "id": "SRS-042", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, Protocol Adapter, State Sync", - "verification": "System Test", - "statement": "The Rust Core shall apply reconnect policy after recoverable network or protocol failures." - }, - { - "id": "SRS-043", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync", - "verification": "System Test, Event Replay Test", - "statement": "The Rust Core shall rebuild server state from a fresh snapshot after reconnect succeeds." - }, - { - "id": "SRS-044", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter", - "verification": "Inspection", - "statement": "The Protocol Adapter shall be the only software component that directly depends on `tsclientlib`." - }, - { - "id": "SRS-045", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Rust Core", - "verification": "Protocol Integration Test", - "statement": "The Protocol Adapter shall connect to TeamSpeak 3-compatible servers through `tsclientlib` using user-provided host, port, nickname, identity, and optional password." - }, - { - "id": "SRS-046", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, State Sync", - "verification": "Protocol Integration Test", - "statement": "The Protocol Adapter shall fetch server information, channel list, client list, and current-user information needed to build the initial snapshot." - }, - { - "id": "SRS-047", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, State Sync", - "verification": "Unit Test, Integration Test", - "statement": "The Protocol Adapter shall translate raw `tsclientlib` events into internal protocol events." - }, - { - "id": "SRS-048", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Rust Core", - "verification": "Unit Test, Integration Test", - "statement": "The Protocol Adapter shall translate protocol errors into normalized Chanora protocol errors." - }, - { - "id": "SRS-049", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter", - "verification": "Protocol Integration Test", - "statement": "The Protocol Adapter shall implement channel join commands and report success or failure to Rust Core." - }, - { - "id": "SRS-050", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter", - "verification": "Protocol Integration Test", - "statement": "The Protocol Adapter shall implement channel text message send and receive paths." - }, - { - "id": "SRS-051", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Audio Subsystem", - "verification": "Protocol Integration Test, Audio Test", - "statement": "The Protocol Adapter shall implement voice packet send and receive paths required by the audio subsystem." - }, - { - "id": "SRS-052", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Diagnostics", - "verification": "Demo", - "statement": "The Protocol Adapter shall expose protocol-probe diagnostics for connection, snapshot, event, text, and voice compatibility." - }, - { - "id": "SRS-054", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync", - "verification": "Unit Test", - "statement": "The State Synchronization Engine shall maintain a connection state model containing connection status, server info, current user, channels, clients, channel tree, permissions, chat state, and voice state where available." - }, - { - "id": "SRS-055", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync", - "verification": "Unit Test, Integration Test", - "statement": "The State Synchronization Engine shall produce a full snapshot event after initial synchronization." - }, - { - "id": "SRS-056", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync", - "verification": "Unit Test", - "statement": "The State Synchronization Engine shall apply live server events as deterministic deltas." - }, - { - "id": "SRS-057", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync, Rust Core", - "verification": "Unit Test, Integration Test", - "statement": "The State Synchronization Engine shall preserve per-connection event ordering." - }, - { - "id": "SRS-058", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync", - "verification": "Unit Test", - "statement": "The State Synchronization Engine shall use reducer functions or equivalent deterministic logic for snapshot, channel, client, chat, voice, and connection events." - }, - { - "id": "SRS-059", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync", - "verification": "Event Replay Test, System Test", - "statement": "The State Synchronization Engine shall replace stale server state with a fresh snapshot after reconnect." - }, - { - "id": "SRS-060", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync, Rust Core", - "verification": "Unit Test, Fuzz/Negative Test", - "statement": "The State Synchronization Engine shall tolerate malformed or unexpected protocol events without crashing the application." - }, - { - "id": "SRS-062", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Integration Test", - "statement": "The Audio Subsystem shall coordinate audio capture, preprocessing, audio processing, Opus-compatible encoding, voice packet handoff, receive decoding, jitter buffering, per-user controls, mixing, and playback." - }, - { - "id": "SRS-063", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Test", - "statement": "The Audio Subsystem shall process outgoing microphone PCM frames before encoding." - }, - { - "id": "SRS-064", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Processing Test", - "statement": "The Audio Subsystem shall apply a High-Pass Filter before voice encoding when enabled." - }, - { - "id": "SRS-065", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Processing Test", - "statement": "The Audio Subsystem shall apply Noise Suppression before voice encoding when enabled." - }, - { - "id": "SRS-066", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Adapters", - "verification": "Audio Processing Test", - "statement": "The Audio Subsystem shall apply Echo Canceller before voice encoding when enabled and when playback reference audio is available or the selected backend provides equivalent behavior." - }, - { - "id": "SRS-067", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Processing Test", - "statement": "The Audio Subsystem shall apply Automatic Gain Control before voice encoding when enabled." - }, - { - "id": "SRS-068", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Flutter UI, Storage", - "verification": "UI Test, Unit Test", - "statement": "The Audio Subsystem shall expose enable/disable settings for Echo Canceller, Automatic Gain Control, Noise Suppression, and High-Pass Filter." - }, - { - "id": "SRS-069", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Adapters", - "verification": "Review, Audio Test", - "statement": "The Audio Subsystem shall support conservative default audio processing configuration per platform." - }, - { - "id": "SRS-071", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Test", - "statement": "The Audio Subsystem shall encode outgoing voice using an Opus-compatible encoder." - }, - { - "id": "SRS-072", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Test", - "statement": "The Audio Subsystem shall decode incoming voice using an Opus-compatible decoder." - }, - { - "id": "SRS-073", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Test", - "statement": "The Audio Subsystem shall use jitter buffering for incoming voice packets." - }, - { - "id": "SRS-074", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Flutter UI", - "verification": "Audio Test, UI Test", - "statement": "The Audio Subsystem shall support per-user mute during playback." - }, - { - "id": "SRS-076", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem", - "verification": "Audio Test", - "statement": "The Audio Subsystem shall mix decoded incoming voice streams before playback." - }, - { - "id": "SRS-077", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Rust Core, Flutter UI", - "verification": "System Test", - "statement": "The Audio Subsystem shall implement microphone mute as a gate that prevents local voice transmission." - }, - { - "id": "SRS-078", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Rust Core, Flutter UI", - "verification": "System Test", - "statement": "The Audio Subsystem shall implement output deaf as a gate that prevents incoming voice playback." - }, - { - "id": "SRS-079", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Flutter UI", - "verification": "System Test", - "statement": "The Audio Subsystem shall implement push-to-talk gating before outgoing voice packet transmission." - }, - { - "id": "SRS-080", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Flutter UI", - "verification": "Audio Test, UI Test", - "statement": "The Audio Subsystem shall provide input level metering to Rust Core or Flutter through UI-safe events." - }, - { - "id": "SRS-081", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, State Sync, Flutter UI", - "verification": "Audio Test, UI Test", - "statement": "The Audio Subsystem shall provide speaking indicator events for local and remote users where available." - }, - { - "id": "SRS-084", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage", - "verification": "Unit Test, Integration Test", - "statement": "The software shall persist bookmarks in local non-secret storage." - }, - { - "id": "SRS-086", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Audio Subsystem", - "verification": "Unit Test, Integration Test", - "statement": "The software shall persist audio settings in local non-secret storage." - }, - { - "id": "SRS-089", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage", - "verification": "Inspection", - "statement": "The software shall use SQLite or an equivalent embedded data store for non-secret local data." - }, - { - "id": "SRS-090", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Platform Adapters", - "verification": "Inspection, Security Audit", - "statement": "The software shall access platform secure storage only through a secure storage provider abstraction." - }, - { - "id": "SRS-091", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Platform Secure Storage", - "verification": "Security Audit", - "statement": "The software shall store identity private keys using platform secure storage and shall not persist them in plaintext files." - }, - { - "id": "SRS-092", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Platform Secure Storage, Diagnostics", - "verification": "Security Audit", - "statement": "The software shall store server passwords using platform secure storage and shall not persist them in plaintext logs or non-secret database fields." - }, - { - "id": "SRS-093", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Storage", - "verification": "Security Audit", - "statement": "The software shall redact secrets before writing diagnostic logs or diagnostic exports." - }, - { - "id": "SRS-094", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, Protocol Adapter, Storage", - "verification": "Unit Test, Security Test", - "statement": "The software shall validate user input before passing values to protocol operations or storage operations." - }, - { - "id": "SRS-095", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics", - "verification": "Inspection, Demo", - "statement": "The software shall produce structured diagnostic logs for connection, protocol, state, audio, storage, platform, and release-relevant events." - }, - { - "id": "SRS-101", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "All Software Components", - "verification": "Privacy Review", - "statement": "The software shall minimize personal data collection in MVP." - }, - { - "id": "SRS-102", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics", - "verification": "Security Audit", - "statement": "The software shall not automatically upload diagnostics in MVP." - }, - { - "id": "SRS-103", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, Bridge, Diagnostics", - "verification": "UI Test, Security Review", - "statement": "The software shall display user-safe errors without raw internal stack traces." - }, - { - "id": "SRS-104", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "Platform Integration Test", - "statement": "The software shall provide a platform adapter interface for microphone capture." - }, - { - "id": "SRS-105", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "Platform Integration Test", - "statement": "The software shall provide a platform adapter interface for speaker/headphone playback." - }, - { - "id": "SRS-106", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Audio Subsystem, Flutter UI", - "verification": "Platform Integration Test", - "statement": "The software shall provide a platform adapter interface for audio device enumeration and selection where supported." - }, - { - "id": "SRS-107", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "Platform Integration Test", - "statement": "The software shall provide a platform adapter interface for audio route changes." - }, - { - "id": "SRS-108", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Flutter UI", - "verification": "Platform Integration Test", - "statement": "The software shall provide a platform adapter interface for microphone permission requests." - }, - { - "id": "SRS-110", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "iOS Integration Test", - "statement": "The iOS software shall integrate with AVAudioSession or equivalent platform audio session behavior for foreground voice sessions." - }, - { - "id": "SRS-111", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "Android Integration Test", - "statement": "The Android software shall integrate with foreground service behavior for active voice sessions." - }, - { - "id": "SRS-121", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Application Container, Flutter UI, Deployment Scripts", - "verification": "Legal Review, Inspection", - "statement": "The software release metadata and in-app wording shall not imply official TeamSpeak affiliation." - }, - { - "id": "SRS-123", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Diagnostics", - "verification": "Demo", - "statement": "The software shall include or support a protocol probe tool for validating `tsclientlib` adapter behavior against compatible test servers." - }, - { - "id": "SRS-124", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "State Sync, Diagnostics", - "verification": "Unit Test, Demo", - "statement": "The software shall include reducer tests and event replay tests for state synchronization." - }, - { - "id": "SRS-125", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Diagnostics", - "verification": "Audio Test, Demo", - "statement": "The software shall include audio loopback and audio processing tests for capture, playback, codec, jitter buffer, mixer, Echo Canceller, AGC, Noise Suppression, and High-Pass Filter." - }, - { - "id": "SRS-126", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Diagnostics, Platform Adapters", - "verification": "Security Audit", - "statement": "The software shall include security verification support for secure storage, private-key handling, password handling, input validation, and diagnostic redaction." - }, - { - "id": "SRS-128", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "All Software Components", - "verification": "System Test, Demo", - "statement": "The software shall support MVP acceptance verification demonstrating connection, channel tree, online clients, channel join, send/receive voice, mute, deaf, push-to-talk, required audio processing, channel text, bookmarks, secure storage, a" - }, - { - "id": "SRS-129", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, Audio Subsystem, Platform Adapters", - "verification": "System Test", - "statement": "The software shall expose clear user-facing errors when client hardware lacks a usable microphone, output device, CPU capacity, storage, or network interface required for the requested operation." - }, - { - "id": "SRS-130", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, Protocol Adapter", - "verification": "System Test", - "statement": "The software shall expose clear user-facing errors when network connectivity prevents server connection or real-time voice operation." - }, - { - "id": "SRS-131", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Rust Core, Flutter UI", - "verification": "System Test", - "statement": "The software shall handle external compatible server permissions, availability, and event behavior as external dependencies and shall not assume administrative control." - }, - { - "id": "SRS-132", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Rust Core, Protocol Adapter, Storage", - "verification": "UI Test, System Test", - "statement": "The software shall allow server details, credentials, and identity selection to be supplied by the user rather than hardcoding a server environment." - }, - { - "id": "SRS-133", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapters, Rust Core, Flutter UI", - "verification": "Platform Integration Test", - "statement": "The software shall integrate with OS-level secure storage, permission, audio, network, notification, lifecycle, and app sandboxing policies through platform adapters." - }, - { - "id": "SRS-134", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Software Requirements Engineering", - "verification": "Review, Protocol Probe", - "statement": "The software team shall analyze `tsclientlib` feasibility for all MVP protocol features and document unsupported or risky features before MVP acceptance." - }, - { - "id": "SRS-135", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Adapters", - "verification": "Review, Audio Test", - "statement": "The software team shall analyze real-time audio feasibility on each target platform for capture, playback, required audio processing, and latency." - }, - { - "id": "SRS-136", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, State Sync, Audio Subsystem, Diagnostics, Storage", - "verification": "Review, Performance Test", - "statement": "The software team shall analyze memory growth and define limits for event queues, chat history, logs, audio buffers, and diagnostic bundles." - }, - { - "id": "SRS-137", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Diagnostics, Platform Adapters, Flutter UI", - "verification": "Security Review, Privacy Review", - "statement": "The software team shall analyze security and privacy impacts of local secrets, logs, diagnostics, permissions, and release configuration." - }, - { - "id": "SRS-139", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review", - "statement": "The software requirements baseline shall account for SysDes-037: SE-19 Engineering Process and Evidence shall be a defined system element in the Chanora architecture with responsibility for: Architecture reviews, traceability records, verif" - }, - { - "id": "SRS-140", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review", - "statement": "The software requirements baseline shall account for SysDes-062: The permission-blocked mode shall prevent unsafe voice operations while preserving user visibility into corrective actions." - }, - { - "id": "SRS-141", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering", - "verification": "Review, Inspection", - "statement": "The SRS shall use SysDes design items as the mandatory architectural allocation source for every software requirement." - }, - { - "id": "SRS-142", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering, Change Control", - "verification": "Review, Inspection", - "statement": "If the software team identifies a required software behavior, constraint, or interface that cannot be derived from an existing SysDes design item, the SRS shall not be baselined until the SysDes is revised." - }, - { - "id": "SRS-143", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Requirements Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The SRS shall maintain a SysDes-to-SRS coverage matrix for all SysDes items that require software implementation, software enablement, software-facing constraint handling, or software verification support." - }, - { - "id": "SRS-144", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Design System", - "verification": "Review, Widget Test", - "statement": "The software shall initialize the Flutter client theme using Material 3 as the baseline UI system." - }, - { - "id": "SRS-145", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Inspection, Widget Test", - "statement": "The software shall provide a Chanora Design System layer above Material 3 for product-specific semantic states." - }, - { - "id": "SRS-146", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Unit Test", - "statement": "The software shall define semantic tokens for connection states." - }, - { - "id": "SRS-147", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Unit Test", - "statement": "The software shall define semantic tokens for voice states including speaking, muted, deafened, and push-to-talk active states." - }, - { - "id": "SRS-148", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Unit Test", - "statement": "The software shall define semantic tokens for latency, packet loss, diagnostics, and error states." - }, - { - "id": "SRS-149", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Inspection, Widget Test", - "statement": "The software shall expose design tokens through `ThemeData`, `ColorScheme`, `TextTheme`, component themes, `ThemeExtension`, or equivalent Flutter theming mechanisms." - }, - { - "id": "SRS-150", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter Feature Screens", - "verification": "Review, Static Inspection", - "statement": "Feature screens shall not hardcode product semantic colors, spacing, shape, density, or motion values." - }, - { - "id": "SRS-151", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell", - "verification": "Widget Test, Integration Test", - "statement": "The software shall provide an Adaptive Shell that resolves compact, medium, and expanded layout classes." - }, - { - "id": "SRS-152", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell", - "verification": "Widget Test, Integration Test", - "statement": "The compact layout shall use a single-column primary content structure and mobile-appropriate primary navigation." - }, - { - "id": "SRS-155", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Connection UI", - "verification": "Integration Test", - "statement": "The software shall keep connection status visible or directly reachable in compact, medium, and expanded layouts." - }, - { - "id": "SRS-156", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Voice UI", - "verification": "Integration Test", - "statement": "The software shall keep primary voice controls visible or directly reachable in compact, medium, and expanded layouts." - }, - { - "id": "SRS-157", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Design System", - "verification": "Accessibility Test", - "statement": "The software shall provide screen-reader semantics for critical connection, voice, channel, diagnostics, and settings controls." - }, - { - "id": "SRS-158", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Localization Service", - "verification": "Accessibility Test, Localization Test", - "statement": "The software shall provide localized accessibility labels for icon-only controls." - }, - { - "id": "SRS-159", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Design System", - "verification": "Widget Test, Accessibility Test", - "statement": "The software shall not express critical states by color alone; it shall also use text, iconography, shape, position, or semantic labeling." - }, - { - "id": "SRS-161", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "Accessibility Test", - "statement": "The software shall keep critical actions reachable under increased text scaling." - }, - { - "id": "SRS-162", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Platform Adapter", - "verification": "Platform Test", - "statement": "The software shall apply platform safe area, display cutout, system bar, virtual keyboard, and desktop window inset constraints through shared shell behavior." - }, - { - "id": "SRS-165", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Localization Service", - "verification": "Unit Test, Integration Test", - "statement": "The software shall provide a Localization Service for product-owned user-visible strings." - }, - { - "id": "SRS-166", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Localization Service", - "verification": "Inspection", - "statement": "The software shall externalize product-owned user-visible strings into localization resources or an equivalent mechanism." - }, - { - "id": "SRS-167", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Localization Service", - "verification": "Localization Test", - "statement": "The software shall provide English as the baseline product locale." - }, - { - "id": "SRS-168", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Localization Service", - "verification": "Unit Test", - "statement": "The software shall implement deterministic fallback behavior when a translation key is missing." - }, - { - "id": "SRS-170", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Protocol DTOs", - "verification": "Integration Test", - "statement": "The software shall display server-provided server names, channel names, client nicknames, and text messages as content rather than translating them." - }, - { - "id": "SRS-171", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Bridge, Rust Core, Flutter UI", - "verification": "Integration Test", - "statement": "The software shall preserve Unicode text received from compatible servers through protocol adapter, bridge DTO, state store, and UI rendering paths." - }, - { - "id": "SRS-172", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge, Rust Core, Storage, Diagnostics, Protocol Adapter", - "verification": "Inspection, Integration Test", - "statement": "The software shall use UTF-8 internally for cross-layer text DTOs unless a boundary adapter requires conversion." - }, - { - "id": "SRS-174", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Storage", - "verification": "Test, Audit", - "statement": "The software shall not corrupt multilingual Unicode content in logs or diagnostic exports unless redaction intentionally removes sensitive content." - }, - { - "id": "SRS-180", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Engineering", - "verification": "Inspection", - "statement": "The software requirements baseline shall derive requirements only from `SysDes-XXX` sources." - }, - { - "id": "SRS-181", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Engineering", - "verification": "Inspection", - "statement": "The SRS shall not use direct upstream system-requirement source fields or direct system-requirement trace links." - }, - { - "id": "SRS-182", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Architecture", - "verification": "Inspection", - "statement": "The software architecture shall derive architecture items only from `SRS-XXX` sources." - }, - { - "id": "SRS-183", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Software Design", - "verification": "Inspection", - "statement": "The software detailed design shall derive detailed design items only from `SAD-XXX` sources." - }, - { - "id": "SRS-184", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Design System, Localization Service, Platform Adapter", - "verification": "Integration Test", - "statement": "Theme, localization, and initial platform adaptation shall be initialized before feature screens render user-visible content." - }, - { - "id": "SRS-185", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "iOS Build Configuration, Platform Adapter", - "verification": "Platform Test, Release Inspection", - "statement": "The software shall define iOS 13 as the minimum iOS runtime deployment baseline unless approved platform constraints raise the minimum version." - }, - { - "id": "SRS-186", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "iOS Build Configuration, Release Pipeline", - "verification": "Release Inspection", - "statement": "The iOS/iPadOS release build process shall require Xcode 26 or later and the iOS 26 / iPadOS 26 SDK or later for App Store Connect upload on or after 2026-04-28, unless Apple publishes a newer applicable upload requirement before upload." - }, - { - "id": "SRS-187", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Android Build Configuration, Platform Adapter", - "verification": "Platform Test, Release Inspection", - "statement": "The software shall define Android API 28 (Android 9.0) as the minimum Android runtime baseline, per DEC-004 (Accepted 2026-05-14, which raised the original API 24 recommendation to API 28). The minimum may be raised further only if Flutter," - }, - { - "id": "SRS-188", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Android Build Configuration, Release Pipeline", - "verification": "Release Inspection", - "statement": "The Android release build process shall target the Android API level required by Google Play on the upload date." - }, - { - "id": "SRS-189", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, Flutter UI, Protocol Adapter, State Sync", - "verification": "System Test", - "statement": "The software shall permit only one active server connection per client instance in MVP." - }, - { - "id": "SRS-190", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Settings UI, Platform Audio", - "verification": "Audio Test", - "statement": "The software shall enable AEC, AGC, Noise Suppression, and High-Pass Filter by default where supported and stable, with user or platform control to disable supported processing where applicable." - }, - { - "id": "SRS-192", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Storage, Platform Secure Storage", - "verification": "Storage Test, Security Audit", - "statement": "The software shall use SQLite or an equivalent embedded local database for non-secret local state and platform secure storage for secrets." - }, - { - "id": "SRS-193", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Bridge, Flutter State, Rust Core", - "verification": "Integration Test, Architecture Review", - "statement": "The software shall use a stable typed Flutter/Rust bridge with generated or schema-controlled DTOs." - }, - { - "id": "SRS-194", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Privacy, Release Pipeline", - "verification": "Privacy Review, Security Audit", - "statement": "The software shall not perform automatic diagnostic upload, automatic telemetry upload, or automatic crash reporting in MVP unless a later approved decision updates privacy, security, legal, release, and verification baselines." - }, - { - "id": "SRS-195", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio, Platform Input", - "verification": "Architecture Review, Unit Test", - "statement": "The software shall expose a `DesktopPttBackend` trait owned by the audio subsystem, with one implementation per supported desktop platform (Windows, macOS, Linux/GNOME-Wayland) and a `FocusedPttBackend` implementation used as the universal " - }, - { - "id": "SRS-196", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio, Rust Core", - "verification": "Unit Test, Integration Test", - "statement": "The software shall publish a `PttCapabilityLevel` enum value (`L0` Focused, `L1` Global-shortcut-activation, `L2` Global-hold-to-talk, `L3` Global-with-mouse-buttons, `L4` Device-aware) from the active backend, and the value shall match the" - }, - { - "id": "SRS-197", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Windows)", - "verification": "Platform Test (Windows), Unit Test", - "statement": "The Windows desktop backend shall attempt Raw Input first; if Raw Input is unavailable or initialisation fails, it shall attempt a low-level keyboard hook; if that also fails it shall return `PttCapabilityLevel::L0` and engage Focused PTT. " - }, - { - "id": "SRS-198", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (macOS)", - "verification": "Platform Test (macOS), User Acceptance Test", - "statement": "The macOS desktop backend shall query the operating-system Input Monitoring / Accessibility permission state, return `PttCapabilityLevel::L0` while the permission is undecided or denied, and upgrade to the appropriate Global level only afte" - }, - { - "id": "SRS-199", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Linux)", - "verification": "Platform Test (Linux, GNOME Wayland), Unit Test", - "statement": "The Linux desktop backend shall probe the active display server and compositor at runtime. On GNOME-on-Wayland (the officially-tested target per DEC-026), the backend shall use the freedesktop GlobalShortcuts portal. On any other Linux envi" - }, - { - "id": "SRS-200", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Windows/macOS/Linux)", - "verification": "Platform Test, User Acceptance Test", - "statement": "The software shall support mouse side buttons (typically labelled Mouse4 / Mouse5 or \"back\" / \"forward\") as bindable inputs for Global PTT on the Windows and macOS backends per DEC-027. The Linux GlobalShortcuts portal binding shall accept " - }, - { - "id": "SRS-201", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Integration Test", - "statement": "The audio engine shall expose `capture_active` and `transmit_active` as independent atomic states. `capture_active` shall reflect the input-stream lifecycle and the platform input-permission state; `transmit_active` shall reflect the PTT su" - }, - { - "id": "SRS-202", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Audio", - "verification": "Privacy Review, Unit Test, Diagnostic Inspection", - "statement": "The diagnostics subsystem shall reject any log record carrying a raw key code, scan code, virtual-key value, or key-press timing sequence. The diagnostic export shall include the active `PttCapabilityLevel`, the active backend identifier, a" - }, - { - "id": "SRS-203", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Integration Test", - "statement": "The software shall implement a missed-key-up watchdog per DEC-028: if `transmit_active` has been true for longer than a configured upper bound without a corresponding release event (for example because the OS suppressed the key-up while the" - }, - { - "id": "SRS-204", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio, Bridge, Flutter UI", - "verification": "Integration Test, UI Review", - "statement": "The audio engine shall open the input and output streams on the user's first voice-channel join of the session and shall close them on the last voice-channel leave. The software shall expose no `start_audio` or equivalent manual-start opera" - }, - { - "id": "SRS-205", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio, Bridge, Flutter UI", - "verification": "Unit Test, UI Review", - "statement": "The software shall represent the user's voice transmit mode as a `TransmitMode` enum with variants `Ptt`, `Continuous`, and `VoiceActivity` (the last reserved with no v1 implementation per DEC-030). The setting shall be persisted per identi" - }, - { - "id": "SRS-206", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio", - "verification": "Unit Test, Integration Test", - "statement": "The software shall expose a `release_tail_ms` configuration value (default 200 ms, validated range 0 through 500 ms inclusive) and shall delay the `true → false` transition of `transmit_active` by `release_tail_ms` after the PTT backend rep" - }, - { - "id": "SRS-207", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio, Flutter UI", - "verification": "Functional Test, UI Review", - "statement": "The software shall expose a hard-mute toggle in the Voice Bar UI. While hard-mute is engaged, `transmit_active` shall be forced false regardless of the active transmit mode, the PTT key state, or any other internal signal. Hard-mute state s" - }, - { - "id": "SRS-208", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Android), Platform Adapter (Android)", - "verification": "Android Integration Test, Audio Test", - "statement": "The Chanora Android client shall engage the Android in-call audio mode (for example via `AudioManager.setMode(MODE_IN_COMMUNICATION)` or an equivalent platform routing-assist mechanism) no later than the moment a voice session becomes conne" - }, - { - "id": "SRS-209", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapter (Android), Audio, Flutter UI", - "verification": "Android Integration Test, UI Test, Privacy Review", - "statement": "The Chanora Android client shall request the runtime microphone permission (`RECORD_AUDIO`) at or before voice session activation, and shall not begin microphone capture without that permission having been granted. If the permission is deni" - }, - { - "id": "SRS-210", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Android), Platform Adapter (Android)", - "verification": "Android Integration Test, Audio Test, Loopback Latency Measurement", - "statement": "The Chanora Android voice audio path shall achieve a round-trip mouth-to-ear (\"glass-to-glass\") latency of at most 150 ms on Android devices that support AAudio low-latency performance mode, and at most 250 ms on Android devices that do not" - }, - { - "id": "SRS-211", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Android), Platform Adapter (Android)", - "verification": "Android Integration Test, Audio Test", - "statement": "The Chanora Android voice-capture path shall request the platform \"voice communication\" input preset where the operating system exposes one, so that the OS-side capture chain (gain shaping, far-end suppression, microphone selection) is the " - }, - { - "id": "SRS-212", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Android), Platform Adapter (Android), Audio Engine", - "verification": "Android Integration Test, Audio Test", - "statement": "Where the Android device exposes the hardware voice-audio effect APIs `AcousticEchoCanceler`, `NoiseSuppressor`, and `AutomaticGainControl`, the Chanora Android client shall engage each available effect on the active microphone capture sess" - }, - { - "id": "SRS-213", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Android), Platform Adapter (Android)", - "verification": "Android Integration Test, Audio Test", - "statement": "The Chanora Android voice-output stream shall declare `AAUDIO_USAGE_VOICE_COMMUNICATION` and `AAUDIO_CONTENT_TYPE_SPEECH` at stream construction so that the Android audio policy engine routes the stream under the voice-communication routing" - }, - { - "id": "SRS-214", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Audio (Android), Platform Adapter (Android)", - "verification": "Android Integration Test, Audio Test", - "statement": "The Chanora Android voice input and output streams shall request `AAUDIO_SHARING_MODE_EXCLUSIVE` on a best-effort basis at stream construction, and shall gracefully fall back to `AAUDIO_SHARING_MODE_SHARED` when the platform denies exclusiv" - }, - { - "id": "SRS-215", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapter (Android), Audio (Android), Android Manifest", - "verification": "Android Integration Test, Manifest Review", - "statement": "The Chanora Android client shall maintain active microphone capture while the application is backgrounded by hosting voice transmission inside an Android foreground service whose manifest declares `foregroundServiceType=\"microphone\"`. This " - }, - { - "id": "SRS-216", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "`chanora_audio` crate (realtime capture and playback modules, plus a co-located `benches/` directory)", - "verification": "SWE.4 — automated benchmark execution against the instrumentation surface; reviewers confirm the five metric families enumerated in SysDes-156 each have at least one benchmark target.", - "statement": "The `chanora_audio` software shall expose its realtime capture and playback paths to benchmark instrumentation that covers the five metrics enumerated by SysDes-156: (1) heap allocation count per realtime callback after a warmup window of a" - }, - { - "id": "SRS-217", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "`chanora_audio` crate (the bench harness publishes the baseline JSON), plus the SDD-decided on-disk path under the repository root.", - "verification": "Review — the JSON record format is review-checkable against this clause by inspecting a sample baseline file.", - "statement": "Baseline measurements produced by the SRS-216 benchmark instrumentation shall be stored as structured JSON committed to a deterministic location inside the repository, so that the baseline becomes a reviewable, version-controlled artefact r" - }, - { - "id": "SRS-218", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "CI tooling under `.github/workflows/` (or the SDD-decided equivalent integration point) — specifically a new advisory benchmark workflow file; the exact filename is delegated to SAD/SDD.", - "verification": "Demo — a synthetic pull-request run shall demonstrate the workflow firing, the markdown comparison table rendering, the status check reporting `success` even when a 🔴 marker is present, and the absence of any write to the baseline JSON file under the PR-triggered path. Human reviewers confirm in the PR.", - "statement": "A continuous-integration workflow shall execute the SRS-216 benchmark suite under the constraints authored by SysDes-157, namely: (1) the workflow shall trigger on every pull request opened against the default branch and on every merge to t" - }, - { - "id": "SRS-219", - "source": "requirements/srs.md", - "stage": "P0 / MVP", - "allocation": "CI tooling co-located with the SRS-218 workflow (the comparison logic resides in the same SE-18 surface).", - "verification": "Review — the comparison logic and the tolerance table are review-checkable against this clause; a small synthetic data set (one baseline, one PR run for each of the five metric families with values at, just inside, and just outside the tolerance window) demonstrates each marker assignment.", - "statement": "The advisory comparison performed by the SRS-218 workflow shall apply the per-metric tolerance window and the comparison methodology authored by SysDes-158. The tolerance window shall be evaluated per metric as follows: (a) heap allocation " - }, - { - "id": "SysDes-001", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "This SysDes shall establish the Chanora system architectural design and allocate all SysRS requirements to system elements." - }, - { - "id": "SysDes-002", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "This SysDes shall treat Chanora as a cross-platform application system, not as an operating system." - }, - { - "id": "SysDes-003", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "This SysDes shall be used as the system-level design baseline for downstream SRS, SAD, SDD, and Verification artifacts." - }, - { - "id": "SysDes-004", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The SysDes structure shall provide evidence for static architecture, dynamic architecture, architecture analysis, bidirectional traceability, consistency, and architecture communication." - }, - { - "id": "SysDes-005", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "The SysDes shall identify system elements, their responsibilities, their relationships, and their external interfaces." - }, - { - "id": "SysDes-006", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Platform, Software: Diagnostics, Verification, Deployment / Operations", - "verification": "Review, Inspection", - "statement": "The SysDes shall describe dynamic behavior for connection, synchronization, voice transmission, voice reception, text messaging, reconnect, permissions, diagnostics, and release operation." - }, - { - "id": "SysDes-007", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "The SysDes shall include architecture analysis, major design rationale, known risks, mitigations, and special characteristics." - }, - { - "id": "SysDes-008", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The SysDes shall maintain bidirectional traceability between every SysRS requirement and at least one allocated system element." - }, - { - "id": "SysDes-009", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The SysDes shall identify affected parties for review, agreement, and communication of the system architecture." - }, - { - "id": "SysDes-010", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Protocol Adapter, Software: State Sync, Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Review", - "statement": "The architecture shall separate user interface, protocol logic, state synchronization, audio processing, storage, diagnostics, and platform-specific services." - }, - { - "id": "SysDes-011", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter", - "verification": "Review", - "statement": "The architecture shall isolate direct `tsclientlib` usage inside the protocol adapter." - }, - { - "id": "SysDes-012", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Rust Core", - "verification": "Review", - "statement": "The architecture shall support required target platforms through shared Flutter UI, shared Rust Core, and platform-specific adapters." - }, - { - "id": "SysDes-013", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Review", - "statement": "The architecture shall support production-grade audio processing including Echo Canceller, Automatic Gain Control, Noise Suppression, and High-Pass Filter." - }, - { - "id": "SysDes-014", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Audio Subsystem", - "verification": "Review", - "statement": "The architecture shall support real-time voice behavior without placing real-time audio processing in Flutter UI." - }, - { - "id": "SysDes-015", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Diagnostics, Verification", - "verification": "Review", - "statement": "The architecture shall support secure local handling of identities, passwords, and diagnostics." - }, - { - "id": "SysDes-016", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review", - "statement": "The architecture shall support operation without a Chanora-operated central backend in MVP." - }, - { - "id": "SysDes-017", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter, Software: Audio Subsystem, Deployment / Operations", - "verification": "Review", - "statement": "The architecture shall provide traceable verification hooks for protocol, state, audio, security, packaging, and platform behavior." - }, - { - "id": "SysDes-018", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "External Server, Software: Audio Subsystem, Deployment / Operations", - "verification": "Review, Inspection", - "statement": "The static architecture shall define Chanora as a client application system interacting with users, audio hardware, OS services, networks, deployment environments, and external compatible voice servers." - }, - { - "id": "SysDes-019", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "User / Operator, Platform, Software: Diagnostics, Verification", - "verification": "Review, Inspection", - "statement": "SE-01 User and Operator shall be a defined system element in the Chanora architecture with responsibility for: Human users who operate Chanora, provide server details, manage permissions, and initiate diagnostic export." - }, - { - "id": "SysDes-020", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Hardware, Software: Audio Subsystem", - "verification": "Review, Inspection", - "statement": "SE-02 Client Device Hardware shall be a defined system element in the Chanora architecture with responsibility for: Physical device running Chanora, including CPU, memory, storage, network interfaces, microphone, speaker/headphone, and opti" - }, - { - "id": "SysDes-021", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform, Software: Audio Subsystem, Platform Secure Storage", - "verification": "Review, Inspection", - "statement": "SE-03 Operating System Services shall be a defined system element in the Chanora architecture with responsibility for: OS services used by Chanora: permissions, secure storage, audio stack, network stack, notifications, windowing, lifecycle" - }, - { - "id": "SysDes-022", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Network", - "verification": "Review, Inspection", - "statement": "SE-04 Network Environment shall be a defined system element in the Chanora architecture with responsibility for: IP network path between client and external compatible server, including DNS, routing, NAT, firewall, VPN, Wi-Fi, cellular, and" - }, - { - "id": "SysDes-023", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "External Server, Software: Audio Subsystem, Platform", - "verification": "Review, Inspection", - "statement": "SE-05 External Compatible Voice Server shall be a defined system element in the Chanora architecture with responsibility for: TeamSpeak 3-compatible external server providing channels, clients, identity authentication, voice packets, text m" - }, - { - "id": "SysDes-024", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software, Platform", - "verification": "Review, Inspection", - "statement": "SE-06 Chanora Application Container shall be a defined system element in the Chanora architecture with responsibility for: Installed Chanora client application package on each target platform. Owns app lifecycle at the application boundary." - }, - { - "id": "SysDes-025", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Audio Subsystem, Platform, Software: Diagnostics, Verification", - "verification": "Review, Inspection", - "statement": "SE-07 Flutter UI Layer shall be a defined system element in the Chanora architecture with responsibility for: Cross-platform user interface for connection, channel tree, chat, voice controls, settings, permissions, and diagnostics." - }, - { - "id": "SysDes-026", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter State, Software: Rust Core, Software: State Sync", - "verification": "Review, Inspection", - "statement": "SE-08 Flutter State Layer shall be a defined system element in the Chanora architecture with responsibility for: UI-facing state model derived from Rust Core events and snapshots; owns only presentation state." - }, - { - "id": "SysDes-027", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Bridge, Software: Rust Core", - "verification": "Review, Inspection", - "statement": "SE-09 Flutter-Rust Bridge Layer shall be a defined system element in the Chanora architecture with responsibility for: Generated/native bridge for commands, DTOs, asynchronous calls, event streams, and error propagation between Flutter and " - }, - { - "id": "SysDes-028", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Review, Inspection", - "statement": "SE-10 Rust Core and Connection Manager shall be a defined system element in the Chanora architecture with responsibility for: Authoritative application core coordinating connection actors, commands, events, storage, audio, diagnostics, and " - }, - { - "id": "SysDes-029", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter, Software: Audio Subsystem", - "verification": "Review, Inspection", - "statement": "SE-11 Protocol Adapter using tsclientlib shall be a defined system element in the Chanora architecture with responsibility for: Protocol subsystem wrapping `tsclientlib`, isolating protocol library types, errors, events, and voice packet op" - }, - { - "id": "SysDes-030", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync", - "verification": "Review, Inspection", - "statement": "SE-12 State Synchronization Engine shall be a defined system element in the Chanora architecture with responsibility for: Snapshot + delta state model, deterministic reducers, connection state machine, event ordering, and replayable state b" - }, - { - "id": "SysDes-031", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Review, Inspection", - "statement": "SE-13 Audio Subsystem shall be a defined system element in the Chanora architecture with responsibility for: Capture/playback coordination, Opus-compatible encoding/decoding, jitter buffer, mixer, push-to-talk, mute/deaf, level metering, an" - }, - { - "id": "SysDes-032", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Platform, Platform Secure Storage, Deployment / Operations", - "verification": "Review, Inspection", - "statement": "SE-14 Platform Adapter Layer shall be a defined system element in the Chanora architecture with responsibility for: Platform-specific adapters for audio devices, permissions, lifecycle, notifications, secure storage integration, and release" - }, - { - "id": "SysDes-033", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Software: Storage", - "verification": "Review, Inspection", - "statement": "SE-15 Local Data Store shall be a defined system element in the Chanora architecture with responsibility for: Embedded local database or equivalent storage for bookmarks, recent servers, audio settings, UI settings, per-user volume and mute" - }, - { - "id": "SysDes-034", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Secure Storage", - "verification": "Review, Inspection", - "statement": "SE-16 Secure Storage Provider shall be a defined system element in the Chanora architecture with responsibility for: Platform secure storage integration for identity private keys, server passwords, and future sensitive tokens." - }, - { - "id": "SysDes-035", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Review, Inspection", - "statement": "SE-17 Diagnostics and Observability shall be a defined system element in the Chanora architecture with responsibility for: Structured logs, redaction, event recording, event replay, network diagnostics, audio diagnostics, diagnostic bundle " - }, - { - "id": "SysDes-036", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Storage, Deployment / Operations", - "verification": "Review, Inspection", - "statement": "SE-18 Deployment and Release Environment shall be a defined system element in the Chanora architecture with responsibility for: Packaging, signing, notarization, app-store release, installers, Linux packages, CI/CD, release metadata, and di" - }, - { - "id": "SysDes-037", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "SE-19 Engineering Process and Evidence shall be a defined system element in the Chanora architecture with responsibility for: Architecture reviews, traceability records, verification evidence, compatibility matrix, open decisions, and downs" - }, - { - "id": "SysDes-038", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Flutter State, Software: Bridge, Software: Rust Core", - "verification": "Review, Inspection", - "statement": "The architecture shall use a layered decomposition in which Flutter UI and Flutter State Layer communicate with Rust Core only through the Bridge Layer." - }, - { - "id": "SysDes-039", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Protocol Adapter, Software: State Sync, Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Review, Inspection", - "statement": "The architecture shall place protocol, state synchronization, audio processing, storage coordination, diagnostics, and business rules inside Rust Core or Rust Core-owned elements." - }, - { - "id": "SysDes-040", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Flutter State, Software: Protocol Adapter", - "verification": "Review, Inspection", - "statement": "The architecture shall prevent Flutter UI and Flutter State Layer from invoking `tsclientlib` or depending on raw `tsclientlib` types." - }, - { - "id": "SysDes-041", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Rust Core, Software: Audio Subsystem", - "verification": "Review, Inspection", - "statement": "The architecture shall place real-time audio processing outside Flutter UI and inside the Rust Core audio subsystem or platform-native audio processing adapters." - }, - { - "id": "SysDes-042", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "Every SysRS requirement shall be allocated to at least one system element." - }, - { - "id": "SysDes-043", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "External Server, Deployment / Operations, System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "A SysRS requirement may be allocated to multiple system elements when the requirement depends on cooperation between application software, OS services, hardware, network, external server, deployment, or engineering evidence." - }, - { - "id": "SysDes-044", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Rust Core", - "verification": "Review, Inspection", - "statement": "Functional user-facing requirements shall be allocated to Flutter UI, Rust Core, and the relevant subsystem element that executes the behavior." - }, - { - "id": "SysDes-045", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Hardware, Platform, Network", - "verification": "Review, Inspection", - "statement": "Runtime environment requirements shall be allocated to client device hardware, operating system services, platform adapters, network environment, or external compatible server elements." - }, - { - "id": "SysDes-046", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform, Platform Secure Storage, Software: Diagnostics, Verification, System Engineering", - "verification": "Review, Inspection", - "statement": "Security, privacy, and diagnostics requirements shall be allocated to secure storage, diagnostics, platform adapters, and engineering process evidence." - }, - { - "id": "SysDes-047", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "MVP acceptance requirements shall be allocated to the system elements that implement or enable the accepted behavior." - }, - { - "id": "SysDes-048", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Audio Subsystem, Platform, Software: Diagnostics, Verification", - "verification": "Inspection, Integration Test", - "statement": "IF-001 User Interface shall expose connection, bookmark, channel tree, online client, chat, voice control, audio setting, permission, and diagnostic workflows." - }, - { - "id": "SysDes-049", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Bridge, Software: Protocol Adapter", - "verification": "Inspection, Integration Test", - "statement": "IF-003 Flutter-Rust Command Interface shall use bridge-safe DTOs and shall not expose raw protocol or `tsclientlib` types." - }, - { - "id": "SysDes-050", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: State Sync, Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Inspection, Integration Test", - "statement": "IF-004 Rust Core Event Interface shall deliver connection status, snapshots, deltas, voice indicators, errors, and diagnostic events as typed streams." - }, - { - "id": "SysDes-051", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter", - "verification": "Inspection, Integration Test", - "statement": "IF-005 Protocol Adapter Interface shall isolate all direct `tsclientlib` operations and convert protocol data into Chanora internal data structures." - }, - { - "id": "SysDes-052", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "External Server, Software: Protocol Adapter", - "verification": "Inspection, Integration Test", - "statement": "IF-006 External Server Protocol Interface shall use `tsclientlib` to communicate with reachable TeamSpeak 3-compatible external servers." - }, - { - "id": "SysDes-053", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Inspection, Integration Test", - "statement": "IF-007 Audio Capture Interface shall provide PCM capture frames, device state, route changes, and capture errors to the audio subsystem." - }, - { - "id": "SysDes-054", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Inspection, Integration Test", - "statement": "IF-008 Audio Playback Interface shall provide mixed PCM playback frames, playback reference for echo cancellation where required, and playback route events." - }, - { - "id": "SysDes-055", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Storage, Platform Secure Storage", - "verification": "Inspection, Integration Test", - "statement": "IF-009 Secure Storage Interface shall store identity private keys, server passwords, and future sensitive tokens through platform-secure mechanisms." - }, - { - "id": "SysDes-056", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Storage", - "verification": "Inspection, Integration Test", - "statement": "IF-010 Local Data Store Interface shall persist non-secret data through SQLite or an equivalent embedded database." - }, - { - "id": "SysDes-057", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Diagnostics, Verification", - "verification": "Inspection, Integration Test", - "statement": "IF-011 Diagnostics Export Interface shall export user-initiated diagnostic bundles with redaction applied before export." - }, - { - "id": "SysDes-058", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Diagnostics, Verification", - "verification": "Inspection, Integration Test", - "statement": "IF-013 Network Interface shall provide connectivity to external compatible servers and network diagnostic inputs for reconnect and troubleshooting." - }, - { - "id": "SysDes-059", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Platform", - "verification": "Inspection, Integration Test", - "statement": "IF-014 Permission Interface shall mediate microphone, notification, audio session, foreground service, and lifecycle permission behavior on supported platforms." - }, - { - "id": "SysDes-060", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core", - "verification": "System Test, Demo", - "statement": "The connection state machine shall be owned by Rust Core and reflected to Flutter as UI-safe connection status events." - }, - { - "id": "SysDes-061", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter, Software: Audio Subsystem, Platform", - "verification": "System Test, Demo", - "statement": "The voice activity mode shall be active only when a channel context, audio device state, permission state, and protocol voice path are valid." - }, - { - "id": "SysDes-062", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Platform", - "verification": "System Test, Demo", - "statement": "The permission-blocked mode shall prevent unsafe voice operations while preserving user visibility into corrective actions." - }, - { - "id": "SysDes-063", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync", - "verification": "System Test, Demo", - "statement": "The reconnect mode shall preserve UI responsiveness and rebuild authoritative server state from a fresh snapshot after reconnect." - }, - { - "id": "SysDes-064", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Diagnostics, Verification", - "verification": "System Test, Demo", - "statement": "The diagnostics export mode shall be explicitly user-initiated and shall apply redaction before export." - }, - { - "id": "SysDes-065", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Bridge, Software: Rust Core", - "verification": "System Test, Demo", - "statement": "Manual connection flow shall pass host, port, nickname, optional password, and identity reference from Flutter UI to Rust Core through bridge DTOs." - }, - { - "id": "SysDes-066", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Protocol Adapter", - "verification": "System Test, Demo", - "statement": "Rust Core shall use the protocol adapter to establish server connection and authentication through `tsclientlib`." - }, - { - "id": "SysDes-067", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync", - "verification": "System Test, Demo", - "statement": "Initial synchronization shall fetch server information, channel list, client list, and required self-client information before emitting a full snapshot." - }, - { - "id": "SysDes-068", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync", - "verification": "System Test, Demo", - "statement": "Live server events shall be converted into internal events, applied through deterministic reducers, and emitted as UI-safe delta events." - }, - { - "id": "SysDes-069", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Protocol Adapter", - "verification": "System Test, Demo", - "statement": "Channel join flow shall wait for Rust Core/protocol confirmation before treating the UI state as authoritative." - }, - { - "id": "SysDes-070", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Protocol Adapter", - "verification": "System Test, Demo", - "statement": "Text message flow shall route user text from Flutter to Rust Core to protocol adapter and route received messages back as core events." - }, - { - "id": "SysDes-071", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter, Software: Audio Subsystem", - "verification": "System Test, Demo", - "statement": "Outgoing voice flow shall capture PCM frames, apply required processing, encode voice, and send voice packets through the protocol adapter." - }, - { - "id": "SysDes-072", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "System Test, Demo", - "statement": "Incoming voice flow shall receive voice packets, jitter-buffer, decode, apply per-user controls, mix, and play through platform playback." - }, - { - "id": "SysDes-073", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software", - "verification": "System Test, Demo", - "statement": "User-triggered disconnect flow shall transition to Disconnected and shall suppress automatic reconnect." - }, - { - "id": "SysDes-074", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync", - "verification": "System Test, Demo", - "statement": "Recoverable network failure flow shall transition to Reconnecting, apply reconnect policy, and rebuild state from a fresh snapshot." - }, - { - "id": "SysDes-075", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Platform", - "verification": "System Test, Demo", - "statement": "Audio device change flow shall report route/device changes through platform adapters and shall recover or enter a user-safe blocked/degraded state." - }, - { - "id": "SysDes-076", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Storage, Platform Secure Storage, Software: Diagnostics, Verification", - "verification": "System Test, Demo", - "statement": "Secure storage flow shall store and retrieve secrets through platform secure storage without exposing plaintext secrets to logs or diagnostic export." - }, - { - "id": "SysDes-077", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Protocol Adapter, Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Review, Analysis", - "statement": "The architecture shall select Flutter + Rust Core to meet cross-platform UI requirements while centralizing protocol, state, audio, storage, and diagnostics logic." - }, - { - "id": "SysDes-078", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter", - "verification": "Review, Analysis", - "statement": "The architecture shall isolate `tsclientlib` inside the protocol adapter to reduce impact if protocol implementation must be patched, forked, or replaced." - }, - { - "id": "SysDes-079", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync, System Engineering, Verification", - "verification": "Review, Analysis", - "statement": "The architecture shall use snapshot + delta synchronization to support deterministic state recovery, reconnect behavior, event replay, and UI consistency." - }, - { - "id": "SysDes-080", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem, Platform, Platform Secure Storage, Deployment / Operations", - "verification": "Review, Analysis", - "statement": "The architecture shall use platform adapters to isolate OS-specific permission, audio routing, lifecycle, secure storage, notification, and packaging concerns." - }, - { - "id": "SysDes-081", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Review, Analysis", - "statement": "The architecture shall implement audio processing through a backend abstraction that supports platform-native and Rust-based processing options." - }, - { - "id": "SysDes-082", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform", - "verification": "Review, Analysis", - "statement": "The architecture shall define external compatible server behavior as an external dependency and shall not assume control over server-side permissions, availability, or configuration." - }, - { - "id": "SysDes-083", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Review, Analysis", - "statement": "The architecture shall not introduce Chanora-operated backend infrastructure for MVP voice, channel, or text operation." - }, - { - "id": "SysDes-084", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter, Software: Audio Subsystem, Platform, Software: Diagnostics, Verification, Deployment / Operations", - "verification": "Review, Analysis", - "statement": "The architecture shall treat real-time audio behavior, secure secret storage, protocol compatibility, mobile lifecycle policy, diagnostic redaction, and release signing as special characteristics requiring explicit verification." - }, - { - "id": "SysDes-085", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Flutter UI, Software: Bridge, Software: Rust Core", - "verification": "Review", - "statement": "The architecture shall allocate UI responsiveness to Flutter UI, Bridge Layer, Rust Core asynchronous command handling, and non-blocking reconnect behavior." - }, - { - "id": "SysDes-086", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Hardware, Network, Software: Audio Subsystem, Platform", - "verification": "Test, Analysis", - "statement": "The architecture shall allocate local audio latency requirements to client device hardware, OS audio stack, platform adapters, audio subsystem, codec processing, and network environment." - }, - { - "id": "SysDes-087", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Audio Subsystem, Software: Diagnostics, Verification", - "verification": "Test, Analysis", - "statement": "The architecture shall allocate memory growth constraints to Rust Core queue management, diagnostics log retention, chat history limits, and audio buffer bounds." - }, - { - "id": "SysDes-088", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Protocol Adapter, Software: State Sync, Software: Audio Subsystem", - "verification": "Test, Analysis", - "statement": "The architecture shall allocate reliability recovery requirements to Rust Core reconnect policy, protocol adapter error mapping, state engine snapshot replacement, and platform audio recovery." - }, - { - "id": "SysDes-089", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Secure Storage, Software: Diagnostics, Verification", - "verification": "Audit", - "statement": "The architecture shall allocate security requirements to platform secure storage, secret redaction, diagnostics redaction, user-safe error mapping, and input validation." - }, - { - "id": "SysDes-090", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform, Software: Diagnostics, Verification", - "verification": "Audit", - "statement": "The architecture shall allocate privacy requirements to user-controlled diagnostics, minimal data collection, and permission explainers." - }, - { - "id": "SysDes-091", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Storage, Deployment / Operations, System Engineering, Verification", - "verification": "Inspection, Demo", - "statement": "The architecture shall allocate packaging and release requirements to deployment environment, signing/notarization/app-store packaging, and release metadata review." - }, - { - "id": "SysDes-092", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "Each SysRS requirement shall trace forward to one or more system elements and to one or more SysDes design items with Type, Stage, Allocation, and Verification attributes." - }, - { - "id": "SysDes-093", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "Each system element shall trace backward to one or more SysRS requirements unless explicitly marked as supporting infrastructure." - }, - { - "id": "SysDes-094", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "Each interface shall trace backward to the SysRS requirements that require the interaction or runtime dependency." - }, - { - "id": "SysDes-095", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "Each dynamic flow shall trace backward to the SysRS functional, state, performance, security, or operational requirements that require the behavior." - }, - { - "id": "SysDes-096", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "Architecture changes shall update the SysRS allocation matrix, affected interfaces, affected dynamic flows, and verification hooks." - }, - { - "id": "SysDes-097", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "Consistency review shall check that SysDes does not contradict SysRS, does not introduce unapproved scope, and does not remove required MVP capabilities." - }, - { - "id": "SysDes-098", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Rust Core, Software: Audio Subsystem, Deployment / Operations, System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The system architecture shall be reviewed by product, system engineering, Rust Core engineering, Flutter engineering, audio engineering, platform engineering, QA, security/privacy, and release stakeholders." - }, - { - "id": "SysDes-099", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "Architecture review evidence shall include review date, participants, open issues, decisions, accepted risks, and approval status." - }, - { - "id": "SysDes-100", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering", - "verification": "Review, Inspection", - "statement": "The agreed SysDes baseline shall be communicated to downstream SRS, SAD, SDD, and Verification authors." - }, - { - "id": "SysDes-101", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Protocol Adapter, Software: Audio Subsystem, Platform, Platform Secure Storage, Deployment / Operations, System Engineering, Verification", - "verification": "Review, Inspection", - "statement": "Any change affecting `tsclientlib` isolation, real-time audio processing, secure storage, mobile lifecycle, or release wording shall trigger architecture impact review." - }, - { - "id": "SysDes-102", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "External Server, Software: Protocol Adapter", - "verification": "Test, Demo", - "statement": "Protocol verification shall use a protocol probe tool and compatibility matrix to verify external server compatibility and `tsclientlib` adapter behavior." - }, - { - "id": "SysDes-103", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: State Sync", - "verification": "Test, Demo", - "statement": "State verification shall use reducer tests and event replay tests to verify snapshot, delta, reconnect, and malformed-event behavior." - }, - { - "id": "SysDes-104", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Audio Subsystem", - "verification": "Test, Demo", - "statement": "Audio verification shall use audio loopback and processing tests to verify capture, playback, codec, jitter buffer, mixer, Echo Canceller, AGC, Noise Suppression, and High-Pass Filter." - }, - { - "id": "SysDes-105", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Secure Storage, Software: Diagnostics, Verification", - "verification": "Audit", - "statement": "Security verification shall audit secure storage, private-key handling, password handling, input validation, and diagnostic redaction on all target platforms." - }, - { - "id": "SysDes-106", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software: Storage, Deployment / Operations", - "verification": "Inspection, Demo", - "statement": "Deployment verification shall verify release package creation, signing, notarization, app-store builds, and release metadata wording." - }, - { - "id": "SysDes-107", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Verification", - "verification": "Test, Demo", - "statement": "MVP acceptance verification shall demonstrate all acceptance requirements from SysRS-241 through SysRS-257." - }, - { - "id": "SysDes-108", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Software Requirements Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The SysDes shall be the mandatory architectural allocation layer between SysRS and SRS; downstream SRS requirements shall use SysDes IDs as their direct source and shall not use SysRS IDs as their direct source." - }, - { - "id": "SysDes-109", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Software Requirements Engineering", - "verification": "Review, Inspection", - "statement": "The SysDes shall provide enough allocation information for SWE.1 to derive software requirements without directly reinterpreting SysRS requirements." - }, - { - "id": "SysDes-110", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Software Requirements Engineering, Change Control, Verification", - "verification": "Review, Inspection", - "statement": "If a downstream software requirement cannot be derived from an existing SysDes design item, the SysDes shall be revised before the SRS is revised or baselined." - }, - { - "id": "SysDes-111", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Design System", - "verification": "Review, Inspection", - "statement": "The system architecture shall adopt Material 3 as the baseline UI design system for the Flutter client and shall define Chanora-specific UI semantics above it." - }, - { - "id": "SysDes-112", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Design System, Flutter UI", - "verification": "Review, Inspection", - "statement": "The system architecture shall allocate Material 3 theming to a dedicated Design System element rather than to feature screens." - }, - { - "id": "SysDes-113", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Inspection, Test", - "statement": "The Design System element shall own `ThemeData`, `ColorScheme`, typography mapping, shape mapping, elevation mapping, motion mapping, and product semantic token mapping." - }, - { - "id": "SysDes-114", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Design System", - "verification": "Review, Inspection, Test", - "statement": "The system architecture shall define Chanora semantic tokens for connection state, voice state, latency state, channel state, diagnostics state, and accessibility state." - }, - { - "id": "SysDes-115", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Adaptive Shell", - "verification": "Test, Demo", - "statement": "The system architecture shall allocate compact, medium, and expanded layout selection to an Adaptive Shell element." - }, - { - "id": "SysDes-116", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Adaptive Shell, Voice UI", - "verification": "System Test, Demo", - "statement": "The Adaptive Shell shall keep connection status and primary voice controls visible or directly reachable across compact, medium, and expanded layouts." - }, - { - "id": "SysDes-117", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Design System, Verification", - "verification": "Accessibility Test, Review", - "statement": "The system architecture shall allocate accessibility semantics, non-color-only state expression, text scaling, and focus visibility to the Flutter UI and Design System elements." - }, - { - "id": "SysDes-118", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapter, Flutter Shell", - "verification": "Platform Test, Integration Test", - "statement": "The platform architecture shall allocate safe areas, display cutouts, system bars, virtual keyboards, desktop insets, Android back behavior, and iOS navigation behavior to Platform Adapter and Flutter Shell elements." - }, - { - "id": "SysDes-119", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Localization Service, Flutter UI, Diagnostics", - "verification": "Inspection, Test", - "statement": "The system architecture shall allocate user-visible string localization to a Localization Service used by Flutter UI and diagnostics presentation." - }, - { - "id": "SysDes-120", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Protocol Adapter, Rust Core, Flutter UI", - "verification": "Integration Test, System Test", - "statement": "The system architecture shall allocate server-provided names and messages to a Content Text path that is displayed without product localization or translation." - }, - { - "id": "SysDes-121", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI, Bridge, Rust Core, Storage, Diagnostics, Protocol Adapter", - "verification": "Inspection, Integration Test", - "statement": "The system architecture shall standardize UTF-8 as the internal text representation across Flutter, Rust Core, storage, diagnostics, and protocol-facing adapter DTOs." - }, - { - "id": "SysDes-126", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "System Engineering, Software Engineering, Verification", - "verification": "Review, Inspection", - "statement": "The system architecture shall enforce lifecycle traceability so that SysDes covers SysRS, SRS covers software-related SysDes, SAD covers SRS, and SDD covers SAD." - }, - { - "id": "SysDes-127", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software Engineering, System Engineering", - "verification": "Inspection", - "statement": "The system architecture shall prevent SRS requirements from linking directly to SysRS IDs; SRS shall use `Source SysDes` only." - }, - { - "id": "SysDes-128", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software Architecture, System Engineering", - "verification": "Inspection", - "statement": "The system architecture shall prevent SAD architecture items from linking directly to SysRS or SysDes IDs; SAD shall use `Source SRS` only." - }, - { - "id": "SysDes-129", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Software Design, System Engineering", - "verification": "Inspection", - "statement": "The system architecture shall prevent SDD detailed design items from linking directly to SysRS, SysDes, or SRS IDs; SDD shall use `Source SAD` only." - }, - { - "id": "SysDes-130", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Design System, Localization Service, Flutter UI", - "verification": "Accessibility Test, Inspection", - "statement": "The system architecture shall allocate localized accessibility labels for icon-only controls to the Design System and Localization Service." - }, - { - "id": "SysDes-131", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Localization Service", - "verification": "Test", - "statement": "The system architecture shall allocate language fallback behavior to the Localization Service and require deterministic fallback to the baseline product language." - }, - { - "id": "SysDes-132", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter App Shell, Design System, Localization Service", - "verification": "Integration Test", - "statement": "The system architecture shall allocate theme and localization initialization to application startup before feature screens render user-visible content." - }, - { - "id": "SysDes-133", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapter, Release / Operations, Verification", - "verification": "Review, Platform Test", - "statement": "The system architecture shall allocate iOS runtime minimum version policy to platform configuration, platform verification, release readiness, and iOS platform services." - }, - { - "id": "SysDes-134", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Release / Operations, iOS Build Configuration, Verification", - "verification": "Release Inspection", - "statement": "The system architecture shall allocate Apple App Store Connect upload SDK compliance to release engineering and release readiness rather than runtime feature logic." - }, - { - "id": "SysDes-135", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Adapter, Android Build Configuration, Release / Operations, Verification", - "verification": "Review, Platform Test, Release Inspection", - "statement": "The system architecture shall allocate Android minimum runtime version policy and Google Play target SDK compliance to Android platform configuration, platform verification, and release readiness." - }, - { - "id": "SysDes-136", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Rust Core, Flutter UI, State Sync, Audio Subsystem, Protocol Adapter", - "verification": "Review, System Test", - "statement": "The system architecture shall constrain MVP runtime state, UI, audio routing, and protocol orchestration to one active server connection per client instance." - }, - { - "id": "SysDes-137", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Audio Subsystem, Platform Audio, Settings UI", - "verification": "Audio Test, Review", - "statement": "The system architecture shall allocate AEC, AGC, Noise Suppression, and High-Pass Filter default enablement to the audio subsystem with platform capability detection and user-setting control." - }, - { - "id": "SysDes-139", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Storage, Platform Secure Storage, Security", - "verification": "Storage Test, Security Audit", - "statement": "The system architecture shall allocate non-secret local state to SQLite or an equivalent embedded database and secrets to platform secure storage." - }, - { - "id": "SysDes-140", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Bridge, Flutter State, Rust Core", - "verification": "Architecture Review, Integration Test", - "statement": "The system architecture shall allocate Flutter/Rust communication to a stable typed bridge with generated or schema-controlled DTOs." - }, - { - "id": "SysDes-141", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Privacy, Release / Operations, Security", - "verification": "Privacy Review, Security Audit", - "statement": "The system architecture shall exclude automatic diagnostic upload, telemetry upload, and automatic crash reporting from MVP runtime behavior unless a later approved decision updates privacy, security, legal, release, and verification baseli" - }, - { - "id": "SysDes-142", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Input, Rust Core, Flutter UI, Audio Engine", - "verification": "Architecture Review, Platform Test", - "statement": "The desktop Push-to-Talk subsystem shall be allocated to a dedicated **Platform PTT Backend** layer that is selected at runtime per operating system, display server, and granted permission set, with the **Rust Core PTT State Machine** ownin" - }, - { - "id": "SysDes-143", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Input, Rust Core, Flutter UI, Bridge", - "verification": "Integration Test, Architecture Review", - "statement": "The platform PTT backend layer shall report a typed `PttCapabilityLevel` (`L0` Focused, `L1` Global-shortcut-activation, `L2` Global-hold-to-talk, `L3` Global-with-mouse-buttons, `L4` Device-aware) to the Rust Core, and the Rust Core shall " - }, - { - "id": "SysDes-144", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Audio Engine, Rust Core", - "verification": "Audio Test, Integration Test", - "statement": "The audio capture pipeline shall expose two distinct atomic states: `capture_active` (the input stream is open and frames flow into the encoder feeder) and `transmit_active` (the encoder is producing outbound Opus frames and the protocol la" - }, - { - "id": "SysDes-145", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Platform Input", - "verification": "Platform Test (Win/macOS/Linux), Architecture Review", - "statement": "The Windows desktop backend shall be allocated to a Raw-Input-first implementation with a low-level-hook fallback and a Focused-PTT terminal fallback. The macOS desktop backend shall be allocated to a permission-aware Global PTT implementat" - }, - { - "id": "SysDes-146", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Diagnostics, Platform Input", - "verification": "Privacy Review, Diagnostic Inspection, Unit Test", - "statement": "A **diagnostics sanitizer** shall sit between the platform PTT backend and the application log sink. The sanitizer shall reject any record carrying raw key codes, scan codes, virtual-key values, or key-press timing sequences before the reco" - }, - { - "id": "SysDes-147", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Flutter UI", - "verification": "User Acceptance Test, UI Review", - "statement": "The Flutter Voice UI shall surface the active PTT capability level and the active backend identifier as user-facing status (capability badge, fallback notice) so the user understands when a Global PTT binding cannot be honoured and a Focuse" - }, - { - "id": "SysDes-148", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Release / Operations, Verification", - "verification": "Release Inspection, Architecture Review", - "statement": "The release verification record shall carry per-platform PTT capability evidence (detected `PttCapabilityLevel`, active backend identifier, fallback exercised yes/no) before any release artefact may claim Global PTT support. Release notes s" - }, - { - "id": "SysDes-149", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Audio Engine, Bridge, Flutter UI", - "verification": "Unit Test, UI Review", - "statement": "The system architecture shall define a `TransmitMode` element carried as an enum at the audio + bridge + UI boundary with variants `Ptt`, `Continuous`, and a reserved `VoiceActivity` placeholder that has no allocated implementation in this " - }, - { - "id": "SysDes-150", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Audio Engine, Bridge, Flutter UI", - "verification": "Integration Test, UI Review", - "statement": "The audio engine lifecycle shall be allocated to voice-channel membership: the input and output streams shall open on the user's first voice-channel join of the session and shall close on the last voice-channel leave. The output-stream open" - }, - { - "id": "SysDes-151", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Audio Engine", - "verification": "Unit Test, Integration Test", - "statement": "A release-tail element shall be allocated to the audio subsystem as a system-level timer adapter on the `transmit_active` atomic defined in SysDes-144, parameterised by a configurable release-tail period (default 200 ms; user-configurable r" - }, - { - "id": "SysDes-152", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Android Platform Adapter, Audio Subsystem (consumer)", - "verification": "Integration Test, System Test", - "statement": "The system architecture shall allocate engagement of the Android voice-communication audio mode (for example `AudioManager.setMode(MODE_IN_COMMUNICATION)` or an equivalent platform routing-assist mechanism) to the Android Platform Adapter a" - }, - { - "id": "SysDes-153", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Android Platform Adapter, Permission Subsystem, Audio Subsystem (consumer), Voice Subsystem (consumer), Flutter UI (re-request affordance)", - "verification": "Integration Test, System Test", - "statement": "The system architecture shall allocate Android runtime microphone permission (`RECORD_AUDIO`) acquisition and denial-handling to the Android Platform Adapter as the system-design partitioning between the Permission Subsystem and the Voice S" - }, - { - "id": "SysDes-154", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "Android Platform Adapter (owner), Audio Subsystem (consumer through trait), Platform Adapter Layer, Diagnostics (sharing-mode telemetry)", - "verification": "Integration Test, System Test, Audio Test", - "statement": "The system architecture shall allocate the Android voice audio backend as a dedicated subsystem owned by the Android Platform Adapter and exposed to the cross-platform Audio Subsystem (SE-13) through a trait-based backend abstraction, mirro" - }, - { - "id": "SysDes-155", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "macOS Build Configuration (owner), Platform Adapter Layer (macOS Platform Services), Release / Operations (Release Pipeline), Verification", - "verification": "Review, Platform Test (macOS), Release Inspection", - "statement": "The system architecture shall allocate the macOS runtime baseline as a dedicated platform-baseline element parallel to SysDes-133 (iOS) and SysDes-135 (Android), covering (a) the macOS deployment-target policy as a single-source-of-truth sy" - }, - { - "id": "SysDes-156", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "SE-13 (Audio Subsystem) — owns the realtime path being measured; the realtime callback, the Opus encode/decode paths, and the resampler are all SE-13 surfaces", - "verification": "Automated Benchmark Execution (criterion crate per SDD); Inspection of the JSON baseline records committed to the default branch", - "statement": "The system architecture shall allocate to SE-13 (Audio Subsystem) the obligation to expose its realtime capture and playback paths to benchmark instrumentation, such that the maintained numeric performance baselines authorized by SysRS-307 " - }, - { - "id": "SysDes-157", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "SE-18 (Deployment and Release Environment) — owns CI/CD per SysDes-036; the CI workflow definitions, the GitHub Actions runner selection, the PR status-check surface, the PR comment rendering, and the baseline-update workflow all live here", - "verification": "Demo (a CI run on a synthetic PR demonstrates the markdown table report, the visual markers, the non-blocking `success` status on a simulated regression, and the \"no baseline\" fallback on a metric with no prior baseline)", - "statement": "The system architecture shall allocate to SE-18 (Deployment and Release Environment) the integration of the realtime-audio benchmark suite (SysDes-156) into the existing CI workflow as an **advisory** regression-reporting surface, satisfyin" - }, - { - "id": "SysDes-158", - "source": "architecture/sysdes.md", - "stage": "P0 / MVP", - "allocation": "SE-18 (Deployment and Release Environment) — the comparison logic lives in CI tooling co-located with the workflow authored in SysDes-157", - "verification": "Review (the per-metric tolerance values are subject to periodic review; SDD ratifies the values as authored here and may refine the harness implementation; future SAD/SDD revisions may re-tune the values as baseline noise characteristics are observed empirically)", - "statement": "The system architecture shall allocate to SE-18 (Deployment and Release Environment) the per-metric tolerance window and the baseline-comparison methodology used by the advisory CI surface (SysDes-157), satisfying SysRS-309. The tolerance w" - } - ], - "P1": [ - { - "id": "SAD-035", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Storage, Platform Secure Storage", - "verification": "Security Test, Inspection", - "statement": "The architecture shall isolate local storage and secure storage behavior behind storage components." - }, - { - "id": "SAD-036", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics", - "verification": "Audit, Test", - "statement": "The architecture shall isolate diagnostics and redaction behavior in diagnostics components." - }, - { - "id": "SAD-037", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Build, Release, Operations", - "verification": "Inspection, Demo", - "statement": "The architecture shall isolate deployment, release, and packaging support in build and release assets." - }, - { - "id": "SAD-038", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Verification Support", - "verification": "Review, Inspection", - "statement": "The architecture shall provide verification hooks for requirements traceability and platform validation." - }, - { - "id": "SAD-039", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Flutter State", - "verification": "Unit Test, Review", - "statement": "The architecture shall use view models as the boundary between feature screens and application state." - }, - { - "id": "SAD-040", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Bridge Facade", - "verification": "Inspection, Integration Test", - "statement": "The architecture shall use a bridge facade as the only Flutter-to-Rust command boundary." - }, - { - "id": "SAD-041", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Bridge, Flutter State", - "verification": "Integration Test", - "statement": "The architecture shall map asynchronous Rust events into Flutter UI state updates." - }, - { - "id": "SAD-042", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Verification Support", - "verification": "Review, Test", - "statement": "The architecture shall define test seams for theme, localization, adaptive shell, platform services, and bridge facade." - }, - { - "id": "SAD-043", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Design System, Flutter App Shell", - "verification": "Widget Test", - "statement": "The architecture shall support light and dark theme mode behavior." - }, - { - "id": "SAD-044", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Design System", - "verification": "Widget Test", - "statement": "The architecture shall allow dynamic color as an optional enhancement without overriding Chanora semantic state tokens." - }, - { - "id": "SAD-051", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Architecture", - "verification": "Review, Inspection", - "statement": "The architecture shall include a SWE.2 evaluation checklist covering software elements, allocation, interfaces, dynamic behavior, resource objectives, feasibility, testability, traceability, consistency, and communication." - }, - { - "id": "SAD-053", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Architecture", - "verification": "Review, Analysis", - "statement": "The architecture shall include performance and resource views for UI responsiveness, audio latency, memory growth, reconnect, localization, and diagnostics." - }, - { - "id": "SAD-054", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Architecture", - "verification": "Review", - "statement": "The architecture shall document assumptions and dependencies that affect design feasibility." - }, - { - "id": "SAD-057", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Architecture, Software Design", - "verification": "Review, Inspection", - "statement": "The architecture shall explicitly support detailed design refinement of interface names, types, units, resolutions, ranges, and default values." - }, - { - "id": "SAD-058", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Architecture, Software Design", - "verification": "Review", - "statement": "The architecture shall define software units as the target decomposition level for SWE.3 detailed design." - }, - { - "id": "SAD-059", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Design, Verification", - "verification": "Review, Inspection", - "statement": "The architecture shall define that SDD detail must be sufficient to support implementation and unit verification." - }, - { - "id": "SAD-060", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Software Architecture, Software Design, Verification", - "verification": "Review", - "statement": "The architecture shall require SAD and SDD changes to be communicated to relevant affected parties before downstream implementation or verification updates are treated as baseline." - }, - { - "id": "SAD-066", - "source": "architecture/sad.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Platform Audio Adapter", - "verification": "Architecture Review, Audio Test", - "statement": "The software architecture shall hide platform-native and fallback audio processing implementations behind the audio subsystem interface." - }, - { - "id": "SDD-051", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`ArchitectureViewDocs` shall maintain context, container, component, runtime, deployment, data, security, diagnostics, UI, audio, protocol, and i18n views." - }, - { - "id": "SDD-052", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`ArchitectureViewDocs` shall maintain an ADR index for accepted, deprecated, and superseded architectural decisions." - }, - { - "id": "SDD-053", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`DeploymentConfig` shall define platform-specific packaging and release configuration units." - }, - { - "id": "SDD-054", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Unit Test, Audit", - "statement": "`DiagnosticsArchitecture` shall centralize logging, event keys, localized descriptions, redaction, and export presentation." - }, - { - "id": "SDD-055", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`ArchitectureTraceChecker` shall check that SDD items reference only SAD IDs." - }, - { - "id": "SDD-056", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`ArchitectureTraceChecker` shall check that SAD items reference only SRS IDs." - }, - { - "id": "SDD-057", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`ArchitectureViewDocs` shall define reviewable architecture views for context, containers, components, runtime behavior, deployment, data, security, diagnostics, UI, audio, protocol, and i18n." - }, - { - "id": "SDD-058", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`ArchitectureViewDocs` shall define an ADR record structure with ID, title, status, decision, rationale, consequences, affected units, and change history." - }, - { - "id": "SDD-059", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`InterfaceDetailCatalog` shall refine SAD architectural interfaces into unit-level interface entries." - }, - { - "id": "SDD-060", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`InterfaceDetailCatalog` shall define command, event, localization, Unicode, diagnostics, adaptive layout, audio meter, and platform-intent interfaces." - }, - { - "id": "SDD-061", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review, Analysis", - "statement": "`PerformanceDesignNotes` shall record UI responsiveness, audio latency, memory growth, reconnect, localization, and diagnostics resource considerations." - }, - { - "id": "SDD-062", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`AssumptionRegister` shall record software design assumptions and dependencies that affect feasibility." - }, - { - "id": "SDD-064", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review, Inspection", - "statement": "Each software unit detailed design shall include responsibility, inputs, outputs, interfaces, dynamic behavior, error behavior, resource behavior, and verification hooks where applicable." - }, - { - "id": "SDD-065", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`UnitConstructionGuideline` shall state that software units are constructed according to the SDD rather than reverse-documented from code." - }, - { - "id": "SDD-067", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`SoftwareUnitTraceChecker` shall support impact analysis from changed SAD items to affected SDD items." - }, - { - "id": "SDD-068", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`DetailedDesignReviewRecord` shall capture review evidence for SDD completeness, testability, and consistency with SAD." - }, - { - "id": "SDD-069", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Review", - "statement": "`UnitVerificationHandoff` shall identify verification method candidates for each software unit." - }, - { - "id": "SDD-070", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Inspection", - "statement": "`SoftwareUnitRegistry` shall maintain the list of software units, their owning component, source SAD items, and implementation status." - }, - { - "id": "SDD-076", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Unit Test, Audio Test", - "statement": "`AudioProcessingBackendSelector` shall select platform-native audio processing first where available and stable and isolate any fallback backend behind the audio subsystem interface." - }, - { - "id": "SDD-109", - "source": "architecture/sdd.md", - "stage": "P1 / Beta", - "allocation": "", - "verification": "Build Test, Release Inspection", - "statement": "`AndroidAaBuildPipeline` shall define the Android App Bundle (`.aab`) packaging pipeline used for release builds, covering bundletool configuration, ABI splits, signing assertions in CI, and version-code / version-name derivation." - }, - { - "id": "SRS-028", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Flutter UI, Diagnostics", - "verification": "UI Test, Security Audit", - "statement": "The software shall provide a diagnostics UI that allows user-initiated diagnostic export and displays user-safe diagnostic status." - }, - { - "id": "SRS-053", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Protocol Adapter, Bridge", - "verification": "Architecture Review", - "statement": "The Protocol Adapter shall support future patching, forking, or replacement of protocol implementation without changing Flutter UI contracts." - }, - { - "id": "SRS-061", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "State Sync, Diagnostics", - "verification": "Demo", - "statement": "The State Synchronization Engine shall provide replayable event recording hooks for debugging state synchronization." - }, - { - "id": "SRS-070", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Platform Adapters", - "verification": "Inspection", - "statement": "The Audio Subsystem shall provide an audio processing backend abstraction supporting platform-native and Rust-based implementations." - }, - { - "id": "SRS-075", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Flutter UI, Storage", - "verification": "Audio Test, UI Test", - "statement": "The Audio Subsystem shall support per-user volume during playback." - }, - { - "id": "SRS-082", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Platform Adapters", - "verification": "Platform Integration Test", - "statement": "The Audio Subsystem shall recover from audio route or device changes by reconfiguring audio streams or entering a user-safe blocked/degraded state." - }, - { - "id": "SRS-083", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Diagnostics", - "verification": "Demo", - "statement": "The Audio Subsystem shall provide audio loopback and processing test hooks for verification." - }, - { - "id": "SRS-085", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Storage", - "verification": "Unit Test, Integration Test", - "statement": "The software shall persist recent servers in local non-secret storage." - }, - { - "id": "SRS-087", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Storage, Flutter UI", - "verification": "Unit Test, Integration Test", - "statement": "The software shall persist UI settings in local non-secret storage." - }, - { - "id": "SRS-088", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Storage, Audio Subsystem", - "verification": "Unit Test", - "statement": "The software shall persist per-user volume and per-user mute preferences where applicable." - }, - { - "id": "SRS-096", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, Flutter UI", - "verification": "Demo, Security Audit", - "statement": "The software shall support user-initiated diagnostic export." - }, - { - "id": "SRS-097", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, Protocol Adapter, State Sync", - "verification": "Demo", - "statement": "The software shall provide protocol event recording in development or diagnostics mode." - }, - { - "id": "SRS-098", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, State Sync", - "verification": "Demo", - "statement": "The software shall provide event replay support for state synchronization debugging." - }, - { - "id": "SRS-099", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, Audio Subsystem, Platform Adapters", - "verification": "Demo", - "statement": "The software shall provide audio diagnostics for capture, processing backend, codec, jitter buffer, mixer, playback, and route/device status." - }, - { - "id": "SRS-100", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, Rust Core, Protocol Adapter", - "verification": "Demo", - "statement": "The software shall provide network diagnostics for connection status, reconnect attempts, latency where available, and protocol-relevant network failures." - }, - { - "id": "SRS-109", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Platform Adapters, Flutter UI", - "verification": "Platform Integration Test", - "statement": "The software shall provide a platform adapter interface for notification permission requests where required." - }, - { - "id": "SRS-112", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "Android Integration Test", - "statement": "The Android software shall handle audio focus and Bluetooth route changes where platform APIs expose them." - }, - { - "id": "SRS-113", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Platform Adapters, Audio Subsystem", - "verification": "Desktop Integration Test", - "statement": "The desktop software shall support validated audio backends for Windows, macOS, and Linux." - }, - { - "id": "SRS-114", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Platform Adapters, Rust Core, Audio Subsystem", - "verification": "Platform Integration Test", - "statement": "The software shall provide platform adapter lifecycle events for foreground/background, app resume, app suspend, audio interruption, and shutdown where supported." - }, - { - "id": "SRS-115", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Platform Adapters, Diagnostics, Flutter UI", - "verification": "Review, Demo", - "statement": "The software shall document unsupported or degraded platform behaviors through user-safe status and diagnostics." - }, - { - "id": "SRS-116", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Application Container, Deployment Scripts", - "verification": "Build Test", - "statement": "The software shall produce build artifacts for Windows installer packaging." - }, - { - "id": "SRS-118", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Application Container, Deployment Scripts", - "verification": "Build Test", - "statement": "The software shall produce Linux release packages using the selected supported package formats." - }, - { - "id": "SRS-119", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Application Container, Deployment Scripts", - "verification": "Build Test", - "statement": "The software shall produce Android AAB release builds." - }, - { - "id": "SRS-120", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Application Container, Deployment Scripts", - "verification": "Build Test", - "statement": "The software shall produce iOS TestFlight and App Store release builds when signing assets are available." - }, - { - "id": "SRS-122", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Application Container, Diagnostics, Deployment Scripts", - "verification": "Inspection, Security Review", - "statement": "The software shall provide release-build configuration that disables development-only diagnostic capture unless explicitly enabled for test builds." - }, - { - "id": "SRS-127", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Deployment Scripts, Application Container", - "verification": "Build Test, Release Audit", - "statement": "The software shall include release verification support for package creation, signing, notarization, app-store builds, and release metadata wording." - }, - { - "id": "SRS-138", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Platform Adapters, Audio Subsystem, Deployment Scripts", - "verification": "Review, Platform Test", - "statement": "The software team shall analyze operating-environment impacts of mobile lifecycle, foreground service behavior, AVAudioSession behavior, route changes, and app store policies." - }, - { - "id": "SRS-153", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Flutter App Shell", - "verification": "Widget Test, Integration Test", - "statement": "The medium layout shall support side navigation or equivalent navigation rail behavior where available display width permits." - }, - { - "id": "SRS-160", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Flutter UI", - "verification": "Widget Test, System Test", - "statement": "The software shall support visible focus indication and logical focus traversal for desktop and tablet keyboard use." - }, - { - "id": "SRS-163", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Flutter App Shell, Platform Adapter", - "verification": "Platform Test", - "statement": "The software shall model Android back navigation as a platform back intent handled by the shell or platform service layer." - }, - { - "id": "SRS-164", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Flutter App Shell, Platform Adapter", - "verification": "Platform Test", - "statement": "The software shall model iOS navigation gestures, safe areas, keyboard avoidance, and haptics through platform-aware UI services." - }, - { - "id": "SRS-169", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Localization Service, Flutter UI", - "verification": "Review, Inspection", - "statement": "The software shall allow additional locales to be added without changing protocol, audio, state synchronization, or storage modules." - }, - { - "id": "SRS-173", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Protocol Adapter, Platform Adapter, Bridge", - "verification": "Inspection, Integration Test", - "statement": "The software shall isolate non-UTF-8 or platform-specific string conversion in boundary adapters." - }, - { - "id": "SRS-176", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Localization Service, Diagnostics UI", - "verification": "Localization Test", - "statement": "The software shall use locale-aware formatting for user-visible dates, times, numbers, and diagnostic timestamps." - }, - { - "id": "SRS-177", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics", - "verification": "Inspection, Test", - "statement": "The software shall keep diagnostic event keys, codes, and machine-readable fields language-neutral." - }, - { - "id": "SRS-178", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, Localization Service", - "verification": "Inspection, Localization Test", - "statement": "The software shall allow user-facing diagnostic descriptions to be localized independently of machine-readable diagnostic fields." - }, - { - "id": "SRS-179", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "UX, Software Engineering", - "verification": "Review", - "statement": "The software shall maintain UI/UX guideline, design token, component catalog, adaptive layout, and platform behavior documents as downstream design baselines." - }, - { - "id": "SRS-191", - "source": "requirements/srs.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Platform Audio Adapter", - "verification": "Architecture Review, Audio Test", - "statement": "The software shall use platform-native audio processing first for MVP where available and stable, and shall isolate any Rust/WebRTC-style fallback behind the audio subsystem boundary." - }, - { - "id": "SysDes-122", - "source": "architecture/sysdes.md", - "stage": "P1 / Beta", - "allocation": "Protocol Adapter, Platform Adapter, Bridge", - "verification": "Inspection, Integration Test", - "statement": "The system architecture shall isolate non-UTF-8 or platform-specific string conversion at explicit boundary adapters." - }, - { - "id": "SysDes-123", - "source": "architecture/sysdes.md", - "stage": "P1 / Beta", - "allocation": "Flutter UI, Localization Service", - "verification": "Test", - "statement": "The system architecture shall allocate bidirectional text rendering and locale-aware formatting to Flutter UI and Localization Service components." - }, - { - "id": "SysDes-124", - "source": "architecture/sysdes.md", - "stage": "P1 / Beta", - "allocation": "Diagnostics, Localization Service", - "verification": "Inspection, Test", - "statement": "The diagnostics architecture shall keep machine-readable diagnostic keys language-neutral while allowing user-facing descriptions to be localized." - }, - { - "id": "SysDes-125", - "source": "architecture/sysdes.md", - "stage": "P1 / Beta", - "allocation": "System Engineering, UX, Software Team", - "verification": "Review", - "statement": "The system architecture shall define UI/UX guideline, design token, component catalog, adaptive layout, and platform behavior documents as downstream design baselines." - }, - { - "id": "SysDes-138", - "source": "architecture/sysdes.md", - "stage": "P1 / Beta", - "allocation": "Audio Subsystem, Platform Audio Adapter, Rust Core", - "verification": "Architecture Review, Audio Test", - "statement": "The system architecture shall prefer platform-native audio processing for MVP and isolate any Rust/WebRTC-style fallback behind the audio subsystem boundary." - } - ], - "P2": [ - { - "id": "SAD-045", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Software Design", - "verification": "Inspection", - "statement": "The architecture shall keep future SDD derivation independent from non-SAD requirement IDs." - }, - { - "id": "SAD-046", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Software Architecture", - "verification": "Review", - "statement": "The architecture shall include C4-style architecture views in the SAD baseline." - }, - { - "id": "SAD-047", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Software Architecture", - "verification": "Review", - "statement": "The architecture shall include ADRs for architecturally significant decisions." - }, - { - "id": "SAD-048", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Software Architecture", - "verification": "Review", - "statement": "The architecture shall identify software stakeholders and architecture concerns." - }, - { - "id": "SAD-049", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Release Architecture", - "verification": "Review, Inspection", - "statement": "The architecture shall include deployment and operational architecture views." - }, - { - "id": "SAD-050", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Diagnostics Architecture", - "verification": "Review, Test", - "statement": "The architecture shall include diagnostics and observability architecture views." - }, - { - "id": "SAD-055", - "source": "architecture/sad.md", - "stage": "P2 / Production", - "allocation": "Software Architecture", - "verification": "Review", - "statement": "The architecture shall include a glossary for architecture-specific terms." - }, - { - "id": "SDD-063", - "source": "architecture/sdd.md", - "stage": "P2 / Production", - "allocation": "", - "verification": "Review", - "statement": "`ArchitectureGlossary` shall provide definitions for architecture and detailed design terms." - }, - { - "id": "SRS-117", - "source": "requirements/srs.md", - "stage": "P2 / Production", - "allocation": "Application Container, Deployment Scripts", - "verification": "Build/Release Audit", - "statement": "The software shall produce signed and notarized macOS release builds for production release." - }, - { - "id": "SRS-154", - "source": "requirements/srs.md", - "stage": "P2 / Production", - "allocation": "Flutter App Shell", - "verification": "Integration Test, System Test", - "statement": "The expanded layout shall support persistent side panes and persistent voice controls where available display width permits." - }, - { - "id": "SRS-175", - "source": "requirements/srs.md", - "stage": "P2 / Production", - "allocation": "Flutter UI", - "verification": "Localization Test", - "statement": "The software shall support bidirectional text display where the Flutter platform text engine supports it." - } - ], - "SOP": [ - { - "id": "SysRS-001", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall be a cross-platform client application for channel-based voice communication." - }, - { - "id": "SysRS-002", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support Windows, macOS, Linux, iOS, and Android as target client platforms." - }, - { - "id": "SysRS-003", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall use Flutter for the user-facing client interface." - }, - { - "id": "SysRS-004", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall use Rust Core for protocol handling, connection management, state synchronization, audio processing, storage coordination, diagnostics, and business logic." - }, - { - "id": "SysRS-005", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall use `tsclientlib` as the TeamSpeak-compatible protocol client library." - }, - { - "id": "SysRS-006", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall operate as an independent client and shall not present itself as an official TeamSpeak product." - }, - { - "id": "SysRS-007", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow users to connect to TeamSpeak 3-compatible servers reachable from the client device network." - }, - { - "id": "SysRS-008", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow users to participate in channel-based voice communication when the external server and user permissions allow it." - }, - { - "id": "SysRS-009", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow users to send and receive supported text messages when the external server and user permissions allow it." - }, - { - "id": "SysRS-010", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide local bookmark, recent server, identity reference, UI setting, and audio setting management on the client device." - }, - { - "id": "SysRS-011", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support end users who manually connect to compatible voice servers." - }, - { - "id": "SysRS-012", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support regular users who rely on saved bookmarks, persistent identity, audio settings, and common voice controls." - }, - { - "id": "SysRS-013", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support testers and support operators who export diagnostics for troubleshooting." - }, - { - "id": "SysRS-014", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The SysRS shall provide requirements traceability suitable for engineering, QA, system administration, and release operations." - }, - { - "id": "SysRS-015", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The SysRS shall document external environment dependencies that may need administrator configuration, including network reachability and OS-level audio permissions." - }, - { - "id": "SysRS-016", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall treat external TeamSpeak 3-compatible servers as external systems outside Chanora control." - }, - { - "id": "SysRS-017", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not require a Chanora-operated central server for MVP voice, channel, or text operation." - }, - { - "id": "SysRS-018", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not host or operate TeamSpeak-compatible server functionality in the MVP." - }, - { - "id": "SysRS-019", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall rely on operating system services for microphone permission, secure storage, audio routing, notifications, and mobile lifecycle behavior." - }, - { - "id": "SysRS-020", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall treat app stores, package repositories, installers, and update services as deployment environment components." - }, - { - "id": "SysRS-021", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall treat physical microphones, speakers, wired headsets, USB audio devices, and Bluetooth audio devices as external hardware interfaces." - }, - { - "id": "SysRS-022", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall treat routers, firewalls, VPNs, NAT devices, and ISPs as external network environment components." - }, - { - "id": "SysRS-023", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall expose user-safe errors when external dependencies prevent successful operation." - }, - { - "id": "SysRS-024", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a Flutter UI component responsible for rendering screens, receiving user input, and presenting application state." - }, - { - "id": "SysRS-025", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a Flutter State Layer responsible for UI state derived from Rust Core events." - }, - { - "id": "SysRS-026", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a Bridge Layer responsible for commands, DTOs, asynchronous calls, and event streams between Flutter and Rust Core." - }, - { - "id": "SysRS-027", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a Rust Core component responsible for authoritative connection, protocol, state, audio, storage, settings, and diagnostics behavior." - }, - { - "id": "SysRS-028", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a protocol adapter component that isolates direct `tsclientlib` usage." - }, - { - "id": "SysRS-029", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a state synchronization component that implements snapshot + delta state handling." - }, - { - "id": "SysRS-030", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include an audio subsystem component that coordinates capture, processing, encoding, decoding, jitter buffering, mixing, and playback." - }, - { - "id": "SysRS-031", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a local storage component for non-secret data such as bookmarks, recent servers, UI settings, and audio settings." - }, - { - "id": "SysRS-032", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a secure storage adapter for private identities, passwords, and future sensitive tokens." - }, - { - "id": "SysRS-033", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include a diagnostics component for structured logging, redaction, event recording, event replay, audio diagnostics, network diagnostics, and user-initiated export." - }, - { - "id": "SysRS-034", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall include platform adapter components for desktop and mobile platform services." - }, - { - "id": "SysRS-035", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall use a CPU architecture supported by Flutter, Rust, and the selected target platform." - }, - { - "id": "SysRS-036", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall provide sufficient CPU capacity to run the Chanora application, Opus-compatible voice processing, and enabled audio processing features in real time." - }, - { - "id": "SysRS-037", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall provide sufficient memory for the Chanora application to run without unbounded growth in logs, chat history, audio buffers, or event queues." - }, - { - "id": "SysRS-038", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall provide persistent local storage for Chanora application data, settings, logs, and cached metadata." - }, - { - "id": "SysRS-039", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall provide a microphone or supported audio input device for voice transmission." - }, - { - "id": "SysRS-040", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall provide speakers, headphones, or a supported audio output device for voice playback." - }, - { - "id": "SysRS-041", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device shall provide network connectivity to the selected compatible voice server." - }, - { - "id": "SysRS-042", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The desktop client environment should be validated on devices with at least 4 GB RAM for MVP operation." - }, - { - "id": "SysRS-043", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The desktop client environment should be validated with at least 500 MB free local storage for installation, settings, logs, and diagnostic bundle creation." - }, - { - "id": "SysRS-044", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device should provide stable audio device identifiers where the target platform supports persistent device selection." - }, - { - "id": "SysRS-045", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device should provide hardware or OS support for low-latency audio capture and playback." - }, - { - "id": "SysRS-046", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client device should provide Bluetooth audio support where the target operating system supports it." - }, - { - "id": "SysRS-047", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall document platform-specific hardware limitations discovered during compatibility testing." - }, - { - "id": "SysRS-048", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Windows runtime environment shall support native desktop window integration for the Chanora application." - }, - { - "id": "SysRS-049", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Windows runtime environment shall provide microphone capture, speaker/headphone playback, audio device selection, and secure credential storage to the Chanora application." - }, - { - "id": "SysRS-050", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The macOS runtime environment shall support native desktop window integration for the Chanora application." - }, - { - "id": "SysRS-051", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The macOS runtime environment shall provide microphone permission prompts, microphone capture, speaker/headphone playback, audio device selection, and Keychain access to the Chanora application." - }, - { - "id": "SysRS-052", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Linux runtime environment shall support the desktop environments targeted by the selected release package." - }, - { - "id": "SysRS-053", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Linux runtime environment shall provide microphone capture, speaker/headphone playback, audio device selection, and secure storage through Secret Service, libsecret, or equivalent where available." - }, - { - "id": "SysRS-054", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The iOS runtime environment shall provide microphone permissions, foreground voice session capability, audio route change handling, audio interruption recovery, Keychain access, and AVAudioSession behavior to the Chanora application." - }, - { - "id": "SysRS-055", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Android runtime environment shall provide microphone permissions, foreground voice session capability, foreground service behavior, audio focus, Bluetooth route handling, Android Keystore access, and production audio integration through" - }, - { - "id": "SysRS-056", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall define minimum supported operating system versions for each target platform before beta release." - }, - { - "id": "SysRS-057", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall document OS-level permissions required by each target platform before public release." - }, - { - "id": "SysRS-058", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall fail safely with a user-safe message when a required OS service is unavailable." - }, - { - "id": "SysRS-059", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support microphone input for voice transmission." - }, - { - "id": "SysRS-060", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support speaker or headphone output for voice playback." - }, - { - "id": "SysRS-061", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support audio input and output device selection where the target platform exposes selectable devices." - }, - { - "id": "SysRS-062", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall handle audio route changes where the target platform reports them." - }, - { - "id": "SysRS-063", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall recover gracefully from audio device changes where possible." - }, - { - "id": "SysRS-064", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support Echo Canceller for reducing playback audio leaking into microphone input." - }, - { - "id": "SysRS-065", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support Automatic Gain Control for normalizing microphone input level." - }, - { - "id": "SysRS-066", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support Noise Suppression for reducing stationary background noise." - }, - { - "id": "SysRS-067", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support High-Pass Filter for reducing low-frequency rumble and handling noise." - }, - { - "id": "SysRS-068", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support an audio processing backend abstraction so platform-native and Rust-based processing can be selected per platform." - }, - { - "id": "SysRS-069", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow Echo Canceller to use playback reference audio when the selected implementation requires it." - }, - { - "id": "SysRS-070", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall expose audio processing settings to Rust Core and persist them locally." - }, - { - "id": "SysRS-071", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide conservative default settings for Echo Canceller, Automatic Gain Control, Noise Suppression, and High-Pass Filter." - }, - { - "id": "SysRS-072", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow platform-specific disabling of audio processing features when a feature is unstable or incompatible with a device configuration." - }, - { - "id": "SysRS-073", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall provide an audio loopback test tool for development and compatibility testing." - }, - { - "id": "SysRS-074", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall provide an audio processing test tool for development and compatibility testing." - }, - { - "id": "SysRS-075", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client network environment shall provide IP network connectivity from the client device to the selected compatible voice server." - }, - { - "id": "SysRS-076", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to configure the server host or IP address." - }, - { - "id": "SysRS-077", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to configure the server port." - }, - { - "id": "SysRS-078", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application should support a default TeamSpeak 3-compatible voice port when the user does not provide an explicit port." - }, - { - "id": "SysRS-079", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client network environment shall permit the protocol traffic required by `tsclientlib` and the selected compatible server." - }, - { - "id": "SysRS-080", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client network environment shall support latency suitable for real-time voice communication under expected operating conditions." - }, - { - "id": "SysRS-081", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The client network environment should minimize packet loss for acceptable voice quality." - }, - { - "id": "SysRS-082", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall detect recoverable network failures where possible." - }, - { - "id": "SysRS-083", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall enter reconnect behavior after recoverable network failures." - }, - { - "id": "SysRS-084", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall rebuild server state from a fresh snapshot after reconnect." - }, - { - "id": "SysRS-085", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not require VPN connectivity unless the target server or user environment requires it." - }, - { - "id": "SysRS-086", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall present network failures through user-safe error messages." - }, - { - "id": "SysRS-087", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application should include network diagnostics in user-initiated diagnostic exports." - }, - { - "id": "SysRS-088", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall be TeamSpeak 3-compatible for Chanora MVP operation." - }, - { - "id": "SysRS-089", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall be reachable from the client device network." - }, - { - "id": "SysRS-090", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall permit client connection using the identity, nickname, password, and permissions supplied by the user." - }, - { - "id": "SysRS-091", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall expose server information required for initial synchronization." - }, - { - "id": "SysRS-092", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall expose channel listing required for the channel tree." - }, - { - "id": "SysRS-093", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall expose client listing required for the online client view." - }, - { - "id": "SysRS-094", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall support channel join operations for accessible channels." - }, - { - "id": "SysRS-095", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall emit channel movement events required for state synchronization." - }, - { - "id": "SysRS-096", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall emit client join and leave events required for state synchronization." - }, - { - "id": "SysRS-097", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall emit channel create, update, and delete events required for state synchronization where those events occur." - }, - { - "id": "SysRS-098", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall support channel text message send and receive where user permissions allow." - }, - { - "id": "SysRS-099", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall support voice packet send and receive where user permissions allow." - }, - { - "id": "SysRS-100", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The external voice server shall expose disconnect behavior or connection failure signals that can be mapped by the protocol adapter." - }, - { - "id": "SysRS-101", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall document server-side permissions that can affect channel join, voice transmission, and text messaging." - }, - { - "id": "SysRS-102", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to manually connect to a compatible server." - }, - { - "id": "SysRS-103", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to provide hostname or IP address, port, nickname, and optional server password before connection." - }, - { - "id": "SysRS-104", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support persistent local identity for compatible server authentication." - }, - { - "id": "SysRS-105", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall display connection status to the user." - }, - { - "id": "SysRS-106", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to disconnect from the active server connection." - }, - { - "id": "SysRS-107", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall display the server channel tree after synchronization." - }, - { - "id": "SysRS-108", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall display online clients after synchronization." - }, - { - "id": "SysRS-109", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to join an accessible voice channel." - }, - { - "id": "SysRS-110", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow the user to send and receive channel text messages where permitted." - }, - { - "id": "SysRS-111", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall capture and transmit voice where permitted." - }, - { - "id": "SysRS-112", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall receive and play voice where permitted." - }, - { - "id": "SysRS-113", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide microphone mute control." - }, - { - "id": "SysRS-114", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide output deaf control." - }, - { - "id": "SysRS-115", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide push-to-talk control." - }, - { - "id": "SysRS-116", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall display microphone input level where available." - }, - { - "id": "SysRS-117", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall display speaking indicators where available." - }, - { - "id": "SysRS-118", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow users to save and reuse server bookmarks." - }, - { - "id": "SysRS-119", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow users to configure audio processing features." - }, - { - "id": "SysRS-120", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall allow users to export redacted diagnostics." - }, - { - "id": "SysRS-121", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall use `tsclientlib` inside the protocol subsystem." - }, - { - "id": "SysRS-122", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall isolate direct `tsclientlib` calls inside the `chanora_protocol` component." - }, - { - "id": "SysRS-123", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall prevent raw `tsclientlib` types from crossing into Flutter UI or Flutter State Layer." - }, - { - "id": "SysRS-124", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall convert `tsclientlib` errors into Chanora protocol errors." - }, - { - "id": "SysRS-125", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall convert `tsclientlib` events into internal protocol events." - }, - { - "id": "SysRS-126", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support future patching, replacement, or forking of protocol implementation without changing Flutter UI contracts." - }, - { - "id": "SysRS-127", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall provide protocol compatibility test coverage for MVP features." - }, - { - "id": "SysRS-128", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall include a protocol probe tool for validating target server compatibility." - }, - { - "id": "SysRS-129", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall maintain one authoritative connection state per active server connection." - }, - { - "id": "SysRS-130", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall synchronize state using a snapshot + delta model." - }, - { - "id": "SysRS-131", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall emit a full snapshot after initial synchronization." - }, - { - "id": "SysRS-132", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall emit delta events after live server-side changes." - }, - { - "id": "SysRS-133", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall apply protocol events through deterministic reducers." - }, - { - "id": "SysRS-134", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall preserve event ordering per connection." - }, - { - "id": "SysRS-135", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall rebuild state from a fresh snapshot after reconnect." - }, - { - "id": "SysRS-136", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall prevent Flutter from directly mutating server state." - }, - { - "id": "SysRS-137", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall implement the defined connection state machine from Disconnected through Connecting, Synchronizing, Connected, and Reconnecting." - }, - { - "id": "SysRS-138", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not automatically reconnect after user-triggered disconnect." - }, - { - "id": "SysRS-139", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall include an event replay tool for development and debugging of state synchronization." - }, - { - "id": "SysRS-140", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store server bookmarks locally." - }, - { - "id": "SysRS-141", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store recent servers locally." - }, - { - "id": "SysRS-142", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store audio settings locally." - }, - { - "id": "SysRS-143", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store UI settings locally." - }, - { - "id": "SysRS-144", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store per-user volume preferences locally where applicable." - }, - { - "id": "SysRS-145", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store muted user preferences locally where applicable." - }, - { - "id": "SysRS-146", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall use SQLite or an equivalent embedded database for non-secret local data." - }, - { - "id": "SysRS-147", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall use platform secure storage for sensitive data." - }, - { - "id": "SysRS-148", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store identity private keys using platform secure storage." - }, - { - "id": "SysRS-149", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store server passwords using platform secure storage." - }, - { - "id": "SysRS-150", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not store private keys in plaintext files." - }, - { - "id": "SysRS-151", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not write passwords to logs." - }, - { - "id": "SysRS-152", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall redact secrets from diagnostic exports." - }, - { - "id": "SysRS-153", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall store sensitive data using platform secure storage." - }, - { - "id": "SysRS-154", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall redact secrets from logs." - }, - { - "id": "SysRS-155", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall redact secrets from diagnostic bundles." - }, - { - "id": "SysRS-156", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid exposing internal stack traces to normal users." - }, - { - "id": "SysRS-157", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall validate user input before passing it to protocol operations." - }, - { - "id": "SysRS-158", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Windows runtime environment shall support Windows Credential Manager, DPAPI, or equivalent secure credential storage for the Chanora application." - }, - { - "id": "SysRS-159", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The macOS runtime environment shall support Keychain-based secure credential storage for the Chanora application." - }, - { - "id": "SysRS-160", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The iOS runtime environment shall support Keychain-based secure credential storage for the Chanora application." - }, - { - "id": "SysRS-161", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Android runtime environment shall support Android Keystore or equivalent secure credential storage for the Chanora application." - }, - { - "id": "SysRS-162", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Linux runtime environment shall support Secret Service, libsecret, or equivalent secure storage where available." - }, - { - "id": "SysRS-163", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall minimize collection of personal data." - }, - { - "id": "SysRS-164", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall require user action before exporting diagnostics." - }, - { - "id": "SysRS-165", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall explain microphone permission usage before requesting permission where platform guidelines allow." - }, - { - "id": "SysRS-166", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall explain notification permission usage before requesting permission where platform guidelines allow." - }, - { - "id": "SysRS-167", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not automatically upload diagnostics in MVP." - }, - { - "id": "SysRS-168", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall produce structured diagnostic logs." - }, - { - "id": "SysRS-169", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support log redaction." - }, - { - "id": "SysRS-170", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support protocol event recording in development or diagnostics mode." - }, - { - "id": "SysRS-171", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support event replay for debugging state synchronization." - }, - { - "id": "SysRS-172", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support audio diagnostics." - }, - { - "id": "SysRS-173", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support network diagnostics." - }, - { - "id": "SysRS-174", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall support user-initiated diagnostic export." - }, - { - "id": "SysRS-175", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "Diagnostic export shall exclude or redact sensitive data." - }, - { - "id": "SysRS-176", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall document the expected support workflow for diagnostic bundle collection." - }, - { - "id": "SysRS-177", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall include compatibility test tracking for supported platforms." - }, - { - "id": "SysRS-178", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall keep the UI responsive during connection, synchronization, and reconnect." - }, - { - "id": "SysRS-179", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid visible UI freezes longer than 100 ms during normal operation." - }, - { - "id": "SysRS-180", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall minimize local audio pipeline latency." - }, - { - "id": "SysRS-181", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application should target local audio pipeline latency under 100 ms where platform conditions permit." - }, - { - "id": "SysRS-182", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid unbounded memory growth in chat history." - }, - { - "id": "SysRS-183", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid unbounded memory growth in logs." - }, - { - "id": "SysRS-184", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid unbounded memory growth in audio buffers." - }, - { - "id": "SysRS-185", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid unbounded memory growth in event queues." - }, - { - "id": "SysRS-186", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall perform required audio processing without sustained underruns on supported devices." - }, - { - "id": "SysRS-187", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall keep reconnect processing non-blocking for UI interaction." - }, - { - "id": "SysRS-188", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall recover from transient network loss where possible." - }, - { - "id": "SysRS-189", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall recover gracefully from audio device changes where possible." - }, - { - "id": "SysRS-190", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall isolate connection failures to the affected connection." - }, - { - "id": "SysRS-191", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall avoid crashing on malformed or unexpected protocol events." - }, - { - "id": "SysRS-192", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall support Windows installer packaging for the Chanora application." - }, - { - "id": "SysRS-193", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall support macOS signed and notarized builds for the Chanora application." - }, - { - "id": "SysRS-194", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall support Linux packaging through AppImage, Flatpak, deb, rpm, or a selected subset." - }, - { - "id": "SysRS-195", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall support Android AAB release builds for the Chanora application." - }, - { - "id": "SysRS-196", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall support iOS TestFlight and App Store release builds for the Chanora application." - }, - { - "id": "SysRS-197", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall document platform signing, packaging, and release requirements before public release." - }, - { - "id": "SysRS-198", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall ensure release metadata does not imply official TeamSpeak affiliation." - }, - { - "id": "SysRS-199", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall define release channels for internal, beta, and production builds before external testing." - }, - { - "id": "SysRS-200", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a user interface for manual server connection." - }, - { - "id": "SysRS-201", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a user interface for bookmark management." - }, - { - "id": "SysRS-202", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a user interface for channel tree navigation." - }, - { - "id": "SysRS-203", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a user interface for chat." - }, - { - "id": "SysRS-204", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a user interface for voice controls." - }, - { - "id": "SysRS-205", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a user interface for audio processing settings." - }, - { - "id": "SysRS-206", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a bridge interface between Flutter and Rust Core." - }, - { - "id": "SysRS-207", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a protocol interface between Rust Core and `tsclientlib`." - }, - { - "id": "SysRS-208", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide an audio hardware interface through platform capture and playback adapters." - }, - { - "id": "SysRS-209", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide secure storage interfaces through platform-specific secure storage mechanisms." - }, - { - "id": "SysRS-210", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall provide a network interface to external TeamSpeak 3-compatible servers through `tsclientlib`." - }, - { - "id": "SysRS-211", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not directly expose raw `tsclientlib` types to Flutter." - }, - { - "id": "SysRS-212", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not store private keys in plaintext files." - }, - { - "id": "SysRS-213", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not write passwords to logs." - }, - { - "id": "SysRS-214", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not imply official TeamSpeak affiliation in UI, documentation, or release metadata." - }, - { - "id": "SysRS-215", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall not automatically upload diagnostic information in MVP." - }, - { - "id": "SysRS-216", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall comply with iOS background execution policies." - }, - { - "id": "SysRS-217", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application shall comply with Android foreground service requirements for active voice sessions." - }, - { - "id": "SysRS-218", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not require Chanora-operated backend infrastructure for voice, channels, or text." - }, - { - "id": "SysRS-219", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The selected `tsclientlib` version can support or be extended to support required MVP protocol features." - }, - { - "id": "SysRS-220", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "Required audio processing features can be implemented through a combination of platform-native APIs, Rust DSP, and external audio processing libraries." - }, - { - "id": "SysRS-221", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "Mobile foreground voice behavior is sufficient for MVP." - }, - { - "id": "SysRS-222", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "Background voice behavior will remain constrained by iOS and Android platform policies." - }, - { - "id": "SysRS-223", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "External compatible server administrators are responsible for server availability, permissions, and configuration." - }, - { - "id": "SysRS-224", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "End users are responsible for providing valid server connection details and network access." - }, - { - "id": "SysRS-225", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include TeamSpeak-compatible server hosting functionality." - }, - { - "id": "SysRS-226", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include full server administration functionality." - }, - { - "id": "SysRS-227", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include a complete permission editor." - }, - { - "id": "SysRS-228", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include a plugin system." - }, - { - "id": "SysRS-229", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include 3D positional audio unless explicitly reprioritized." - }, - { - "id": "SysRS-230", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include advanced whisper list management unless explicitly reprioritized." - }, - { - "id": "SysRS-231", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include Server Query administration tools." - }, - { - "id": "SysRS-232", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall not include automatic cloud sync of bookmarks, identities, settings, or diagnostics." - }, - { - "id": "SysRS-233", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall maintain a requirements traceability matrix from SysRS requirements through SysDes, SRS, SAD, SDD, and Verification evidence." - }, - { - "id": "SysRS-234", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify protocol compatibility through a protocol probe tool." - }, - { - "id": "SysRS-235", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify state synchronization through reducer tests and event replay tests." - }, - { - "id": "SysRS-236", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify audio capture, processing, encode/decode, and playback through audio loopback and processing tests." - }, - { - "id": "SysRS-237", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify secure storage behavior on every target platform before public release." - }, - { - "id": "SysRS-238", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify diagnostic redaction before enabling diagnostic export for external testers." - }, - { - "id": "SysRS-239", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify release packaging on every target platform before production release." - }, - { - "id": "SysRS-240", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The project shall verify that public wording does not imply official TeamSpeak affiliation." - }, - { - "id": "SysRS-241", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall connect to a TeamSpeak 3-compatible server using `tsclientlib`." - }, - { - "id": "SysRS-242", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall display the server channel tree." - }, - { - "id": "SysRS-243", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall display online clients." - }, - { - "id": "SysRS-244", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall allow the user to join a voice channel." - }, - { - "id": "SysRS-245", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall send voice." - }, - { - "id": "SysRS-246", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall receive voice." - }, - { - "id": "SysRS-247", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support microphone mute." - }, - { - "id": "SysRS-248", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support output deaf." - }, - { - "id": "SysRS-249", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support push-to-talk." - }, - { - "id": "SysRS-250", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support Echo Canceller." - }, - { - "id": "SysRS-251", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support Automatic Gain Control." - }, - { - "id": "SysRS-252", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support Noise Suppression." - }, - { - "id": "SysRS-253", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall support High-Pass Filter." - }, - { - "id": "SysRS-254", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall send and receive channel text messages." - }, - { - "id": "SysRS-255", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall save and reuse server bookmarks." - }, - { - "id": "SysRS-256", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall use secure storage for sensitive data." - }, - { - "id": "SysRS-257", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The MVP shall export redacted diagnostic logs." - }, - { - "id": "SysRS-258", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall use Material 3 as the baseline design system for the Flutter client user interface." - }, - { - "id": "SysRS-259", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall define a Chanora-specific design system above Material 3 for voice, connection, channel, latency, diagnostics, accessibility, and platform-adaptive states." - }, - { - "id": "SysRS-260", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support compact, medium, and expanded window classes for responsive client layout." - }, - { - "id": "SysRS-261", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall preserve visibility of connection status and primary voice controls across compact, medium, and expanded layouts." - }, - { - "id": "SysRS-262", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall provide screen-reader semantics for critical interactive controls and critical status indicators." - }, - { - "id": "SysRS-263", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall not communicate critical connection, voice, latency, permission, or error states by color alone." - }, - { - "id": "SysRS-264", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support keyboard focus traversal and visible focus indication on desktop-class and tablet keyboard environments." - }, - { - "id": "SysRS-265", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall keep critical controls reachable when the user increases text size using platform accessibility settings." - }, - { - "id": "SysRS-266", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall respect platform safe areas, display cutouts, system bars, virtual keyboards, and desktop window insets." - }, - { - "id": "SysRS-267", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall provide platform-appropriate handling for Android system back navigation, including predictive-back-compatible behavior where supported by the platform." - }, - { - "id": "SysRS-268", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall provide platform-appropriate handling for iOS navigation gestures, safe areas, keyboard avoidance, haptics, and system permission presentation." - }, - { - "id": "SysRS-269", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support localization of user-visible client application strings." - }, - { - "id": "SysRS-270", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall externalize user-visible strings from source code into localization resources or an equivalent localization mechanism." - }, - { - "id": "SysRS-271", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support English as the baseline product language." - }, - { - "id": "SysRS-272", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support adding additional product languages without changing protocol, audio, state synchronization, or storage architecture." - }, - { - "id": "SysRS-273", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall preserve and display Unicode server names, channel names, client nicknames, and text messages received from compatible servers." - }, - { - "id": "SysRS-274", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall use UTF-8 internally for text exchanged between Flutter, Rust Core, local storage, diagnostics, and protocol-facing adapters unless a platform API requires conversion at the boundary." - }, - { - "id": "SysRS-275", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall perform encoding conversion at explicit boundary adapters when external server, operating system, or platform APIs use a different text representation." - }, - { - "id": "SysRS-276", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall not corrupt or drop multilingual Unicode content in logs or diagnostic exports, except where redaction intentionally removes sensitive content." - }, - { - "id": "SysRS-277", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support bidirectional text display for user-visible localized strings and server-provided text where the platform text engine supports it." - }, - { - "id": "SysRS-278", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support locale-aware formatting for dates, times, numbers, and diagnostic timestamps where those values are user-visible." - }, - { - "id": "SysRS-279", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support fallback behavior when a translation key is missing." - }, - { - "id": "SysRS-280", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall keep machine-readable diagnostic fields stable and language-neutral while allowing user-facing diagnostic descriptions to be localized." - }, - { - "id": "SysRS-281", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall separate product localization from server-provided content; server-provided names and messages shall be displayed as content, not translated by the client." - }, - { - "id": "SysRS-282", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support accessibility labels and localization for icon-only controls." - }, - { - "id": "SysRS-283", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall define a UI/UX guideline document as a downstream non-normative design baseline derived from this SysRS and SysDes." - }, - { - "id": "SysRS-284", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall define design tokens for color, typography, spacing, shape, elevation, motion, density, connection state, voice state, latency state, and diagnostics state." - }, - { - "id": "SysRS-285", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall maintain traceability across the hierarchy `SysRS -> SysDes -> SRS -> SAD -> SDD` without allowing SRS, SAD, or SDD to bypass the immediately preceding lifecycle layer." - }, - { - "id": "SysRS-286", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support iOS runtime deployment on iOS 13 or later unless Flutter, plugin, audio, or platform constraints require raising the minimum version." - }, - { - "id": "SysRS-287", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "For Apple App Store Connect upload on or after 2026-04-28, the Chanora iOS/iPadOS build shall be produced with Xcode 26 or later using the iOS 26 / iPadOS 26 SDK or later, unless Apple publishes a newer applicable upload requirement before " - }, - { - "id": "SysRS-288", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora Android application shall support Android API 28 (Android 9.0) or later as the minimum runtime baseline, per DEC-004 (Accepted 2026-05-14, which raised the original API 24 recommendation to API 28). The minimum may be raised fur" - }, - { - "id": "SysRS-289", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "For Google Play submission, the Chanora Android build shall target the Android API level required by Google Play on the upload date." - }, - { - "id": "SysRS-290", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora MVP shall support one active server connection per client instance; multiple simultaneous active server connections shall be deferred outside MVP scope." - }, - { - "id": "SysRS-291", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora MVP shall enable Echo Canceller, Automatic Gain Control, Noise Suppression, and High-Pass Filter by default where supported and stable, while allowing user or platform policy to disable supported processing where applicable." - }, - { - "id": "SysRS-292", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall prefer platform-native audio processing for MVP where available and stable, with Rust/WebRTC-style audio processing retained as a controlled fallback or later architecture option." - }, - { - "id": "SysRS-293", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall use SQLite or an equivalent embedded local database for non-secret local state, while storing secrets only through platform secure storage." - }, - { - "id": "SysRS-294", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall use a stable typed Flutter/Rust bridge with generated or schema-controlled DTOs; `flutter_rust_bridge` is the default candidate unless prototype evidence selects a better option." - }, - { - "id": "SysRS-295", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora MVP shall not perform automatic diagnostic upload, automatic telemetry upload, or automatic crash reporting unless a later approved decision updates privacy, security, legal, release, and verification documents." - }, - { - "id": "SysRS-296", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora desktop application shall support **Focused Push-to-Talk** on Windows, macOS, and Linux. Focused PTT is the minimum required behaviour: the user shall be able to hold a bound input (keyboard key, mouse button) inside the focused" - }, - { - "id": "SysRS-297", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora desktop application shall additionally support **Global Push-to-Talk** where the operating system, the user-granted permission set, the display server, and the available input backend together permit it. Where global PTT is not " - }, - { - "id": "SysRS-298", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The detected desktop PTT capability level shall be exposed to the user interface and to the release verification record. The exposed value shall match the actual runtime capability — the application shall not advertise a Global PTT level wh" - }, - { - "id": "SysRS-299", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora Windows desktop application shall prefer the Raw Input backend for Global PTT, with a low-level keyboard hook used only as a fallback when Raw Input is unavailable, and Focused PTT used as the final fallback when no Global PTT b" - }, - { - "id": "SysRS-300", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora macOS desktop application shall request the operating-system permission required for Global PTT (Input Monitoring / Accessibility), use the permission-aware Global PTT backend when the permission is granted, and fall back to Foc" - }, - { - "id": "SysRS-301", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora Linux desktop application shall use a capability-dependent Global PTT backend selected from the available display server (X11 or Wayland) and compositor support (GNOME on Wayland is the officially-tested target for the first pub" - }, - { - "id": "SysRS-302", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall not log, store, persist, or include in the user-initiated diagnostic export any raw desktop key-event history, key code stream, or key-press timing sequence. Diagnostic export may include the detected PT" - }, - { - "id": "SysRS-303", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall define the v1 voice transmit mode set as `Ptt` and `Continuous`, with `VoiceActivity` reserved on the enum surface but unimplemented in this baseline (deferred per DEC-030). The default mode on a fresh i" - }, - { - "id": "SysRS-304", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall apply a configurable PTT release-tail interval between the moment the bound PTT input reports key-up and the moment the transmit gate closes, so that the trailing syllable of a spoken word is not clipped" - }, - { - "id": "SysRS-305", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "When the Chanora Android application has an active voice session connected, the system shall engage the Android in-call audio mode (for example via `AudioManager.setMode(MODE_IN_COMMUNICATION)` or an equivalent platform routing-assist mecha" - }, - { - "id": "SysRS-306", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora Android application shall acquire the Android runtime microphone permission (`RECORD_AUDIO`) at or before the point of voice session activation, and shall not begin microphone capture without that permission having been granted " - }, - { - "id": "SysRS-307", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora project shall maintain measured numeric performance baselines for the realtime audio capture and playback paths. The baseline set shall include, at minimum: (i) heap allocation count per realtime audio callback measured after wa" - }, - { - "id": "SysRS-308", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora project's continuous-integration workflow shall execute the realtime-audio benchmark suite on every pull request against the default branch and on every merge to the default branch, on at least one host architecture (Linux x86_6" - }, - { - "id": "SysRS-309", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The advisory CI regression comparison authorized by SysRS-308 shall use an explicitly declared tolerance window — a numeric percentage (or a numeric per-metric percentage set) above the maintained baseline (SysRS-307) — beyond which the adv" - }, - { - "id": "SysRS-310", - "source": "requirements/sysrs.md", - "stage": "", - "allocation": "", - "verification": "", - "statement": "The Chanora application system shall support macOS runtime deployment on macOS 10.15 (Catalina) or later as the minimum supported runtime, unless Flutter, plugin, audio, or platform constraints require raising the minimum version. This clau" - } - ] - } -} diff --git a/docs/privacy/privacy-policy.md b/docs/privacy/privacy-policy.md new file mode 100644 index 0000000..d84a49a --- /dev/null +++ b/docs/privacy/privacy-policy.md @@ -0,0 +1,51 @@ +# Chanora Privacy Policy Baseline + +**Document status:** Engineering baseline candidate for DV review +**Date:** 2026-05-29 +**Release status:** Requires product/legal approval before public or store release + +## 1. Summary + +Chanora is a client application for connecting to TeamSpeak 3-compatible servers selected by the user. Chanora does not operate the external servers users connect to and does not control server-side data handling. + +## 2. Data Stored Locally + +Chanora may store the following data on the user's device: + +| Data | Purpose | Storage expectation | +|---|---|---| +| Server bookmarks | Reconnect to user-selected servers | Local database or app storage | +| Recent connection details | Improve reconnection and user workflow where implemented | Local app storage | +| Identity references and sensitive credentials | Authenticate to compatible servers | Platform secure storage or documented fallback | +| Audio and UI settings | Preserve user preferences | Local app storage | +| Diagnostic logs | Troubleshooting when the user chooses to export diagnostics | Local diagnostic storage/export bundle | + +## 3. Permissions + +Chanora may request platform permissions needed for voice communication and app operation: + +| Permission or platform capability | Purpose | +|---|---| +| Microphone | Capture user voice for channel communication | +| Audio session / audio routing | Manage playback, capture, route changes, and voice processing | +| Notifications or foreground service where applicable | Maintain expected voice-session behavior on mobile platforms | +| Keyboard/input monitoring where applicable | Support push-to-talk on desktop platforms when allowed by the OS | +| Network access | Connect to user-selected compatible servers | + +## 4. Diagnostics + +Diagnostic export is user-initiated for MVP. Chanora must not automatically upload diagnostics, telemetry, or crash reports unless a later approved requirement and privacy update authorize that behavior. + +Diagnostic bundles may include logs, platform information, app version, connection state, and error details. They must redact secrets before export. Users choose whether to share exported diagnostics with support or developers. + +## 5. External Servers + +When a user connects to a TeamSpeak 3-compatible server, communication occurs with that external server. Server operators may process connection, voice, text, identity, permission, and logging information according to their own policies. Chanora does not control those external policies. + +## 6. No Automatic Cloud Sync + +The MVP does not include automatic cloud sync of bookmarks, identities, settings, or diagnostics. + +## 7. Release Approval Requirement + +This privacy baseline is sufficient for DV discussion. It must be reviewed and approved by the product/legal owner before public or store release. diff --git a/docs/references/aspice-swe2-swe3-integration-note.md b/docs/references/aspice-swe2-swe3-integration-note.md new file mode 100644 index 0000000..dbae734 --- /dev/null +++ b/docs/references/aspice-swe2-swe3-integration-note.md @@ -0,0 +1,28 @@ +# ASPICE SWE.2/SWE.3 Integration Note + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Purpose + +This note explains how Chanora's SWE.2 and SWE.3 documents support downstream verification. + +## 2. Lifecycle Chain + +```text +SysRS -> SysDes -> SRS -> SAD/SWE.2 -> SDD/SWE.3 -> SWE.4/SWE.5/SWE.6/SYS.4 verification +``` + +## 3. Integration Points + +| Process | Chanora document | Verification handoff | +|---|---|---| +| SWE.2 | `docs/architecture/sad.md` | Defines components, interfaces, runtime flows, dependency rules | +| SWE.3 | `docs/architecture/sdd.md` | Defines modules, detailed behavior, data/state design, verification hooks | +| SWE.4 | `docs/verification/swe4-unit-verification-plan.md` | Consumes SDD module hooks | +| SWE.5 | `docs/verification/swe5-software-integration-verification-plan.md` | Consumes SAD interfaces and SDD integration hooks | +| SWE.6 | `docs/verification/swe6-software-verification-plan.md` | Verifies integrated software against SRS | + +## 4. DV Conclusion + +The SWE.2/SWE.3 integration route is documented for DV. Further itemized IDs can be added if required by a stricter process audit. diff --git a/docs/references/external-references.md b/docs/references/external-references.md new file mode 100644 index 0000000..54c313c --- /dev/null +++ b/docs/references/external-references.md @@ -0,0 +1,21 @@ +# Chanora External References + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Reference List + +| Reference | Use | +|---|---| +| Flutter documentation | Cross-platform UI, build, localization, platform integration | +| Flutter Rust Bridge documentation | Typed Flutter/Rust bridge generation | +| Rust Cargo documentation | Workspace builds, tests, dependency locking | +| TeamSpeak 3-compatible protocol/library documentation | Protocol compatibility through `tsclientlib` | +| Apple Developer documentation | iOS/macOS signing, AVAudioSession, TestFlight/App Store requirements | +| Android Developer documentation | Permissions, foreground services, target SDK, audio routing | +| Material 3 guidelines | UI system baseline | +| ASPICE SWE/SYS process references | Lifecycle terminology for SWE.2, SWE.3, SWE.4, SWE.5, SWE.6, SYS.4 | + +## 2. DV Use + +External references support review but do not override Chanora baselines. diff --git a/docs/release/dv-waiver-register.md b/docs/release/dv-waiver-register.md new file mode 100644 index 0000000..adb94d8 --- /dev/null +++ b/docs/release/dv-waiver-register.md @@ -0,0 +1,35 @@ +# Chanora DV Waiver Register + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Applies to:** DV review and internal candidate validation only + +## 1. Waiver Policy + +A waiver records a known gap that reviewers may accept for a limited decision scope. A waiver does not delete the underlying requirement and does not approve public/store release unless the release owner explicitly accepts that scope. + +## 2. Active Waivers and Deferrals + +| ID | Gap | Source evidence | Impact | Accepted DV scope | Unblock condition | +|---|---|---|---|---|---| +| DV-WVR-001 | DEC-012 legal/trademark/OSS review remains open | `docs/implementation-status-2026-05-28.md` | Blocks public/store release and final compatible-server wording approval | Documentation review and internal validation only | Legal/trademark/OSS owner signs off and required notices/reports are complete | +| DV-WVR-002 | Android Keystore-backed DEK deferred to v1.1 | `docs/implementation-status-2026-05-28.md` | Limits Android secure-storage claim; file-fallback DEK remains a release risk | Internal validation only with limitation stated | Android Keystore-backed DEK implemented or product/security owner accepts release waiver | +| DV-WVR-003 | iOS `AVAudioSession.Mode.voiceChat` status was recorded as a prior blocker but recent commit history indicates follow-up implementation | `docs/implementation-status-2026-05-28.md`, recent commit `feat(ios): use AVAudioSession .voiceChat mode` | Requires updated validation so documents and implementation status do not conflict | DV may proceed if treated as evidence-needs-validation, not as a closed release gate | Run iOS device audio verification and update implementation status | +| DV-WVR-004 | Standalone event replay and runtime reducer-integration evidence not yet complete | `docs/implementation-status-2026-05-28.md`, local `cargo test -p chanora_state --locked` evidence | Reducer unit coverage supports state synchronization, but file-based replay evidence and live-event integration evidence remain open | DV documentation pass and internal validation | Event replay tooling implemented or requirement reprioritized; runtime reducer integration evidence attached | +| DV-WVR-005 | Event replay tool not found | `docs/implementation-status-2026-05-28.md` | Limits P1 state verification hooks SRS-061/SRS-098 | Accepted as P1 deferral | Event replay tooling implemented or requirement reprioritized | +| DV-WVR-006 | Desktop and iOS artifacts are source-buildable or unsigned only | `docs/implementation-status-2026-05-28.md`, `docs/release/ios-build.md` | Blocks packaged public release claims | Internal validation from source/unsigned builds only | Signed/notarized/package artifacts exist and hashes are recorded | +| DV-WVR-007 | Silero VAD asset bundled while `VoiceActivity` is deferred | `docs/implementation-status-2026-05-28.md` | Risk that UI/release wording overstates VAD availability | DV may pass if VoiceActivity remains disabled/coming-soon | VAD implementation allocated in a later baseline or asset/wording reconciled | +| DV-WVR-008 | Artifact hashes, tag, and candidate run IDs are not recorded in release record | `docs/release/release-readiness-go-nogo-record.md` | Blocks final release approval and reproducibility | DV documentation review only | Candidate build run records, tag, commit SHA, and artifact hashes are recorded | + +## 3. Waiver Review Rules + +| Rule | Required behavior | +|---|---| +| Scope control | Waivers in this file apply to DV/internal validation unless explicitly promoted by release owner approval | +| User-visible claims | Release notes, UI, and marketing wording must not claim unsupported capabilities | +| Security/legal claims | Security, privacy, legal, and OSS claims require owner sign-off before public release | +| Closure | A waiver closes only when the unblock condition is satisfied and the release-readiness record is updated | + +## 4. DV Meeting Recommendation + +Accept the waiver register for DV documentation pass. Do not accept these waivers as public/store release approval. diff --git a/docs/release/linux-build.md b/docs/release/linux-build.md deleted file mode 100644 index 062e7a5..0000000 --- a/docs/release/linux-build.md +++ /dev/null @@ -1,102 +0,0 @@ -# Chanora Linux build notes - -This document covers the current Linux desktop build and packaging flow for -`apps/chanora_flutter`. - -## Prerequisites - -- Linux host with desktop Flutter support enabled -- Rust stable -- Flutter stable on `PATH` -- CMake 3.13+ -- `pkg-config` -- GTK3 development headers -- Opus development headers that expose the `opus` pkg-config module used by - tsclientlib/audiopus for Linux audio encode/decode -- PipeWire development headers that expose the `libpipewire-0.3` and - `libspa-0.2` pkg-config modules used by the primary Linux voice backend -- PulseAudio development headers for the fallback Linux voice backend -- `clang` and `gcc` -- sibling checkout of `oboe-rs` at `../oboe-rs` - -Install `opus-devel` on Fedora-style hosts or `libopus-dev` on Debian/Ubuntu. -The workspace does not vendor or patch Opus for Linux; the normal Linux build -links the system `libopus` through `pkg-config`. - -For Linux audio specifically, the product uses PipeWire directly as the primary -backend and PulseAudio directly as the fallback. If you want a fast preflight, -these are the package checks that matter: - -```bash -pkg-config --modversion gtk+-3.0 -pkg-config --modversion opus -pkg-config --modversion libpipewire-0.3 -pkg-config --modversion libspa-0.2 -pkg-config --modversion libpulse -pkg-config --modversion libpulse-simple -``` - -Use `libpipewire-0.3`, not `pipewire-0.3`. The latter probe will fail even on a -correctly provisioned host because it is not the module name exported by the -PipeWire development package. - -## Bundled ONNX Runtime - -Linux release builds include ONNX Runtime for Silero VAD by default. The build -helper downloads the official Linux CPU archive into `.cache/onnxruntime/` for -the current target architecture and stages `libonnxruntime.so` into the app -bundle. - -To pin a specific ONNX Runtime release, export: - -```bash -export CHANORA_ONNXRUNTIME_VERSION=1.26.0 -``` - -To use a local shared library instead of downloading the archive, export: - -```bash -export CHANORA_ONNXRUNTIME_SHARED_LIB=/absolute/path/to/libonnxruntime.so -``` - -When set, the Flutter Linux bundle will include: - -```text -build/linux/x64//bundle/lib/libonnxruntime.so -``` - -and the Linux runner will set `ORT_DYLIB_PATH` to that bundled file at app -startup when the variable is not already present. - -## Build helper - -From the repository root: - -```bash -./tools/build-linux.sh -``` - -The Flutter Linux CMake bundle now owns staging `libchanora_bridge.so` into -`bundle/lib/`. A plain `flutter build linux` will build the host-profile Rust -bridge automatically; `tools/build-linux.sh` exports -`CHANORA_BRIDGE_SHARED_LIB` so release bundles use the already-built targeted -artifact instead of duplicating the copy logic in shell. - -Useful variants: - -```bash -./tools/build-linux.sh --no-rust -./tools/build-linux.sh --regenerate-bindings -./tools/build-linux.sh --version v0.2.0-beta.1 -``` - -## Debian package helper - -To build a `.deb` package: - -```bash -./tools/build-linux-deb.sh -``` - -The Debian helper verifies that `libonnxruntime.so` was actually bundled before -packaging. diff --git a/docs/release/platform-release-policy.md b/docs/release/platform-release-policy.md new file mode 100644 index 0000000..a25290d --- /dev/null +++ b/docs/release/platform-release-policy.md @@ -0,0 +1,26 @@ +# Chanora Platform Release Policy + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Purpose + +This policy defines platform release expectations for Chanora candidate builds. + +## 2. Platform Policy + +| Platform | Minimum release requirement | Current DV state | +|---|---|---| +| Android | Signed release or internal test artifact, target SDK compliance, permission/foreground-service validation | Internal validation only; Android DEK waiver active | +| iOS | Signed TestFlight/App Store build or explicitly unsigned verification build | Unsigned verification build only | +| Windows | Packaged build, smoke result, PTT capability evidence, signing decision | Source-buildable only | +| macOS | Universal build, signing/notarization, PTT capability evidence | Source-buildable only | +| Linux | Packaged build or documented source-build path, portal/fallback PTT evidence | Source-buildable only | + +## 3. Release Claim Rule + +Release notes and public wording must match the actual artifact and platform capability. A source-buildable platform must not be described as having a finished public binary release. + +## 4. DV Conclusion + +The platform policy is defined. Current candidate scope remains internal validation and documentation review. diff --git a/docs/release/release-readiness-go-nogo-record.md b/docs/release/release-readiness-go-nogo-record.md new file mode 100644 index 0000000..605316c --- /dev/null +++ b/docs/release/release-readiness-go-nogo-record.md @@ -0,0 +1,102 @@ +# Chanora Release Readiness Go/No-Go Record + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Candidate:** `v1.0.0-rc.1` evidence over workspace version `0.2.0-beta.1` +**Decision:** No-Go for public/store release; Conditional Go only for documentation review and continued internal DV validation + +## 1. Decision Summary + +Chanora has enough documented structure for DV review and continued internal release-candidate validation. It is not ready for public, store, or broad external release because legal/trademark/OSS sign-off, platform release artifacts, signing/notarization, and several verification evidence items remain open. + +The phrase `Conditional Go` in this record is restricted to document-baseline review and internal validation planning. It is not a product release approval, not a public beta approval, and not a store-distribution approval. + +| Decision scope | Result | Rationale | +|---|---|---| +| Documentation baseline for DV meeting | Conditional Go | SysRS, SysDes, SRS, verification plans, traceability summary, and waiver register are available | +| Internal candidate validation | Conditional Go | Core product paths are implemented enough to continue targeted validation with recorded limitations | +| Public release | No-Go | DEC-012 and platform release gates remain open | +| Store release | No-Go | Signing, app-store build, legal/privacy/security evidence, and artifact records are incomplete | + +## 2. Candidate Metadata + +| Field | Value | +|---|---| +| Workspace version | `0.2.0-beta.1` | +| CHANGELOG latest candidate | `v1.0.0-rc.1` | +| Build number | `76` from `apps/chanora_flutter/pubspec.yaml` | +| Commit SHA | To be recorded from the candidate build job before release approval | +| Git tag | To be recorded if `v1.0.0-rc.1` is tagged for release validation | +| Artifact hashes | Not recorded in current workspace; required before release approval | +| Release owner | Product / Release Operations | +| Verification owner | Software QA with System Engineering support | + +## 3. Current Implementation Readiness + +Current implementation status is summarized in `docs/implementation-status-2026-05-28.md`. + +| Area | Readiness statement | +|---|---| +| Core product path | Connect, channel/voice/chat/bookmark/storage/diagnostics paths are substantially implemented | +| CI baseline | Rust and Flutter automated checks are defined in `.github/workflows/ci.yml` | +| Audio benchmarks | Advisory workflow exists in `.github/workflows/bench-advisory.yml` | +| Platform coverage | Android and iOS platform work exists; desktop/iOS release artifacts are not ready for public distribution | +| Documentation | Verification plan set and DV gate summaries now exist for review | + +## 4. Release Gates + +| Gate | Status | Decision impact | +|---|---|---| +| Requirements/design baseline | Passed for DV | SysRS/SysDes/SRS available | +| Verification plan baseline | Passed for DV | `docs/verification/` plan set available | +| Traceability summary | Passed for DV | `docs/governance/traceability-matrix.md` available | +| Legal/trademark/OSS review DEC-012 | Blocked / open | Blocks public/store release | +| Privacy policy baseline | Baseline candidate | Requires owner/legal review before public/store release | +| Security/privacy evidence | Partial | Blocks strong secure-storage and diagnostic claims until audits attach evidence | +| Android secure-storage DEK | Deferred to v1.1 | Requires waiver for internal testing; limits release claim | +| iOS release build/signing | Unsigned verification only | Blocks TestFlight/App Store release | +| macOS signing/notarization | Not complete | Blocks macOS public binary release | +| Windows/Linux packaging | Source-buildable only for candidate | Blocks packaged public desktop release claims | +| Artifact hashes | Not recorded | Blocks final release approval | + +## 5. Verification Evidence Status + +| Evidence | Current status | Required action before public/store release | +|---|---|---| +| Rust workspace check/test | CI defined | Attach latest passing candidate run | +| Flutter analyze/test | CI defined | Attach latest passing candidate run | +| Cargo deny and cargo-about | CI defined | Attach latest passing candidate run and inventory records | +| Flutter license inventory | CI defined | Attach latest passing candidate run and inventory records | +| iOS unsigned build | CI defined | Attach latest passing candidate run; add signing evidence before release | +| Compatible-server demo | Evidence not attached in this record | Run and attach demo notes/logs | +| Audio send/receive and processing demo | Evidence not attached in this record | Run and attach platform evidence | +| Diagnostics redaction/export demo | Evidence not attached in this record | Run and attach export review | +| Platform secure-storage audit | Partial | Attach per-platform audit or waiver | +| PTT capability evidence | Partial | Attach per-platform `PttCapabilityLevel` and backend record | + +## 6. Platform Readiness + +| Platform | Current readiness | Release decision | +|---|---|---| +| Android | Core platform implementation present; Android Keystore-backed DEK deferred | Conditional internal validation only | +| iOS | Unsigned build path present; signing and store pipeline incomplete | No-Go for store release | +| Windows | Source-buildable; smoke procedure exists | No-Go for packaged release until smoke/signing evidence exists | +| macOS | Source-buildable; public artifact not in candidate | No-Go for packaged release until signing/notarization evidence exists | +| Linux | Source-buildable; packaging not confirmed | No-Go for packaged release until artifact evidence exists | + +## 7. Waivers and Deferrals + +All current waivers and deferrals are controlled by `docs/release/dv-waiver-register.md`. A release approver may accept a waiver for internal validation, but public/store release waivers require explicit product, legal/security, and release-owner approval where applicable. + +## 8. Final DV Recommendation + +Recommended DV meeting outcome: + +| Question | Recommendation | +|---|---| +| Can the document baseline pass DV review? | Yes, with recorded limitations | +| Can internal release-candidate validation continue? | Yes, with the waiver register attached | +| Can Chanora be publicly released now? | No | +| Can store release proceed now? | No | + +The release decision remains **No-Go** until current candidate run evidence, legal/trademark/OSS approval, privacy/security approval, platform signing, and artifact records are complete. diff --git a/docs/requirements/srs.md b/docs/requirements/srs.md new file mode 100644 index 0000000..994ec81 --- /dev/null +++ b/docs/requirements/srs.md @@ -0,0 +1,22 @@ +# Software Requirements Specification + +**Document status:** DV entry-point record +**Canonical document:** `../srs.md` + +The canonical Chanora Software Requirements Specification currently lives at `docs/srs.md`. This file preserves the README-advertised path `docs/requirements/srs.md` for DV navigation. + +Reviewers shall use `docs/srs.md` as the authoritative SRS baseline until the repository migration moves the canonical file into this directory. + +## DV Review Summary + +| Topic | Canonical source | +|---|---| +| Software requirement attributes | `docs/srs.md` section 2 | +| Component requirement groups | `docs/srs.md` section 5 | +| MVP acceptance verification support | `docs/srs.md` SRS-128 | +| SysDes-to-SRS coverage | `docs/srs.md` section 9 | +| SWE lifecycle handoff | `docs/srs.md` section 8 | + +## DV Position + +The SRS baseline is reviewable for DV. SAD, SDD, and verification documents consume this baseline through `docs/architecture/sad.md`, `docs/architecture/sdd.md`, and `docs/verification/`. diff --git a/docs/requirements/sysrs.md b/docs/requirements/sysrs.md new file mode 100644 index 0000000..38836f2 --- /dev/null +++ b/docs/requirements/sysrs.md @@ -0,0 +1,22 @@ +# System Requirements Specification + +**Document status:** DV entry-point record +**Canonical document:** `../sysrs.md` + +The canonical Chanora System Requirements Specification currently lives at `docs/sysrs.md`. This file preserves the README-advertised path `docs/requirements/sysrs.md` for DV navigation. + +Reviewers shall use `docs/sysrs.md` as the authoritative SysRS baseline until the repository migration moves the canonical file into this directory. + +## DV Review Summary + +| Topic | Canonical source | +|---|---| +| System scope and context | `docs/sysrs.md` sections 2 through 5 | +| Verification and validation requirements | `docs/sysrs.md` section 24 | +| MVP acceptance requirements | `docs/sysrs.md` section 25, SysRS-241 through SysRS-257 | +| Traceability requirement | `docs/sysrs.md` SysRS-233 and SysRS-285 | +| No automatic telemetry posture | `docs/sysrs.md` SysRS-295 | + +## DV Position + +The SysRS baseline is reviewable for DV. Release approval remains controlled by `docs/release/release-readiness-go-nogo-record.md`. diff --git a/docs/security/dependency-and-supply-chain-report.md b/docs/security/dependency-and-supply-chain-report.md new file mode 100644 index 0000000..a11313c --- /dev/null +++ b/docs/security/dependency-and-supply-chain-report.md @@ -0,0 +1,43 @@ +# Chanora Dependency and Supply Chain Report + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Scope:** Dependency, license, advisory, and supply-chain evidence for DV review + +## 1. Purpose + +This report summarizes the current dependency and supply-chain controls visible in the repository. It is not the final DEC-012 legal sign-off. + +## 2. Automated Controls + +| Control | Location | Current behavior | +|---|---|---| +| Rust license/advisory/bans/sources check | `.github/workflows/ci.yml` supply-chain job | Runs `cargo deny check --workspace --all-features` | +| Rust license inventory freshness | `.github/workflows/ci.yml` license-inventory job | Regenerates cargo-about inventory and diffs `docs/security/license-inventory.md` | +| Flutter license inventory freshness | `.github/workflows/ci.yml` flutter-license-inventory job | Runs `tools/dump_flutter_licenses.sh` and fails if `docs/security/flutter-license-inventory.md` is stale | +| Rust dependency lock enforcement | `.github/workflows/ci.yml` Rust job | Uses `cargo check/test --locked` | +| Flutter dependency resolution | `.github/workflows/ci.yml` Flutter job | Runs `flutter pub get`, analyze, and tests | + +## 3. Dependency Areas + +| Area | Examples from current repository | DV status | +|---|---|---| +| Rust workspace crates | `chanora_protocol`, `chanora_audio`, `chanora_storage`, `chanora_diagnostics`, `chanora_bridge`, `chanora_resolver`, `chanora_server_prefetch`, `chanora_state`, `chanora_core` | In workspace and covered by CI commands | +| Flutter app dependencies | `flutter_rust_bridge`, `audio_session`, `flutter_foreground_task`, `package_info_plus`, `share_plus`, `shared_preferences` | Covered by Flutter dependency resolution and license inventory job | +| Native/audio dependencies | Opus, platform audio stacks, Android/iOS audio services | Require platform build and license review evidence | +| Patched Rust dependency | `cmake` patched to a pinned git revision for Android build support | Requires DEC-032 tracking and periodic reevaluation | +| Pinned Android audio fork | `oboe` uses `https://github.com/EdisonJwa/oboe-rs` at a fixed revision | Removes machine-local path dependency while preserving reviewed Android callback/session fixes | + +## 4. Open Evidence Gaps + +| Gap | Impact | Required action | +|---|---|---| +| `docs/security/license-inventory.md` and Flutter inventory are referenced by CI but were not present in the current document listing | CI may fail or the repository may have untracked/missing inventory artifacts | Generate and commit inventories or update CI/documentation to the actual artifact location | +| DEC-012 remains open | Blocks public/store release | Complete legal/trademark/OSS review | +| Candidate CI run IDs are not recorded in release readiness | Reviewers cannot tie evidence to a specific release candidate | Attach latest passing run IDs to release-readiness record | +| Patched `cmake` dependency requires monitoring | Long-term supply-chain risk if fork remains pinned indefinitely | Reevaluate when upstream release includes the needed Android fix | +| `audiopus_sys` unmaintained advisory `RUSTSEC-2026-0150` is explicitly ignored in `deny.toml` | Keeps current Opus path buildable but remains a supply-chain risk | Track replacement or upstream remediation before public release sign-off | + +## 5. DV Conclusion + +Automated supply-chain controls are defined, but final dependency/legal approval is not complete. This supports DV documentation pass with a public-release blocker. diff --git a/docs/security/diagnostic-redaction-audit-report.md b/docs/security/diagnostic-redaction-audit-report.md new file mode 100644 index 0000000..5783cb0 --- /dev/null +++ b/docs/security/diagnostic-redaction-audit-report.md @@ -0,0 +1,31 @@ +# Chanora Diagnostic Redaction Audit Report + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Scope + +This report defines the diagnostic redaction audit required before enabling diagnostics for external testers or public release. + +## 2. Redaction Targets + +| Data class | Required handling | +|---|---| +| Server passwords | Redact before export | +| Identity/private-key material | Redact before export | +| Tokens and known secrets | Redact through known-secret registry | +| Host/IP/email/path values | Redact or minimize according to diagnostic policy | +| User text content | Avoid unnecessary capture; redact when classified as sensitive | + +## 3. Evidence Required + +| Evidence | Required result | +|---|---| +| Unit tests | Redactor removes sample secrets and registered known secrets | +| Export demo | User-initiated export produces a JSON bundle without sample secrets | +| Manual review | Reviewer inspects candidate export before external release | +| Privacy alignment | Privacy policy describes user-initiated diagnostic sharing | + +## 4. DV Conclusion + +Audit expectations are defined. Candidate export evidence must be attached before external tester release. diff --git a/docs/security/flutter-license-inventory.md b/docs/security/flutter-license-inventory.md new file mode 100644 index 0000000..5fe3902 --- /dev/null +++ b/docs/security/flutter-license-inventory.md @@ -0,0 +1,5284 @@ +# Chanora — Flutter / Dart license inventory + +Generated by `tools/dump_flutter_licenses.sh` from +`apps/chanora_flutter/pubspec.lock`. The script walks +every resolved dependency in the lockfile, pulls each +package's LICENSE file out of the local pub cache, +and writes the result here. CI checks that the +committed copy matches the regenerated output. + +Chanora itself is dual-licensed under +[Apache License 2.0](../../LICENSE-APACHE) or the +[MIT License](../../LICENSE-MIT) at the recipient's +option (DEC-020). The packages listed below carry +their own licenses and are redistributed under those +terms. + +| Package | Version | Source | License found | +|---------|---------|--------|---------------| +| `_fe_analyzer_shared` | 93.0.0 | hosted | yes | +| `analyzer` | 10.0.1 | hosted | yes | +| `args` | 2.7.0 | hosted | yes | +| `async` | 2.13.1 | hosted | yes | +| `audio_session` | 0.2.3 | hosted | yes | +| `boolean_selector` | 2.1.2 | hosted | yes | +| `build` | 4.0.6 | hosted | yes | +| `build_cli_annotations` | 2.1.1 | hosted | yes | +| `build_config` | 1.3.0 | hosted | yes | +| `build_daemon` | 4.1.1 | hosted | yes | +| `build_runner` | 2.15.0 | hosted | yes | +| `built_collection` | 5.1.1 | hosted | yes | +| `built_value` | 8.12.6 | hosted | yes | +| `characters` | 1.4.1 | hosted | yes | +| `checked_yaml` | 2.0.4 | hosted | yes | +| `clock` | 1.1.2 | hosted | yes | +| `code_assets` | 1.0.0 | hosted | yes | +| `collection` | 1.19.1 | hosted | yes | +| `connectivity_plus` | 7.1.1 | hosted | yes | +| `connectivity_plus_platform_interface` | 2.1.0 | hosted | yes | +| `convert` | 3.1.2 | hosted | yes | +| `cross_file` | 0.3.5+2 | hosted | yes | +| `crypto` | 3.0.7 | hosted | yes | +| `dart_style` | 3.1.7 | hosted | yes | +| `dbus` | 0.7.12 | hosted | yes | +| `fake_async` | 1.3.3 | hosted | yes | +| `ffi` | 2.2.0 | hosted | yes | +| `ffi_leak_tracker` | 0.1.2 | hosted | yes | +| `file` | 7.0.1 | hosted | yes | +| `fixnum` | 1.1.1 | hosted | yes | +| `flutter` | 0.0.0 | sdk | yes | +| `flutter_foreground_task` | 9.2.2 | hosted | yes | +| `flutter_lints` | 6.0.0 | hosted | yes | +| `flutter_localizations` | 0.0.0 | sdk | yes | +| `flutter_rust_bridge` | 2.12.0 | hosted | yes | +| `flutter_test` | 0.0.0 | sdk | yes | +| `flutter_web_plugins` | 0.0.0 | sdk | yes | +| `freezed` | 3.2.5 | hosted | yes | +| `freezed_annotation` | 3.1.0 | hosted | yes | +| `glob` | 2.1.3 | hosted | yes | +| `graphs` | 2.3.2 | hosted | yes | +| `haptic_kit` | 1.0.0 | hosted | yes | +| `hooks` | 1.0.3 | hosted | yes | +| `http` | 1.6.0 | hosted | yes | +| `http_multi_server` | 3.2.2 | hosted | yes | +| `http_parser` | 4.1.2 | hosted | yes | +| `intl` | 0.20.2 | hosted | yes | +| `io` | 1.0.5 | hosted | yes | +| `jni` | 1.0.0 | hosted | yes | +| `jni_flutter` | 1.0.1 | hosted | yes | +| `json_annotation` | 4.12.0 | hosted | yes | +| `leak_tracker` | 11.0.2 | hosted | yes | +| `leak_tracker_flutter_testing` | 3.0.10 | hosted | yes | +| `leak_tracker_testing` | 3.0.2 | hosted | yes | +| `lints` | 6.1.0 | hosted | yes | +| `logging` | 1.3.0 | hosted | yes | +| `matcher` | 0.12.19 | hosted | yes | +| `material_color_utilities` | 0.13.0 | hosted | yes | +| `meta` | 1.17.0 | hosted | yes | +| `mime` | 2.0.0 | hosted | yes | +| `native_toolchain_c` | 0.17.6 | hosted | yes | +| `nm` | 0.5.0 | hosted | yes | +| `objective_c` | 9.3.0 | hosted | yes | +| `package_config` | 2.2.0 | hosted | yes | +| `package_info_plus` | 10.1.0 | hosted | yes | +| `package_info_plus_platform_interface` | 4.1.0 | hosted | yes | +| `path` | 1.9.1 | hosted | yes | +| `path_provider` | 2.1.5 | hosted | yes | +| `path_provider_android` | 2.3.1 | hosted | yes | +| `path_provider_foundation` | 2.6.0 | hosted | yes | +| `path_provider_linux` | 2.2.1 | hosted | yes | +| `path_provider_platform_interface` | 2.1.2 | hosted | yes | +| `path_provider_windows` | 2.3.0 | hosted | yes | +| `petitparser` | 7.0.2 | hosted | yes | +| `platform` | 3.1.6 | hosted | yes | +| `plugin_platform_interface` | 2.1.8 | hosted | yes | +| `pool` | 1.5.2 | hosted | yes | +| `pub_semver` | 2.2.0 | hosted | yes | +| `pubspec_parse` | 1.5.0 | hosted | yes | +| `record_use` | 0.6.0 | hosted | yes | +| `rxdart` | 0.28.0 | hosted | yes | +| `share_plus` | 13.1.0 | hosted | yes | +| `share_plus_platform_interface` | 7.1.0 | hosted | yes | +| `shared_preferences` | 2.5.5 | hosted | yes | +| `shared_preferences_android` | 2.4.23 | hosted | yes | +| `shared_preferences_foundation` | 2.5.6 | hosted | yes | +| `shared_preferences_linux` | 2.4.1 | hosted | yes | +| `shared_preferences_platform_interface` | 2.4.2 | hosted | yes | +| `shared_preferences_web` | 2.4.3 | hosted | yes | +| `shared_preferences_windows` | 2.4.1 | hosted | yes | +| `shelf` | 1.4.2 | hosted | yes | +| `shelf_web_socket` | 3.0.0 | hosted | yes | +| `sky_engine` | 0.0.0 | sdk | yes | +| `source_gen` | 4.2.3 | hosted | yes | +| `source_span` | 1.10.2 | hosted | yes | +| `stack_trace` | 1.12.1 | hosted | yes | +| `stream_channel` | 2.1.4 | hosted | yes | +| `stream_transform` | 2.1.1 | hosted | yes | +| `string_scanner` | 1.4.1 | hosted | yes | +| `term_glyph` | 1.2.2 | hosted | yes | +| `test_api` | 0.7.10 | hosted | yes | +| `typed_data` | 1.4.0 | hosted | yes | +| `url_launcher` | 6.3.2 | hosted | yes | +| `url_launcher_android` | 6.3.30 | hosted | yes | +| `url_launcher_ios` | 6.4.1 | hosted | yes | +| `url_launcher_linux` | 3.2.2 | hosted | yes | +| `url_launcher_macos` | 3.2.5 | hosted | yes | +| `url_launcher_platform_interface` | 2.3.2 | hosted | yes | +| `url_launcher_web` | 2.4.3 | hosted | yes | +| `url_launcher_windows` | 3.1.5 | hosted | yes | +| `uuid` | 4.5.3 | hosted | yes | +| `vector_math` | 2.2.0 | hosted | yes | +| `vm_service` | 15.2.0 | hosted | yes | +| `watcher` | 1.2.1 | hosted | yes | +| `web` | 1.1.1 | hosted | yes | +| `web_socket` | 1.0.1 | hosted | yes | +| `web_socket_channel` | 3.0.3 | hosted | yes | +| `win32` | 6.2.0 | hosted | yes | +| `xdg_directories` | 1.1.0 | hosted | yes | +| `xml` | 6.6.1 | hosted | yes | +| `yaml` | 3.1.3 | hosted | yes | + +## Full license texts + +### _fe_analyzer_shared 93.0.0 + +``` +Copyright 2019, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### analyzer 10.0.1 + +``` +Copyright 2013, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### args 2.7.0 + +``` +Copyright 2013, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### async 2.13.1 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### audio_session 0.2.3 + +``` +MIT License + +Copyright (c) 2020 Ryan Heise and the project 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. +``` + +### boolean_selector 2.1.2 + +``` +Copyright 2016, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### build 4.0.6 + +``` +Copyright 2016, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### build_cli_annotations 2.1.1 + +``` +MIT License + +Copyright (c) 2018 Kevin Moore + +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. +``` + +### build_config 1.3.0 + +``` +Copyright 2017, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### build_daemon 4.1.1 + +``` +Copyright 2019, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### build_runner 2.15.0 + +``` +Copyright 2016, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### built_collection 5.1.1 + +``` +Copyright 2015, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### built_value 8.12.6 + +``` +Copyright 2015, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### characters 1.4.1 + +``` +Copyright 2019, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### checked_yaml 2.0.4 + +``` +Copyright 2019, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### clock 1.1.2 + +``` + + 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. +``` + +### code_assets 1.0.0 + +``` +Copyright 2025, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### collection 1.19.1 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### connectivity_plus 7.1.1 + +``` +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### connectivity_plus_platform_interface 2.1.0 + +``` +Copyright 2020 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### convert 3.1.2 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### cross_file 0.3.5+2 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### crypto 3.0.7 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### dart_style 3.1.7 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### dbus 0.7.12 + +``` +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. +``` + +### fake_async 1.3.3 + +``` + + 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. +``` + +### ffi 2.2.0 + +``` +Copyright 2019, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### ffi_leak_tracker 0.1.2 + +``` +BSD 3-Clause License + +Copyright (c) 2026, Halil Durmus + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### file 7.0.1 + +``` +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### fixnum 1.1.1 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### flutter 0.0.0 + +``` +Copyright 2014 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### flutter_foreground_task 9.2.2 + +``` +MIT License + +Copyright (c) 2021 Dev-hwang + +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. +``` + +### flutter_lints 6.0.0 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### flutter_localizations 0.0.0 + +``` +Copyright 2014 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### flutter_rust_bridge 2.12.0 + +``` +MIT License + +Copyright (c) 2021 fzyzcjy + +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. +``` + +### flutter_test 0.0.0 + +``` +Copyright 2014 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### flutter_web_plugins 0.0.0 + +``` +Copyright 2014 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### freezed 3.2.5 + +``` +MIT License + +Copyright (c) 2020 Remi Rousselet + +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. +``` + +### freezed_annotation 3.1.0 + +``` +MIT License + +Copyright (c) 2020 Remi Rousselet + +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. +``` + +### glob 2.1.3 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### graphs 2.3.2 + +``` +Copyright 2017, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### haptic_kit 1.0.0 + +``` +MIT License + +Copyright (c) 2026 Eryk Kruk + +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. +``` + +### hooks 1.0.3 + +``` +Copyright 2025, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### http 1.6.0 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### http_multi_server 3.2.2 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### http_parser 4.1.2 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### intl 0.20.2 + +``` +Copyright 2013, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### io 1.0.5 + +``` +Copyright 2017, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### jni 1.0.0 + +``` +Copyright 2022, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### jni_flutter 1.0.1 + +``` +Copyright 2026, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### json_annotation 4.12.0 + +``` +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### leak_tracker 11.0.2 + +``` +Copyright 2022, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### leak_tracker_flutter_testing 3.0.10 + +``` +Copyright 2022, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### leak_tracker_testing 3.0.2 + +``` +Copyright 2022, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### lints 6.1.0 + +``` +Copyright 2021, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### logging 1.3.0 + +``` +Copyright 2013, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### matcher 0.12.19 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### material_color_utilities 0.13.0 + +``` + + 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 2021 Google LLC + + 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. +``` + +### meta 1.17.0 + +``` +Copyright 2016, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### mime 2.0.0 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### native_toolchain_c 0.17.6 + +``` +Copyright 2023, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### nm 0.5.0 + +``` +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. +``` + +### objective_c 9.3.0 + +``` +Copyright 2024, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### package_config 2.2.0 + +``` +Copyright 2019, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### package_info_plus 10.1.0 + +``` +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### package_info_plus_platform_interface 4.1.0 + +``` +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path 1.9.1 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path_provider 2.1.5 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path_provider_android 2.3.1 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path_provider_foundation 2.6.0 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path_provider_linux 2.2.1 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path_provider_platform_interface 2.1.2 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### path_provider_windows 2.3.0 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### petitparser 7.0.2 + +``` +The MIT License + +Copyright (c) 2006-2024 Lukas Renggli. +All rights reserved. + +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. +``` + +### platform 3.1.6 + +``` +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### plugin_platform_interface 2.1.8 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### pool 1.5.2 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### pub_semver 2.2.0 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### pubspec_parse 1.5.0 + +``` +Copyright 2018, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### record_use 0.6.0 + +``` +Copyright 2024, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### rxdart 0.28.0 + +``` + 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. +``` + +### share_plus 13.1.0 + +``` +Copyright 2017, the Flutter project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### share_plus_platform_interface 7.1.0 + +``` +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences 2.5.5 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences_android 2.4.23 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences_foundation 2.5.6 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences_linux 2.4.1 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences_platform_interface 2.4.2 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences_web 2.4.3 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shared_preferences_windows 2.4.1 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shelf 1.4.2 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### shelf_web_socket 3.0.0 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### sky_engine 0.0.0 + +``` +Copyright 2014 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### source_gen 4.2.3 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### source_span 1.10.2 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### stack_trace 1.12.1 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### stream_channel 2.1.4 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### stream_transform 2.1.1 + +``` +Copyright 2017, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### string_scanner 1.4.1 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### term_glyph 1.2.2 + +``` +Copyright 2017, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### test_api 0.7.10 + +``` +Copyright 2018, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### typed_data 1.4.0 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher 6.3.2 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_android 6.3.30 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_ios 6.4.1 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_linux 3.2.2 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_macos 3.2.5 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_platform_interface 2.3.2 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_web 2.4.3 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### url_launcher_windows 3.1.5 + +``` +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### uuid 4.5.3 + +``` +Copyright (c) 2021 Yulian Kuncheff + +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. +``` + +### vector_math 2.2.0 + +``` +Copyright 2015, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2013 Andrew Magill + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +``` + +### vm_service 15.2.0 + +``` +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### watcher 1.2.1 + +``` +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### web 1.1.1 + +``` +Copyright 2023, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### web_socket 1.0.1 + +``` +Copyright 2024, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### web_socket_channel 3.0.3 + +``` +Copyright 2016, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### win32 6.2.0 + +``` +BSD 3-Clause License + +Copyright (c) 2024, Halil Durmus + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### xdg_directories 1.1.0 + +``` +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### xml 6.6.1 + +``` +The MIT License + +Copyright (c) 2006-2025 Lukas Renggli. +All rights reserved. + +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. +``` + +### yaml 3.1.3 + +``` +Copyright (c) 2014, the Dart project authors. +Copyright (c) 2006, Kirill Simonov. + +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/docs/security/license-inventory.md b/docs/security/license-inventory.md new file mode 100644 index 0000000..d3544f4 --- /dev/null +++ b/docs/security/license-inventory.md @@ -0,0 +1,10765 @@ +# Chanora — Third-party license inventory + +This document enumerates every third-party crate that ships in a +release build of Chanora and the license under which Chanora +redistributes it. Generated by `cargo about generate` from +`about.toml` at the repository root. + +Chanora itself is dual-licensed under +[Apache License 2.0](../../LICENSE-APACHE) or the +[MIT License](../../LICENSE-MIT) at the recipient's option (DEC-020 +in `docs/governance/product-decision-register.md`). The crates +listed below carry their own licenses and are redistributed under +those terms. + +## Licenses in use + +| License | Crate count | +|---------|-------------| +| `Apache License 2.0` | 330 | +| `MIT License` | 74 | +| `Unicode License v3` | 19 | +| `BSD 3-Clause "New" or "Revised" License` | 14 | +| `ISC License` | 9 | +| `Community Data License Agreement Permissive 2.0` | 1 | +| `zlib License` | 1 | + +## Crates + +| Crate | Version | License | Source | +|-------|---------|---------|--------| +| powerfmt | 0.2.0 | `Apache License 2.0` | | +| deranged | 0.5.8 | `Apache License 2.0` | | +| oboe-sys | 0.6.3 | `Apache License 2.0` | | +| oboe | 0.6.3 | `Apache License 2.0` | | +| encoding_rs | 0.8.35 | `Apache License 2.0` | | +| ort-sys | 2.0.0-rc.12 | `Apache License 2.0` | | +| ort | 2.0.0-rc.12 | `Apache License 2.0` | | +| tinyvec | 1.11.0 | `Apache License 2.0` | | +| utf8_iter | 1.0.4 | `Apache License 2.0` | | +| zeroize | 1.8.2 | `Apache License 2.0` | | +| zeroize_derive | 1.4.3 | `Apache License 2.0` | | +| windows-collections | 0.3.2 | `Apache License 2.0` | | +| windows-core | 0.54.0 | `Apache License 2.0` | | +| windows-core | 0.62.2 | `Apache License 2.0` | | +| windows-future | 0.3.2 | `Apache License 2.0` | | +| windows-implement | 0.60.2 | `Apache License 2.0` | | +| windows-interface | 0.59.3 | `Apache License 2.0` | | +| windows-link | 0.2.1 | `Apache License 2.0` | | +| windows-numerics | 0.3.1 | `Apache License 2.0` | | +| windows-registry | 0.6.1 | `Apache License 2.0` | | +| windows-result | 0.1.2 | `Apache License 2.0` | | +| windows-result | 0.4.1 | `Apache License 2.0` | | +| windows-strings | 0.5.1 | `Apache License 2.0` | | +| windows-sys | 0.45.0 | `Apache License 2.0` | | +| windows-sys | 0.60.2 | `Apache License 2.0` | | +| windows-sys | 0.61.2 | `Apache License 2.0` | | +| windows-targets | 0.42.2 | `Apache License 2.0` | | +| windows-targets | 0.52.6 | `Apache License 2.0` | | +| windows-targets | 0.53.5 | `Apache License 2.0` | | +| windows-threading | 0.2.1 | `Apache License 2.0` | | +| windows | 0.54.0 | `Apache License 2.0` | | +| windows | 0.62.2 | `Apache License 2.0` | | +| windows_x86_64_gnu | 0.52.6 | `Apache License 2.0` | | +| windows_x86_64_gnu | 0.53.1 | `Apache License 2.0` | | +| windows_x86_64_msvc | 0.42.2 | `Apache License 2.0` | | +| windows_x86_64_msvc | 0.52.6 | `Apache License 2.0` | | +| windows_x86_64_msvc | 0.53.1 | `Apache License 2.0` | | +| moka | 0.12.15 | `Apache License 2.0` | | +| tinyvec_macros | 0.1.1 | `Apache License 2.0` | | +| zerocopy | 0.8.48 | `Apache License 2.0` | | +| allo-isolate | 0.1.27 | `Apache License 2.0` | | +| cpal | 0.17.3 | `Apache License 2.0` | | +| rustls-platform-verifier | 0.7.0 | `Apache License 2.0` | | +| self_cell | 1.2.2 | `Apache License 2.0` | | +| tracing-android | 0.2.0 | `Apache License 2.0` | | +| ipnet | 2.12.0 | `Apache License 2.0` | | +| prefix-trie | 0.8.4 | `Apache License 2.0` | | +| env_filter | 0.1.4 | `Apache License 2.0` | | +| fallible-iterator | 0.3.0 | `Apache License 2.0` | | +| fallible-streaming-iterator | 0.1.9 | `Apache License 2.0` | | +| foreign-types-shared | 0.1.1 | `Apache License 2.0` | | +| foreign-types | 0.3.2 | `Apache License 2.0` | | +| hex | 0.4.3 | `Apache License 2.0` | | +| jni-sys | 0.3.1 | `Apache License 2.0` | | +| jni-sys | 0.4.1 | `Apache License 2.0` | | +| native-tls | 0.2.18 | `Apache License 2.0` | | +| openssl-macros | 0.1.1 | `Apache License 2.0` | — | +| openssl | 0.10.80 | `Apache License 2.0` | | +| resolv-conf | 0.7.6 | `Apache License 2.0` | | +| serde_spanned | 1.1.1 | `Apache License 2.0` | | +| toml | 1.1.2+spec-1.1.0 | `Apache License 2.0` | | +| toml_datetime | 1.1.1+spec-1.1.0 | `Apache License 2.0` | | +| toml_edit | 0.25.11+spec-1.1.0 | `Apache License 2.0` | | +| toml_parser | 1.1.2+spec-1.1.0 | `Apache License 2.0` | | +| toml_writer | 1.1.1+spec-1.1.0 | `Apache License 2.0` | | +| omnom | 3.0.0 | `Apache License 2.0` | | +| async-broadcast | 0.7.2 | `Apache License 2.0` | | +| hickory-net | 0.26.1 | `Apache License 2.0` | | +| hickory-proto | 0.26.1 | `Apache License 2.0` | | +| hickory-resolver | 0.26.1 | `Apache License 2.0` | | +| rustfft | 6.4.1 | `Apache License 2.0` | | +| strength_reduce | 0.2.4 | `Apache License 2.0` | | +| futures-channel | 0.3.32 | `Apache License 2.0` | | +| futures-core | 0.3.32 | `Apache License 2.0` | | +| futures-executor | 0.3.32 | `Apache License 2.0` | | +| futures-io | 0.3.32 | `Apache License 2.0` | | +| futures-macro | 0.3.32 | `Apache License 2.0` | | +| futures-sink | 0.3.32 | `Apache License 2.0` | | +| futures-task | 0.3.32 | `Apache License 2.0` | | +| futures-util | 0.3.32 | `Apache License 2.0` | | +| futures | 0.3.32 | `Apache License 2.0` | | +| typenum | 1.20.0 | `Apache License 2.0` | | +| reqwest | 0.12.28 | `Apache License 2.0` | | +| reqwest | 0.13.3 | `Apache License 2.0` | | +| http | 1.4.0 | `Apache License 2.0` | | +| tokio-rustls | 0.26.4 | `Apache License 2.0` | | +| pin-utils | 0.1.0 | `Apache License 2.0` | | +| ecdsa | 0.16.9 | `Apache License 2.0` | | +| rfc6979 | 0.4.0 | `Apache License 2.0` | | +| ppv-lite86 | 0.2.21 | `Apache License 2.0` | | +| rustls-pki-types | 1.14.1 | `Apache License 2.0` | | +| keyring | 3.6.3 | `Apache License 2.0` | | +| async-recursion | 1.1.1 | `Apache License 2.0` | | +| t4rust-derive | 0.3.1 | `Apache License 2.0` | | +| tsproto-structs | 0.2.0 | `Apache License 2.0` | | +| cmake | 0.1.54 | `Apache License 2.0` | | +| addr2line | 0.25.1 | `Apache License 2.0` | | +| ahash | 0.8.12 | `Apache License 2.0` | | +| atomic-waker | 1.1.2 | `Apache License 2.0` | | +| atomic | 0.5.3 | `Apache License 2.0` | | +| autocfg | 1.5.1 | `Apache License 2.0` | | +| backtrace | 0.3.76 | `Apache License 2.0` | | +| base64 | 0.22.1 | `Apache License 2.0` | | +| bitflags | 1.3.2 | `Apache License 2.0` | | +| bitflags | 2.11.1 | `Apache License 2.0` | | +| cc | 1.2.62 | `Apache License 2.0` | | +| cfg-if | 1.0.4 | `Apache License 2.0` | | +| concurrent-queue | 2.5.0 | `Apache License 2.0` | | +| core-foundation-sys | 0.8.7 | `Apache License 2.0` | | +| core-foundation | 0.10.1 | `Apache License 2.0` | | +| core-foundation | 0.9.4 | `Apache License 2.0` | | +| coreaudio-rs | 0.14.2 | `Apache License 2.0` | | +| critical-section | 1.2.0 | `Apache License 2.0` | | +| crossbeam-channel | 0.5.15 | `Apache License 2.0` | | +| crossbeam-epoch | 0.9.18 | `Apache License 2.0` | | +| crossbeam-utils | 0.8.21 | `Apache License 2.0` | | +| dbus-secret-service | 4.1.0 | `Apache License 2.0` | | +| displaydoc | 0.2.5 | `Apache License 2.0` | | +| either | 1.16.0 | `Apache License 2.0` | | +| equivalent | 1.0.2 | `Apache License 2.0` | | +| errno | 0.3.14 | `Apache License 2.0` | | +| event-listener-strategy | 0.5.4 | `Apache License 2.0` | | +| event-listener | 5.4.1 | `Apache License 2.0` | | +| fastrand | 2.4.1 | `Apache License 2.0` | | +| find-msvc-tools | 0.1.9 | `Apache License 2.0` | | +| fnv | 1.0.7 | `Apache License 2.0` | | +| form_urlencoded | 1.2.2 | `Apache License 2.0` | | +| futures-lite | 2.6.1 | `Apache License 2.0` | | +| gimli | 0.32.3 | `Apache License 2.0` | | +| group | 0.13.0 | `Apache License 2.0` | | +| hashbrown | 0.14.5 | `Apache License 2.0` | | +| hashbrown | 0.17.1 | `Apache License 2.0` | | +| heck | 0.5.0 | `Apache License 2.0` | | +| httparse | 1.10.1 | `Apache License 2.0` | | +| hyper-rustls | 0.27.9 | `Apache License 2.0` | | +| hyper-tls | 0.6.0 | `Apache License 2.0` | | +| idna | 1.1.0 | `Apache License 2.0` | | +| idna_adapter | 1.2.2 | `Apache License 2.0` | | +| indexmap | 2.14.0 | `Apache License 2.0` | | +| ipconfig | 0.3.4 | `Apache License 2.0` | | +| itertools | 0.14.0 | `Apache License 2.0` | | +| jni | 0.21.1 | `Apache License 2.0` | | +| jobserver | 0.1.34 | `Apache License 2.0` | | +| lazy_static | 1.5.0 | `Apache License 2.0` | | +| linux-raw-sys | 0.12.1 | `Apache License 2.0` | | +| lock_api | 0.4.14 | `Apache License 2.0` | | +| log | 0.4.29 | `Apache License 2.0` | | +| matrixmultiply | 0.3.10 | `Apache License 2.0` | | +| mime | 0.3.17 | `Apache License 2.0` | | +| ndarray | 0.17.2 | `Apache License 2.0` | | +| num-bigint | 0.4.6 | `Apache License 2.0` | | +| num-complex | 0.4.6 | `Apache License 2.0` | | +| num-derive | 0.4.2 | `Apache License 2.0` | | +| num-integer | 0.1.46 | `Apache License 2.0` | | +| num-traits | 0.2.19 | `Apache License 2.0` | | +| num_cpus | 1.17.0 | `Apache License 2.0` | | +| object | 0.37.3 | `Apache License 2.0` | | +| once_cell | 1.21.4 | `Apache License 2.0` | | +| openssl-probe | 0.2.1 | `Apache License 2.0` | | +| ordered-stream | 0.2.0 | `Apache License 2.0` | | +| parking | 2.2.1 | `Apache License 2.0` | | +| parking_lot | 0.12.5 | `Apache License 2.0` | | +| parking_lot_core | 0.9.12 | `Apache License 2.0` | | +| percent-encoding | 2.3.2 | `Apache License 2.0` | | +| pkg-config | 0.3.33 | `Apache License 2.0` | | +| primal-check | 0.3.4 | `Apache License 2.0` | | +| rawpointer | 0.2.1 | `Apache License 2.0` | | +| regex-automata | 0.4.14 | `Apache License 2.0` | | +| regex-syntax | 0.8.10 | `Apache License 2.0` | | +| regex | 1.12.3 | `Apache License 2.0` | | +| ring | 0.17.14 | `Apache License 2.0` | | +| rustc-demangle | 0.1.27 | `Apache License 2.0` | | +| rustc_version | 0.4.1 | `Apache License 2.0` | | +| rustix | 1.1.4 | `Apache License 2.0` | | +| rustls-native-certs | 0.8.3 | `Apache License 2.0` | | +| rustls | 0.23.40 | `Apache License 2.0` | | +| scopeguard | 1.2.0 | `Apache License 2.0` | | +| security-framework-sys | 2.17.0 | `Apache License 2.0` | | +| security-framework | 2.11.1 | `Apache License 2.0` | | +| security-framework | 3.7.0 | `Apache License 2.0` | | +| signal-hook-registry | 1.4.8 | `Apache License 2.0` | | +| simd_cesu8 | 1.1.1 | `Apache License 2.0` | | +| smallvec | 1.15.1 | `Apache License 2.0` | | +| socket2 | 0.6.3 | `Apache License 2.0` | | +| stable_deref_trait | 1.2.1 | `Apache License 2.0` | | +| system-configuration-sys | 0.6.0 | `Apache License 2.0` | | +| system-configuration | 0.7.0 | `Apache License 2.0` | | +| tempfile | 3.27.0 | `Apache License 2.0` | | +| thread_local | 1.1.9 | `Apache License 2.0` | | +| threadpool | 1.8.1 | `Apache License 2.0` | | +| unicode-xid | 0.2.6 | `Apache License 2.0` | | +| url | 2.5.8 | `Apache License 2.0` | | +| uuid | 1.23.1 | `Apache License 2.0` | | +| version_check | 0.9.5 | `Apache License 2.0` | | +| ff | 0.13.1 | `Apache License 2.0` | | +| hashlink | 0.9.1 | `Apache License 2.0` | | +| bit-vec | 0.6.3 | `Apache License 2.0` | | +| minimal-lexical | 0.2.1 | `Apache License 2.0` | | +| hkdf | 0.12.4 | `Apache License 2.0` | | +| aes | 0.8.4 | `Apache License 2.0` | | +| base16ct | 0.2.0 | `Apache License 2.0` | | +| base64ct | 1.8.3 | `Apache License 2.0` | | +| block-buffer | 0.10.4 | `Apache License 2.0` | | +| chacha20 | 0.10.0 | `Apache License 2.0` | | +| chacha20 | 0.9.1 | `Apache License 2.0` | | +| chacha20poly1305 | 0.10.1 | `Apache License 2.0` | | +| cipher | 0.4.4 | `Apache License 2.0` | | +| cmac | 0.7.2 | `Apache License 2.0` | | +| const-oid | 0.9.6 | `Apache License 2.0` | | +| cpufeatures | 0.2.17 | `Apache License 2.0` | | +| cpufeatures | 0.3.0 | `Apache License 2.0` | | +| crypto-bigint | 0.5.5 | `Apache License 2.0` | | +| crypto-common | 0.1.7 | `Apache License 2.0` | | +| ctr | 0.9.2 | `Apache License 2.0` | | +| dbl | 0.3.2 | `Apache License 2.0` | | +| der | 0.7.10 | `Apache License 2.0` | | +| digest | 0.10.7 | `Apache License 2.0` | | +| digest | 0.9.0 | `Apache License 2.0` | | +| eax | 0.5.0 | `Apache License 2.0` | | +| elliptic-curve | 0.13.8 | `Apache License 2.0` | | +| hmac | 0.12.1 | `Apache License 2.0` | | +| inout | 0.1.4 | `Apache License 2.0` | | +| md-5 | 0.10.6 | `Apache License 2.0` | | +| opaque-debug | 0.3.1 | `Apache License 2.0` | | +| p256 | 0.13.2 | `Apache License 2.0` | | +| pem-rfc7468 | 0.7.0 | `Apache License 2.0` | | +| pkcs8 | 0.10.2 | `Apache License 2.0` | | +| poly1305 | 0.8.0 | `Apache License 2.0` | | +| primeorder | 0.13.6 | `Apache License 2.0` | | +| sec1 | 0.7.3 | `Apache License 2.0` | | +| sha-1 | 0.10.1 | `Apache License 2.0` | | +| sha2 | 0.10.9 | `Apache License 2.0` | | +| signature | 2.2.0 | `Apache License 2.0` | | +| spki | 0.7.3 | `Apache License 2.0` | | +| universal-hash | 0.5.1 | `Apache License 2.0` | | +| aead | 0.5.2 | `Apache License 2.0` | | +| rand_core | 0.10.1 | `Apache License 2.0` | | +| rand_core | 0.6.4 | `Apache License 2.0` | | +| getrandom | 0.2.17 | `Apache License 2.0` | | +| getrandom | 0.3.4 | `Apache License 2.0` | | +| getrandom | 0.4.2 | `Apache License 2.0` | | +| rand_chacha | 0.3.1 | `Apache License 2.0` | | +| adler2 | 2.0.1 | `Apache License 2.0` | | +| proc-macro-crate | 3.5.0 | `Apache License 2.0` | | +| transpose | 0.2.3 | `Apache License 2.0` | | +| vcpkg | 0.2.15 | `Apache License 2.0` | | +| enumflags2 | 0.7.12 | `Apache License 2.0` | | +| enumflags2_derive | 0.7.12 | `Apache License 2.0` | | +| dbus | 0.9.11 | `Apache License 2.0` | | +| libdbus-sys | 0.2.7 | `Apache License 2.0` | | +| bytemuck | 1.25.0 | `Apache License 2.0` | | +| bytemuck_derive | 1.10.2 | `Apache License 2.0` | | +| android_log-sys | 0.2.0 | `Apache License 2.0` | | +| android_log-sys | 0.3.2 | `Apache License 2.0` | | +| android_logger | 0.15.1 | `Apache License 2.0` | | +| ts-bookkeeping | 0.1.0 | `Apache License 2.0` | | +| tsclientlib | 0.2.0 | `Apache License 2.0` | | +| tsproto-packets | 0.1.0 | `Apache License 2.0` | | +| tsproto-types | 0.1.0 | `Apache License 2.0` | | +| tsproto | 0.2.0 | `Apache License 2.0` | | +| chanora_core | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_audio | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_bridge | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_diagnostics | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_protocol | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_resolver | 0.1.0 | `Apache License 2.0` | — | +| chanora_server_prefetch | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_state | 0.2.0-beta.1 | `Apache License 2.0` | | +| chanora_storage | 0.2.0-beta.1 | `Apache License 2.0` | | +| alsa | 0.11.0 | `Apache License 2.0` | | +| anyhow | 1.0.102 | `Apache License 2.0` | | +| async-trait | 0.1.89 | `Apache License 2.0` | | +| aws-lc-sys | 0.41.0 | `Apache License 2.0` | | +| cesu8 | 1.1.0 | `Apache License 2.0` | | +| dart-sys | 4.1.5 | `Apache License 2.0` | | +| dasp_sample | 0.11.0 | `Apache License 2.0` | | +| dispatch2 | 0.3.1 | `Apache License 2.0` | | +| dunce | 1.0.5 | `Apache License 2.0` | | +| itoa | 1.0.18 | `Apache License 2.0` | | +| jni-macros | 0.22.4 | `Apache License 2.0` | | +| jni-sys-macros | 0.4.1 | `Apache License 2.0` | | +| jni | 0.22.4 | `Apache License 2.0` | | +| libc | 0.2.186 | `Apache License 2.0` | | +| linux-keyutils | 0.2.5 | `Apache License 2.0` | | +| mach2 | 0.5.0 | `Apache License 2.0` | | +| miniz_oxide | 0.8.9 | `Apache License 2.0` | | +| ndk-context | 0.1.1 | `Apache License 2.0` | | +| ndk-sys | 0.6.0+11769913 | `Apache License 2.0` | | +| ndk | 0.9.0 | `Apache License 2.0` | | +| num-conv | 0.2.2 | `Apache License 2.0` | | +| num_enum | 0.7.6 | `Apache License 2.0` | | +| num_enum_derive | 0.7.6 | `Apache License 2.0` | | +| objc2-audio-toolbox | 0.3.2 | `Apache License 2.0` | | +| objc2-avf-audio | 0.3.2 | `Apache License 2.0` | | +| objc2-core-audio-types | 0.3.2 | `Apache License 2.0` | | +| objc2-core-audio | 0.3.2 | `Apache License 2.0` | | +| objc2-core-foundation | 0.3.2 | `Apache License 2.0` | | +| pin-project-lite | 0.2.17 | `Apache License 2.0` | | +| portable-atomic | 1.13.1 | `Apache License 2.0` | | +| proc-macro2 | 1.0.106 | `Apache License 2.0` | | +| quicklz | 0.3.1 | `Apache License 2.0` | | +| quote | 1.0.45 | `Apache License 2.0` | | +| rand | 0.10.1 | `Apache License 2.0` | | +| rand | 0.8.6 | `Apache License 2.0` | | +| ref-cast-impl | 1.0.25 | `Apache License 2.0` | | +| ref-cast | 1.0.25 | `Apache License 2.0` | | +| rustls-platform-verifier-android | 0.1.1 | `Apache License 2.0` | | +| rustversion | 1.0.22 | `Apache License 2.0` | | +| ryu | 1.0.23 | `Apache License 2.0` | | +| semver | 1.0.28 | `Apache License 2.0` | | +| serde | 1.0.228 | `Apache License 2.0` | | +| serde_core | 1.0.228 | `Apache License 2.0` | | +| serde_derive | 1.0.228 | `Apache License 2.0` | | +| serde_json | 1.0.150 | `Apache License 2.0` | | +| serde_repr | 0.1.20 | `Apache License 2.0` | | +| serde_urlencoded | 0.7.1 | `Apache License 2.0` | | +| shlex | 1.3.0 | `Apache License 2.0` | | +| simdutf8 | 0.1.5 | `Apache License 2.0` | | +| syn | 2.0.117 | `Apache License 2.0` | | +| sync_wrapper | 1.0.2 | `Apache License 2.0` | | +| tagptr | 0.2.0 | `Apache License 2.0` | | +| thiserror-impl | 1.0.69 | `Apache License 2.0` | | +| thiserror-impl | 2.0.18 | `Apache License 2.0` | | +| thiserror | 1.0.69 | `Apache License 2.0` | | +| thiserror | 2.0.18 | `Apache License 2.0` | | +| time-core | 0.1.8 | `Apache License 2.0` | | +| time-macros | 0.2.27 | `Apache License 2.0` | | +| time | 0.3.47 | `Apache License 2.0` | | +| unicode-ident | 1.0.24 | `Apache License 2.0` | | +| widestring | 1.2.1 | `Apache License 2.0` | | +| sonora-agc2 | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| sonora-common-audio | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| sonora-fft | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| sonora-ns | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| sonora-simd | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| sonora | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| subtle-ng | 2.5.0 | `BSD 3-Clause "New" or "Revised" License` | | +| subtle | 2.6.1 | `BSD 3-Clause "New" or "Revised" License` | | +| aws-lc-sys | 0.41.0 | `BSD 3-Clause "New" or "Revised" License` | | +| curve25519-dalek-ng | 4.1.1 | `BSD 3-Clause "New" or "Revised" License` | | +| git-testament-derive | 0.2.1 | `BSD 3-Clause "New" or "Revised" License` | | +| git-testament | 0.2.6 | `BSD 3-Clause "New" or "Revised" License` | | +| sonora-aec3 | 0.1.0 | `BSD 3-Clause "New" or "Revised" License` | | +| encoding_rs | 0.8.35 | `BSD 3-Clause "New" or "Revised" License` | | +| webpki-roots | 1.0.7 | `Community Data License Agreement Permissive 2.0` | | +| untrusted | 0.9.0 | `ISC License` | | +| simple_asn1 | 0.6.4 | `ISC License` | | +| ring | 0.17.14 | `ISC License` | | +| libloading | 0.9.0 | `ISC License` | | +| rustls-webpki | 0.103.13 | `ISC License` | | +| audiopus_sys | 0.2.2 | `ISC License` | | +| audiopus | 0.3.0-rc.0 | `ISC License` | | +| aws-lc-rs | 1.17.0 | `ISC License` | | +| aws-lc-sys | 0.41.0 | `ISC License` | | +| uds_windows | 1.2.1 | `MIT License` | | +| sdl2-sys | 0.37.0 | `MIT License` | | +| openssl-sys | 0.9.116 | `MIT License` | | +| mio | 1.2.0 | `MIT License` | | +| nom | 7.1.3 | `MIT License` | | +| libsqlite3-sys | 0.30.1 | `MIT License` | | +| rusqlite | 0.32.1 | `MIT License` | | +| hyper | 1.9.0 | `MIT License` | | +| schannel | 0.1.29 | `MIT License` | | +| memoffset | 0.9.1 | `MIT License` | | +| version-compare | 0.1.1 | `MIT License` | | +| h2 | 0.4.14 | `MIT License` | | +| bytes | 1.11.1 | `MIT License` | | +| want | 0.3.1 | `MIT License` | | +| try-lock | 0.2.5 | `MIT License` | | +| slab | 0.4.12 | `MIT License` | | +| sharded-slab | 0.1.7 | `MIT License` | | +| matchers | 0.2.0 | `MIT License` | | +| tokio-native-tls | 0.3.1 | `MIT License` | | +| tracing-attributes | 0.1.31 | `MIT License` | | +| tracing-core | 0.1.36 | `MIT License` | | +| tracing-log | 0.2.0 | `MIT License` | | +| tracing-subscriber | 0.3.23 | `MIT License` | | +| tracing | 0.1.44 | `MIT License` | | +| tower-layer | 0.3.3 | `MIT License` | | +| tower-service | 0.3.3 | `MIT License` | | +| tower | 0.5.3 | `MIT License` | | +| tower-http | 0.6.11 | `MIT License` | | +| http-body | 1.0.1 | `MIT License` | | +| http-body-util | 0.1.3 | `MIT License` | | +| delegate-attr | 0.3.0 | `MIT License` | | +| hyper-util | 0.1.20 | `MIT License` | | +| zbus | 5.15.0 | `MIT License` | | +| zbus_macros | 5.15.0 | `MIT License` | | +| zbus_names | 4.3.2 | `MIT License` | | +| zvariant | 5.11.0 | `MIT License` | | +| zvariant_derive | 5.11.0 | `MIT License` | | +| synstructure | 0.13.2 | `MIT License` | | +| fs_extra | 1.3.0 | `MIT License` | | +| alsa-sys | 0.4.0 | `MIT License` | | +| dashmap | 5.5.3 | `MIT License` | | +| tokio-macros | 2.7.0 | `MIT License` | | +| oslog | 0.2.0 | `MIT License` | | +| block2 | 0.6.2 | `MIT License` | | +| flutter_rust_bridge | 2.12.0 | `MIT License` | | +| flutter_rust_bridge_macros | 2.12.0 | `MIT License` | | +| objc2-encode | 4.1.0 | `MIT License` | | +| objc2-foundation | 0.3.2 | `MIT License` | | +| objc2 | 0.6.4 | `MIT License` | | +| webrtc-vad | 0.4.0 | `MIT License` | | +| build-target | 0.4.0 | `MIT License` | | +| tokio-stream | 0.1.18 | `MIT License` | | +| tokio-util | 0.7.18 | `MIT License` | | +| tokio | 1.52.3 | `MIT License` | | +| endi | 1.1.1 | `MIT License` | | +| zmij | 1.0.21 | `MIT License` | | +| zvariant_utils | 3.3.1 | `MIT License` | | +| winnow | 1.0.3 | `MIT License` | | +| sdl2 | 0.37.0 | `MIT License` | | +| aho-corasick | 1.1.4 | `MIT License` | | +| byteorder | 1.5.0 | `MIT License` | | +| csv-core | 0.1.13 | `MIT License` | | +| csv | 1.4.0 | `MIT License` | | +| memchr | 2.8.0 | `MIT License` | | +| walkdir | 2.5.0 | `MIT License` | | +| combine | 4.6.7 | `MIT License` | | +| data-encoding | 2.11.0 | `MIT License` | | +| aws-lc-sys | 0.41.0 | `MIT License` | | +| derive_more-impl | 2.1.1 | `MIT License` | | +| derive_more | 2.1.1 | `MIT License` | | +| same-file | 1.0.6 | `MIT License` | | +| winapi-util | 0.1.11 | `MIT License` | | +| nu-ansi-term | 0.50.3 | `MIT License` | | +| generic-array | 0.14.7 | `MIT License` | | +| unicode-ident | 1.0.24 | `Unicode License v3` | | +| icu_collections | 2.2.0 | `Unicode License v3` | | +| icu_locale_core | 2.2.0 | `Unicode License v3` | | +| icu_normalizer | 2.2.0 | `Unicode License v3` | | +| icu_normalizer_data | 2.2.0 | `Unicode License v3` | | +| icu_properties | 2.2.0 | `Unicode License v3` | | +| icu_properties_data | 2.2.0 | `Unicode License v3` | | +| icu_provider | 2.2.0 | `Unicode License v3` | | +| litemap | 0.8.2 | `Unicode License v3` | | +| potential_utf | 0.1.5 | `Unicode License v3` | | +| tinystr | 0.8.3 | `Unicode License v3` | | +| writeable | 0.6.3 | `Unicode License v3` | | +| yoke-derive | 0.8.2 | `Unicode License v3` | | +| yoke | 0.8.2 | `Unicode License v3` | | +| zerofrom-derive | 0.1.7 | `Unicode License v3` | | +| zerofrom | 0.1.8 | `Unicode License v3` | | +| zerotrie | 0.2.4 | `Unicode License v3` | | +| zerovec-derive | 0.11.3 | `Unicode License v3` | | +| zerovec | 0.11.6 | `Unicode License v3` | | +| sdl2-sys | 0.37.0 | `zlib License` | | + +## Full license texts + +### Apache License 2.0 + +``` + + 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 2023 Jacob Pratt et al. + + 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. + +``` + +### Apache License 2.0 + +``` + + 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 2024 Jacob Pratt et al. + + 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. + +``` + +### Apache License 2.0 + +``` + + 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. + +``` + +### Apache License 2.0 + +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +### Apache License 2.0 + +``` + 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 2020 - 2026 Tatsuya Kawano + + 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. + +``` + +### Apache License 2.0 + +``` + 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 2020 Tomasz "Soveu" Marx + + 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. + + +``` + +### Apache License 2.0 + +``` + 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 2023 The Fuchsia Authors + + 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. + + +``` + +### Apache License 2.0 + +``` + 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. + +``` + +### Apache License 2.0 + +``` + 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 2017 Juniper Networks, Inc. + + 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. + +``` + +### Apache License 2.0 + +``` + 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. + + +``` + +### Apache License 2.0 + +``` + 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 + + Copyright 2019 Yoshua Wuyts + + 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. + +``` + +### Apache License 2.0 + +``` + 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 + + Copyright 2020 Yoshua Wuyts + + 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. + +``` + +### Apache License 2.0 + +``` + Apache License + Version 2.0, January 2004 + https://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 + + https://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. + + +``` + +### Apache License 2.0 + +``` + 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. + + +``` + +### Apache License 2.0 + +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +### Apache License 2.0 + +``` + 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 2014 Paho Lurie-Gregg + +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. +``` + +### Apache License 2.0 + +``` + 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 2016 Sean McArthur + +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. + +``` + +### Apache License 2.0 + +``` + 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 2017 http-rs authors + +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. + +``` + +### Apache License 2.0 + +``` + 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 2017 quininer kel + +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. + +``` + +### Apache License 2.0 + +``` + 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 2018 The pin-utils authors + +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. + +``` + +### Apache License 2.0 + +``` + 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 2018-2022 RustCrypto Developers + +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. + +``` + +### Apache License 2.0 + +``` + 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 2019 The CryptoCorrosion Contributors + +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. + +``` + +### Apache License 2.0 + +``` + 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 2023 Dirkjan Ochtman + +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. + +``` + +### Apache License 2.0 + +``` + 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 [2017] [keyring developers] + +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. + +``` + +### Apache License 2.0 + +``` + 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. +``` + +### Apache License 2.0 + +``` + 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. + +``` + +### Apache License 2.0 + +``` + 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. + + +``` + +### Apache License 2.0 + +``` + 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. +``` + +### Apache License 2.0 + +``` + 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. + +``` + +### Apache License 2.0 + +``` + 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. +``` + +### Apache License 2.0 + +``` + 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. + +``` + +### Apache License 2.0 + +``` + 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. + + +``` + +### Apache License 2.0 + +``` + Apache License + Version 2.0, January 2004 + https://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. + +``` + +### Apache License 2.0 + +``` + Apache License + Version 2.0, January 2004 + https://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 + + https://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. + +``` + +### Apache License 2.0 + +``` + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +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 + + https://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. + +``` + +### Apache License 2.0 + +``` + 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 2022 The transpose developers + +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. + +``` + +### Apache License 2.0 + +``` + 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. + + +``` + +### Apache License 2.0 + +``` +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: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +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 +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 + + +Copyright 2017-2023 Maik Klein, Maja Kądziołka + +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. + +``` + +### Apache License 2.0 + +``` +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: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +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 +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 + + +Copyright [2017] [Maik Klein] + +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. + +``` + +### Apache License 2.0 + +``` +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 2014-2018 David Henningsson <diwic@ubuntu.com> and other contributors + + 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. + + +``` + +### Apache License 2.0 + +``` +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. + +``` + +### Apache License 2.0 + +``` +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 2016 The android_log_sys Developers + +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. + +``` + +### Apache License 2.0 + +``` +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 2016 The android_logger Developers + +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. + +``` + +### Apache License 2.0 + +``` +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. + +``` + +### BSD 3-Clause "New" or "Revised" License + +``` +Copyright (c) 2011, The WebRTC Project Authors. All rights reserved. +Copyright (c) 2016, Arun Raghavan and contributors. All rights reserved. +Copyright (c) 2026, dignifiedquire. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +### BSD 3-Clause "New" or "Revised" License + +``` +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +### BSD 3-Clause "New" or "Revised" License + +``` +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +### BSD 3-Clause "New" or "Revised" License + +``` +Copyright (c) <year> <owner>. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +### BSD 3-Clause "New" or "Revised" License + +``` +Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +### Community Data License Agreement Permissive 2.0 + +``` +# Community Data License Agreement - Permissive - Version 2.0 + +This is the Community Data License Agreement - Permissive, Version +2.0 (the "agreement"). Data Provider(s) and Data Recipient(s) agree +as follows: + +## 1. Provision of the Data + +1.1. A Data Recipient may use, modify, and share the Data made +available by Data Provider(s) under this agreement if that Data +Recipient follows the terms of this agreement. + +1.2. This agreement does not impose any restriction on a Data +Recipient's use, modification, or sharing of any portions of the +Data that are in the public domain or that may be used, modified, +or shared under any other legal exception or limitation. + +## 2. Conditions for Sharing Data + +2.1. A Data Recipient may share Data, with or without modifications, so +long as the Data Recipient makes available the text of this agreement +with the shared Data. + +## 3. No Restrictions on Results + +3.1. This agreement does not impose any restriction or obligations +with respect to the use, modification, or sharing of Results. + +## 4. No Warranty; Limitation of Liability + +4.1. All Data Recipients receive the Data subject to the following +terms: + +THE DATA IS PROVIDED ON AN "AS IS" BASIS, WITHOUT REPRESENTATIONS, +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. + +NO DATA PROVIDER SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. + +``` + +### ISC License + +``` +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +### ISC License + +``` +Copyright (c) 2017 Adam Wick + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +``` + +### ISC License + +``` +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +### ISC License + +``` +Copyright © 2015, Simonas Kazlauskas + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +``` + +### ISC License + +``` +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + +``` + +### ISC License + +``` +ISC License + +Copyright (c) 2019, Lakelezz + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +### ISC License + +``` +ISC License + +Copyright (c) 2019, Lakelezz + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +### ISC License + +``` +ISC License: + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +### MIT License + +``` + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + 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 + +``` + +### MIT License + +``` +#ifndef _imKStoUCS_h +#define _imKStoUCS_h + +/* +Copyright (C) 2003-2006,2008 Jamey Sharp, Josh Triplett +Copyright © 2009 Red Hat, Inc. +Copyright 1990-1992,1999,2000,2004,2009,2010 Oracle and/or its affiliates. +All rights reserved. + +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 (including the next +paragraph) 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. +*/ + +extern unsigned int SDL_KeySymToUcs4(Uint32 keysym); + +#endif /* _imKStoUCS_h */ + +``` + +### MIT License + +``` +Copyright (c) 2014 Alex Crichton + +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. + +``` + +### MIT License + +``` +Copyright (c) 2014 Carl Lerche and other MIO 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. + +``` + +### MIT License + +``` +Copyright (c) 2014-2019 Geoffroy Couprie + +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. + +``` + +### MIT License + +``` +Copyright (c) 2014-2021 The rusqlite 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. + +``` + +### MIT License + +``` +Copyright (c) 2014-2026 Sean McArthur + +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. + +``` + +### MIT License + +``` +Copyright (c) 2015 steffengy + +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. + +``` + +### MIT License + +``` +Copyright (c) 2017 Gilad Naaman + +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. +``` + +### MIT License + +``` +Copyright (c) 2017 Tim Visée + +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. + + +``` + +### MIT License + +``` +Copyright (c) 2017 h2 authors + +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. + +``` + +### MIT License + +``` +Copyright (c) 2018 Carl Lerche + +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. + +``` + +### MIT License + +``` +Copyright (c) 2018-2019 Sean McArthur + +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. + + +``` + +### MIT License + +``` +Copyright (c) 2018-2023 Sean McArthur +Copyright (c) 2016 Alex Crichton + +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. + + +``` + +### MIT License + +``` +Copyright (c) 2019 Carl Lerche + +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. + +``` + +### MIT License + +``` +Copyright (c) 2019 Eliza Weisman + +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. + +``` + +### MIT License + +``` +Copyright (c) 2019 Eliza Weisman + +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. + +``` + +### MIT License + +``` +Copyright (c) 2019 Tokio 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. + +``` + +### MIT License + +``` +Copyright (c) 2019 Tower 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. + +``` + +### MIT License + +``` +Copyright (c) 2019-2021 Tower 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. + +``` + +### MIT License + +``` +Copyright (c) 2019-2024 Sean McArthur & Hyper 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. + +``` + +### MIT License + +``` +Copyright (c) 2019-2025 Sean McArthur & Hyper 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. + +``` + +### MIT License + +``` +Copyright (c) 2020 Xidorn Quan + +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. + +``` + +### MIT License + +``` +Copyright (c) 2023-2025 Sean McArthur + +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. + +``` + +### MIT License + +``` +Copyright (c) 2024 Zeeshan Ali Khan & zbus 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. + +``` + +### MIT License + +``` +Copyright 2016 Nika Layzell + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) 2017 Denis Kurilenko + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) 2018 diwic + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) 2019 Acrimon + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) 2019 Yoshua Wuyts +Copyright (c) Tokio 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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) 2020 Steven Joruk + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) <year> <copyright holders> + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) <year> <copyright holders> + +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 (including the next paragraph) 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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) OpenByte <development.openbyte@gmail.com> + +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. + +``` + +### MIT License + +``` +MIT License + +Copyright (c) Tokio 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. + +``` + +### 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. + +``` + +### 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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2013 Mozilla Foundation + +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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2015 Markus Westerlind + +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. + + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2015-2020 Julien Cretin +Copyright (c) 2017-2020 Google Inc. + +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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2015-2020 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). + +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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2016 Jelte Fennema + +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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2017 Andrew Gallant + +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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2014 Benjamin Sago +Copyright (c) 2021-2022 The Nushell Project 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. + +``` + +### MIT License + +``` +The MIT License (MIT) + +Copyright (c) 2015 Bartłomiej Kamiński + +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. +``` + +### Unicode License v3 + +``` +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +``` + +### Unicode License v3 + +``` +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + +``` + +### zlib License + +``` +Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org> + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +``` + + +--- + +Regenerate with: + +```bash +cargo about generate --output-file docs/security/license-inventory.md about-md.hbs +cargo about generate --output-file docs/security/license-inventory.html about.hbs +``` + +This artefact supports the DEC-012 legal review handoff at +`docs/governance/legal-review-readiness.md`. diff --git a/docs/security/secure-storage-audit-report.md b/docs/security/secure-storage-audit-report.md new file mode 100644 index 0000000..9799c4d --- /dev/null +++ b/docs/security/secure-storage-audit-report.md @@ -0,0 +1,31 @@ +# Chanora Secure Storage Audit Report + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Scope + +This report defines the secure-storage audit expected before public/store release. + +## 2. Audit Matrix + +| Platform | Expected backend | Current evidence state | +|---|---|---| +| Android | Android secure storage / Keystore-backed protection where implemented | Android Keystore-backed DEK deferred to v1.1 | +| iOS | Keychain | Device audit required | +| macOS | Keychain | Device audit required | +| Windows | Credential Manager | Device audit required | +| Linux | Secret Service with documented fallback behavior | Device audit required | + +## 3. Required Checks + +| Check | Required result | +|---|---| +| Secret persistence | Server passwords and identity secrets are not stored as unprotected plaintext | +| Fallback disclosure | Any file fallback is disclosed in release/security records | +| Diagnostic interaction | Stored secrets are registered for redaction where they can enter logs/export | +| CI behavior | CI keyring disabling is limited to headless test environments | + +## 4. DV Conclusion + +Secure-storage audit requirements are defined, but per-platform audit evidence is not complete. Public/store release remains blocked for strong secure-storage claims. diff --git a/docs/security/security-privacy-legal-guideline.md b/docs/security/security-privacy-legal-guideline.md new file mode 100644 index 0000000..8e68b32 --- /dev/null +++ b/docs/security/security-privacy-legal-guideline.md @@ -0,0 +1,63 @@ +# Chanora Security, Privacy, and Legal Guideline + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Scope:** Release-gate expectations for security, privacy, diagnostics, legal wording, and OSS review + +## 1. Purpose + +This guideline defines the minimum security, privacy, and legal checks that must be satisfied before Chanora moves beyond internal validation. It supports DV review by making gate ownership and evidence expectations explicit. + +## 2. Gate Summary + +| Gate | Requirement source | Required evidence | Current DV status | +|---|---|---|---| +| Secure storage | SysRS-237, SysRS-256, SRS-090 through SRS-095, SRS-126, SRS-137 | Platform audit for secret storage and fallback behavior | Partial; Android DEK waiver active | +| Diagnostic redaction | SysRS-238, SysRS-257, SRS-093 through SRS-102, SRS-126 | Redaction tests and export review | Evidence must be attached before external tester enablement | +| No automatic telemetry/upload | SysRS-295 and related SysDes/SRS privacy clauses | Inspection of runtime behavior and privacy wording | Baseline policy: no automatic upload in MVP | +| Dependency and OSS review | DEC-012, CI supply-chain jobs | Cargo deny/about, Flutter license inventory, legal sign-off | CI checks defined; DEC-012 remains open | +| Trademark/non-affiliation wording | SysRS-006, SysRS-240 | Legal/trademark review and UI/release wording inspection | Blocks public release until signed off | +| Privacy policy | Privacy and release readiness requirements | Published or approved privacy text | Baseline candidate exists; owner/legal review required | + +## 3. Secure Storage Expectations + +Sensitive data includes server passwords, identity/private-key material, tokens, diagnostic known secrets, and any credential-equivalent values. The application must use platform secure storage where available and must not silently advertise stronger protection than the active backend provides. + +Required release evidence: + +| Platform | Required evidence | +|---|---| +| Android | Key storage and DEK behavior review; Android Keystore-backed DEK gap is a release limitation until closed | +| iOS | Keychain behavior review on device | +| macOS | Keychain behavior review | +| Windows | Credential Manager behavior review | +| Linux | Secret Service behavior review and fallback limitation disclosure | + +## 4. Diagnostic Redaction Expectations + +Diagnostics must be user-initiated. Logs and export bundles must redact credentials, tokens, private paths when sensitive, host/IP/email where configured, and known secrets registered by runtime code. + +Diagnostic evidence must include: + +| Evidence | Required result | +|---|---| +| Redaction tests | Known secret classes are removed or replaced before export | +| Export demo | User can create a diagnostic bundle without automatic upload | +| Manual inspection | Exported JSON does not contain sample secrets used during the test | +| Privacy wording | User-facing policy explains what diagnostics contain and how sharing occurs | + +## 5. Legal and OSS Expectations + +Before public/store release, DEC-012 must close with approval covering: + +| Topic | Required result | +|---|---| +| TeamSpeak compatibility wording | Wording states compatibility without implying official affiliation | +| App name and metadata | Store, README, About dialog, and release notes avoid prohibited claims | +| OSS license inventory | Rust and Flutter dependencies have reviewed license inventory | +| NOTICE and attribution | Notices are complete for bundled or linked third-party software | +| Vulnerability/advisory review | Known critical issues are resolved, waived, or documented by owner approval | + +## 6. DV Conclusion + +Security, privacy, and legal gates are sufficiently documented for DV discussion. They are not sufficiently closed for public/store release. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md new file mode 100644 index 0000000..1c8564e --- /dev/null +++ b/docs/security/threat-model.md @@ -0,0 +1,33 @@ +# Chanora Threat Model + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Scope + +This threat model covers the Chanora client, local storage, diagnostics, bridge boundary, protocol adapter, audio path, platform services, and release artifacts. External compatible servers are outside Chanora control. + +## 2. Assets + +| Asset | Protection goal | +|---|---| +| Server passwords and identities | Prevent plaintext persistence and diagnostic leakage | +| Voice audio | Avoid unintended transmit and preserve user control | +| Diagnostic logs | Redact secrets before user-initiated export | +| Local bookmarks/settings | Preserve integrity and avoid accidental disclosure | +| Release artifacts | Preserve integrity and accurate capability claims | + +## 3. Primary Threats + +| Threat | Mitigation | Current DV status | +|---|---|---| +| Secret leakage in diagnostics | Known-secret registry and redactor | Requires export evidence | +| Plaintext or weak secret storage | Platform secure-storage abstraction and encryption | Android DEK waiver active | +| Stuck push-to-talk transmit | Missed-key-up watchdog and transmit gate | Requires platform PTT evidence | +| Protocol-library leakage into UI | Protocol adapter isolation | Architecture baseline covers boundary | +| Malicious or misconfigured external server | Treat server as external dependency and expose safe errors | Requires compatible-server/negative evidence | +| Over-claiming platform support | Capability records and release policy | Release record controls claims | + +## 4. DV Conclusion + +Major threats and mitigations are identified. Final release requires audit evidence for secure storage, diagnostics, platform PTT, and release artifacts. diff --git a/docs/additional_docs/srs.md b/docs/srs.md similarity index 100% rename from docs/additional_docs/srs.md rename to docs/srs.md diff --git a/docs/superpowers/plans/2026-05-28-chanora-server-prefetch-crate.md b/docs/superpowers/plans/2026-05-28-chanora-server-prefetch-crate.md new file mode 100644 index 0000000..7e3359a --- /dev/null +++ b/docs/superpowers/plans/2026-05-28-chanora-server-prefetch-crate.md @@ -0,0 +1,689 @@ +# Chanora Server Prefetch Crate Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move server-resolution prefetch policy from `chanora_core` into a focused crate named `chanora_server_prefetch` without changing Flutter APIs, resolver behavior, protocol dialing behavior, or Android connect UX. + +**Architecture:** Add `crates/chanora_server_prefetch` as a workspace member. The new crate owns normalization, single-entry TTL cache, generation rejection, resolver-backed async warming, and fresh exact-match lookup. `chanora_core` keeps the trust boundary for `ConnectConfig.resolved_address`, using the prefetcher only to populate one-time dial config while keeping stored/reconnect config sanitized. + +**Tech Stack:** Rust workspace, `tokio`, `tracing`, `thiserror`, `chanora_resolver`, `chanora_core`, `chanora_protocol`, Flutter Android smoke via ADB. + +--- + +## File Structure + +- Create `crates/chanora_server_prefetch/Cargo.toml`: package metadata and dependencies. +- Create `crates/chanora_server_prefetch/src/lib.rs`: public `ServerPrefetcher`, public `ServerPrefetchError`, internal cache entry/state, resolver-backed prefetch logic, and unit tests. +- Modify `Cargo.toml`: add `crates/chanora_server_prefetch` to workspace members and update the workspace layout comment. +- Modify `core/chanora_core/Cargo.toml`: replace the direct `chanora_resolver` dependency with `chanora_server_prefetch`. +- Modify `core/chanora_core/src/lib.rs`: remove private prefetch cache/resolver helpers, add `ServerPrefetcher`, delegate `prefetch_server_resolution`, and keep connect config sanitization tests. +- Modify `Cargo.lock`: generated by `cargo test`/`cargo check` after adding the crate. + +--- + +### Task 1: Add `chanora_server_prefetch` Crate With Cache Policy Tests + +**Files:** +- Modify: `Cargo.toml` +- Create: `crates/chanora_server_prefetch/Cargo.toml` +- Create: `crates/chanora_server_prefetch/src/lib.rs` + +- [ ] **Step 1: Add the workspace member and package files** + +In top-level `Cargo.toml`, update the layout comment and members: + +```toml +# crates/chanora_server_prefetch — server-resolution prefetch cache/policy +# crates/chanora_bridge/ — Flutter/Rust typed DTOs + glue +``` + +```toml +members = [ + "core/chanora_core", + "crates/chanora_protocol", + "crates/chanora_state", + "crates/chanora_audio", + "crates/chanora_storage", + "crates/chanora_diagnostics", + "crates/chanora_server_prefetch", + "crates/chanora_bridge", + "crates/chanora_resolver", +] +``` + +Create `crates/chanora_server_prefetch/Cargo.toml`: + +```toml +[package] +name = "chanora_server_prefetch" +description = "Chanora — server-address prefetch cache and policy built on chanora_resolver." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[dependencies] +chanora_resolver = { path = "../chanora_resolver" } +thiserror.workspace = true +tracing.workspace = true +tokio = { version = "1", features = ["sync", "rt", "macros"] } + +[features] +test-support = [] +``` + +Create `crates/chanora_server_prefetch/src/lib.rs` with the initial crate implementation and tests: + +```rust +//! Server-address prefetch cache and policy for Chanora. +//! +//! This crate owns speculative server-resolution warming. It does not +//! decide whether a connection should use a prefetched address; callers +//! must still apply their own trust boundary before dialing. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use thiserror::Error; +use tokio::sync::Mutex; +use tracing::{info, warn}; + +const SERVER_PREFETCH_TTL: Duration = Duration::from_secs(120); + +#[derive(Debug, Error)] +pub enum ServerPrefetchError { + #[error("resolver initialization failed: {0}")] + ResolverInit(String), +} + +#[derive(Debug, Clone)] +struct ServerPrefetchEntry { + normalized_host: String, + resolved_address: SocketAddr, + completed_at: Instant, + generation: u64, +} + +#[derive(Debug, Default)] +struct ServerPrefetchCache { + latest_generation: u64, + entry: Option, + last_failure: Option, +} + +impl ServerPrefetchCache { + fn begin(&mut self, host: &str) -> u64 { + if normalize_host(host).is_empty() { + return self.latest_generation; + } + self.latest_generation = self.latest_generation.saturating_add(1); + self.last_failure = None; + self.latest_generation + } + + fn store_success( + &mut self, + generation: u64, + host: &str, + resolved_address: SocketAddr, + completed_at: Instant, + ) { + if generation != self.latest_generation { + return; + } + self.entry = Some(ServerPrefetchEntry { + normalized_host: normalize_host(host), + resolved_address, + completed_at, + generation, + }); + self.last_failure = None; + } + + fn store_failure(&mut self, generation: u64, error: String) { + if generation != self.latest_generation { + return; + } + self.last_failure = Some(error); + } + + fn fresh_match(&self, host: &str, now: Instant) -> Option { + let normalized = normalize_host(host); + let entry = self.entry.as_ref()?; + if entry.normalized_host != normalized { + return None; + } + if entry.generation != self.latest_generation { + return None; + } + if now.duration_since(entry.completed_at) > SERVER_PREFETCH_TTL { + return None; + } + Some(entry.resolved_address) + } +} + +#[derive(Debug, Clone, Default)] +pub struct ServerPrefetcher { + cache: Arc>, +} + +impl ServerPrefetcher { + pub fn new() -> Self { + Self::default() + } + + pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError> { + let normalized = normalize_host(&host); + if normalized.is_empty() { + return Ok(()); + } + + let generation = { + let mut cache = self.cache.lock().await; + cache.begin(&normalized) + }; + let cache = self.cache.clone(); + tokio::spawn(async move { + info!(target: "chanora_server_prefetch", host = %normalized, "resolution prefetch started"); + let result = resolve_socket(&normalized).await; + let mut guard = cache.lock().await; + match result { + Ok(addr) => { + info!( + target: "chanora_server_prefetch", + host = %normalized, + resolved = %addr, + "resolution prefetch result" + ); + guard.store_success(generation, &normalized, addr, Instant::now()); + } + Err(err) => { + warn!( + target: "chanora_server_prefetch", + host = %normalized, + error = %err, + "resolution prefetch failed" + ); + guard.store_failure(generation, err.to_string()); + } + } + }); + Ok(()) + } + + pub async fn fresh_match(&self, host: &str) -> Option { + let resolved = { + let cache = self.cache.lock().await; + cache.fresh_match(host, Instant::now()) + }; + match resolved { + Some(addr) => { + info!( + target: "chanora_server_prefetch", + host = %host, + resolved = %addr, + "connect using prefetched resolution" + ); + Some(addr) + } + None => { + info!(target: "chanora_server_prefetch", host = %host, "connect prefetch miss or stale"); + None + } + } + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn begin_for_test(&self, host: &str) -> u64 { + let mut cache = self.cache.lock().await; + cache.begin(host) + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn store_success_for_test( + &self, + generation: u64, + host: &str, + resolved_address: SocketAddr, + completed_at: Instant, + ) { + let mut cache = self.cache.lock().await; + cache.store_success(generation, host, resolved_address, completed_at); + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn latest_generation_for_test(&self) -> u64 { + let cache = self.cache.lock().await; + cache.latest_generation + } +} + +fn normalize_host(host: &str) -> String { + host.trim().to_lowercase() +} + +async fn resolve_socket(host: &str) -> Result { + let resolver = chanora_resolver::ChanoraResolver::new() + .map_err(|err| ServerPrefetchError::ResolverInit(err.to_string()))?; + let resolved = resolver + .resolve_client_address(host) + .await + .map_err(|err| ServerPrefetchError::ResolverInit(err.to_string()))?; + Ok(resolved.connection_addr()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn fresh_exact_match_returns_socket_address() { + let prefetcher = ServerPrefetcher::new(); + let generation = prefetcher.begin_for_test(" Example.COM ").await; + let addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(generation, "example.com", addr, Instant::now()) + .await; + + assert_eq!(prefetcher.fresh_match("example.com").await, Some(addr)); + assert_eq!(prefetcher.fresh_match(" EXAMPLE.com ").await, Some(addr)); + } + + #[tokio::test] + async fn stale_entries_are_ignored() { + let prefetcher = ServerPrefetcher::new(); + let generation = prefetcher.begin_for_test("example.com").await; + let addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test( + generation, + "example.com", + addr, + Instant::now() - SERVER_PREFETCH_TTL - Duration::from_secs(1), + ) + .await; + + assert_eq!(prefetcher.fresh_match("example.com").await, None); + } + + #[tokio::test] + async fn different_hosts_are_ignored() { + let prefetcher = ServerPrefetcher::new(); + let generation = prefetcher.begin_for_test("example.com").await; + let addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(generation, "example.com", addr, Instant::now()) + .await; + + assert_eq!(prefetcher.fresh_match("other.example.com").await, None); + } + + #[tokio::test] + async fn stale_generation_completions_are_ignored() { + let prefetcher = ServerPrefetcher::new(); + let old_generation = prefetcher.begin_for_test("old.example.com").await; + let _new_generation = prefetcher.begin_for_test("new.example.com").await; + let old_addr = "127.0.0.1:9987".parse().unwrap(); + prefetcher + .store_success_for_test(old_generation, "old.example.com", old_addr, Instant::now()) + .await; + + assert_eq!(prefetcher.fresh_match("old.example.com").await, None); + } + + #[tokio::test] + async fn blank_hosts_do_not_update_generation() { + let prefetcher = ServerPrefetcher::new(); + let before = prefetcher.latest_generation_for_test().await; + + prefetcher.prefetch(" ".to_string()).await.unwrap(); + + assert_eq!(prefetcher.latest_generation_for_test().await, before); + } +} +``` + +- [ ] **Step 2: Run new crate tests** + +Run: `cargo test -p chanora_server_prefetch --lib` + +Expected: the new crate compiles and all 5 tests pass. + +- [ ] **Step 3: Commit Task 1** + +```bash +git add Cargo.toml Cargo.lock crates/chanora_server_prefetch +git commit -m "feat(prefetch): add server prefetch crate" +``` + +--- + +### Task 2: Wire Core to `ServerPrefetcher` + +**Files:** +- Modify: `core/chanora_core/Cargo.toml` +- Modify: `core/chanora_core/src/lib.rs` + +- [ ] **Step 1: Replace the core dependency** + +In `core/chanora_core/Cargo.toml`, replace: + +```toml +chanora_resolver = { path = "../../crates/chanora_resolver" } +``` + +with: + +```toml +chanora_server_prefetch = { path = "../../crates/chanora_server_prefetch" } +``` + +- [ ] **Step 2: Remove private prefetch cache implementation from core** + +In `core/chanora_core/src/lib.rs`, remove these private items: + +```rust +const RESOLUTION_PREFETCH_TTL: Duration = Duration::from_secs(120); + +#[derive(Debug, Clone)] +struct ResolutionPrefetchEntry { ... } + +#[derive(Debug, Default)] +struct ResolutionPrefetchCache { ... } + +impl ResolutionPrefetchCache { ... } + +fn normalize_prefetch_host(host: &str) -> String { ... } + +async fn resolve_prefetch_socket(host: &str) -> Result { ... } +``` + +Add this import near the other crate imports: + +```rust +use chanora_server_prefetch::ServerPrefetcher; +``` + +- [ ] **Step 3: Replace the session field and constructor initialization** + +Change the `ChanoraSession` field from: + +```rust +resolution_prefetch: Arc>, +``` + +to: + +```rust +server_prefetch: ServerPrefetcher, +``` + +Change the constructor initialization from: + +```rust +resolution_prefetch: Arc::new(Mutex::new(ResolutionPrefetchCache::default())), +``` + +to: + +```rust +server_prefetch: ServerPrefetcher::new(), +``` + +- [ ] **Step 4: Delegate prefetch and fresh-match lookup** + +Replace `prefetch_server_resolution` with: + +```rust +pub async fn prefetch_server_resolution(&self, host: String) -> Result<(), CoreError> { + self.server_prefetch + .prefetch(host) + .await + .map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed { + host: "prefetch".to_string(), + reason: err.to_string(), + })) +} +``` + +Replace `apply_prefetched_resolution` with: + +```rust +async fn apply_prefetched_resolution(&self, cfg: &mut ConnectConfig) { + cfg.resolved_address = None; + let host = cfg.address.trim(); + if host.is_empty() { + return; + } + if let Some(addr) = self.server_prefetch.fresh_match(host).await { + cfg.resolved_address = Some(addr); + } +} +``` + +Keep `prepare_connect_configs` unchanged except that it calls the updated `apply_prefetched_resolution`. + +- [ ] **Step 5: Update core tests to use the prefetcher seam** + +Keep these existing core tests: + +```rust +session_clears_untrusted_prefetched_resolution_on_cache_miss +connect_config_without_prefetched_resolution_clears_only_resolved_address +connect_config_preparation_keeps_prefetched_address_out_of_stored_config +``` + +Replace any direct access to `session.resolution_prefetch` with calls to public test-support methods on `ServerPrefetcher`. In `crates/chanora_server_prefetch/src/lib.rs`, keep these methods gated with `#[cfg(any(test, feature = "test-support"))]`: + +```rust +#[cfg(any(test, feature = "test-support"))] +pub async fn store_success_for_test( + &self, + generation: u64, + host: &str, + resolved_address: SocketAddr, + completed_at: Instant, +) +``` + +and: + +```rust +#[cfg(any(test, feature = "test-support"))] +pub async fn begin_for_test(&self, host: &str) -> u64 +``` + +Enable the feature for `chanora_core` tests by adding this dev-dependency in `core/chanora_core/Cargo.toml`: + +```toml +[dev-dependencies] +chanora_server_prefetch = { path = "../../crates/chanora_server_prefetch", features = ["test-support"] } +``` + +Then core test setup should look like: + +```rust +let generation = session.server_prefetch.begin_for_test("example.com").await; +session + .server_prefetch + .store_success_for_test(generation, "example.com", cached, std::time::Instant::now()) + .await; +``` + +- [ ] **Step 6: Run core tests and fix compile errors** + +Run: `cargo test -p chanora_core --lib` + +Expected: all core lib tests pass. + +- [ ] **Step 7: Commit Task 2** + +```bash +git add Cargo.toml Cargo.lock core/chanora_core/Cargo.toml core/chanora_core/src/lib.rs crates/chanora_server_prefetch +git commit -m "refactor(core): use server prefetch crate" +``` + +--- + +### Task 3: Remove Duplicated Core Cache Tests and Verify Protocol Is Unchanged + +**Files:** +- Modify: `core/chanora_core/src/lib.rs` +- Inspect only: `crates/chanora_protocol/src/adapter.rs` + +- [ ] **Step 1: Remove core tests moved to the prefetch crate** + +Delete these tests from `core/chanora_core/src/lib.rs` because they now belong in `chanora_server_prefetch`: + +```rust +resolution_prefetch_cache_returns_fresh_exact_match +resolution_prefetch_cache_ignores_stale_entries +resolution_prefetch_cache_ignores_different_hosts +resolution_prefetch_cache_ignores_stale_generation_completion +``` + +Do not delete core trust-boundary tests: + +```rust +session_uses_fresh_prefetched_resolution_for_connect_config +session_clears_untrusted_prefetched_resolution_on_cache_miss +connect_config_without_prefetched_resolution_clears_only_resolved_address +connect_config_preparation_keeps_prefetched_address_out_of_stored_config +``` + +- [ ] **Step 2: Confirm protocol source is untouched** + +Run: `git diff -- crates/chanora_protocol/src/adapter.rs` + +Expected: no diff. If there is a diff, revert only accidental protocol edits by manually restoring the changed lines from `HEAD`; do not use destructive git checkout/reset. + +- [ ] **Step 3: Run combined Rust tests** + +Run: `cargo test -p chanora_server_prefetch -p chanora_core -p chanora_protocol --lib` + +Expected: all tests pass. + +- [ ] **Step 4: Commit Task 3** + +```bash +git add core/chanora_core/src/lib.rs Cargo.lock +git commit -m "test(core): keep prefetch tests at crate boundary" +``` + +--- + +### Task 4: Final Verification and Android Smoke + +**Files:** +- No source edits expected. + +- [ ] **Step 1: Run full focused Rust verification** + +Run: `cargo test -p chanora_resolver -p chanora_server_prefetch -p chanora_protocol -p chanora_core --lib` + +Expected: all tests pass. + +- [ ] **Step 2: Run focused Flutter tests** + +Run from `apps/chanora_flutter`: + +```bash +flutter test test/services/server_resolution_prefetch_scheduler_test.dart test/services/connection_phase_state_test.dart test/services/ts3_server_link_test.dart +``` + +Expected: all tests pass. + +- [ ] **Step 3: Build Android APK** + +Run from `apps/chanora_flutter`: + +```bash +flutter build apk --debug +``` + +Expected: build succeeds and produces `build/app/outputs/flutter-apk/app-debug.apk`. + +- [ ] **Step 4: Run fresh-launch Android prefetch/connect smoke** + +Run from `apps/chanora_flutter` with an emulator/device attached: + +```bash +adb install -r "build/app/outputs/flutter-apk/app-debug.apk" +adb shell am force-stop app.chanora.chanora_flutter +adb logcat -c +adb shell monkey -p app.chanora.chanora_flutter -c android.intent.category.LAUNCHER 1 +sleep 5 +adb shell input tap 354 1363 +sleep 8 +adb logcat -d -v time | rg "resolution prefetch|prefetched|client request resolution|server address resolved|FATAL|AndroidRuntime|ANR" +adb shell uiautomator dump /sdcard/window.xml +adb exec-out cat /sdcard/window.xml +``` + +Expected logs include: + +```text +resolution prefetch started +resolution prefetch result +connect using prefetched resolution +using prefetched server address +``` + +Expected UI hierarchy includes: + +```text +Vigorous Pro +Leave server +Default Channel +ChanoraBeta +``` + +No app `FATAL`, app `AndroidRuntime` crash, or `ANR` lines should appear. `AndroidRuntime` lines from `monkey` or `uiautomator` are not app crashes. + +- [ ] **Step 5: Run one channel switch smoke** + +With the app still connected, run: + +```bash +adb logcat -c +adb shell input tap 300 1705 +sleep 2 +adb shell input tap 300 1465 +sleep 2 +adb logcat -d -v time | rg "voice_join|client_move|FATAL|AndroidRuntime|ANR" +adb shell uiautomator dump /sdcard/window.xml +adb exec-out cat /sdcard/window.xml +``` + +Expected logs include successful moves to channel IDs similar to: + +```text +voice_join accepted by server +client_move resolved by authoritative self channel change +``` + +Expected final UI has `ChanoraBeta` under `Default Channel`. + +- [ ] **Step 6: Check final git state** + +Run: + +```bash +git status --short +git log --oneline -5 +``` + +Expected: only unrelated untracked `config.json` remains, unless the user has added other unrelated work. The latest commits should be the prefetch crate split commits. + +--- + +## Self-Review + +- Spec coverage: the plan adds `chanora_server_prefetch`, moves cache policy and resolver-backed warming there, keeps core as trust boundary, preserves Flutter/protocol behavior, and includes Android connect smoke verification. +- Placeholder scan: no `TBD`, `TODO`, or unspecified edge handling remains. +- Type consistency: the plan uses `ServerPrefetcher`, `ServerPrefetchError`, `prefetch`, and `fresh_match` consistently across crate and core tasks. diff --git a/docs/superpowers/plans/2026-05-28-server-resolution-prefetch.md b/docs/superpowers/plans/2026-05-28-server-resolution-prefetch.md new file mode 100644 index 0000000..e99c4e3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-28-server-resolution-prefetch.md @@ -0,0 +1,899 @@ +# Server Resolution Prefetch Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add invisible server-address resolution prefetch so the active host field can warm Rust resolver state before Connect without changing connection semantics. + +**Architecture:** Flutter schedules debounced prefetch calls for the active host field only. Rust owns resolution prefetch state, TTL, exact-match validation, and connect-time reuse. The protocol layer accepts an optional already-resolved socket address so Connect can skip resolver work only when the core cache says it is safe. + +**Tech Stack:** Flutter/Dart, flutter_rust_bridge generated bindings, Rust async Tokio, `chanora_core`, `chanora_protocol`, `chanora_resolver`, Flutter widget/service tests, Rust unit tests. + +--- + +## File Structure + +- Modify `core/chanora_core/src/lib.rs`: add a session-owned prefetch cache, prefetch API, connect-time cache lookup, and unit tests for TTL/exact-match/generation behavior. +- Modify `crates/chanora_protocol/src/adapter.rs`: add `resolved_address: Option` to `ConnectConfig` and skip `resolve_server_socket()` when present. +- Modify `crates/chanora_bridge/src/api.rs`: add `prefetch_server_resolution(host: String)` bridge function and pass cache-aware connects through `ChanoraSession`. +- Regenerate `crates/chanora_bridge/src/frb_generated.rs`: generated Rust bridge bindings. +- Regenerate `apps/chanora_flutter/lib/src/rust/api.dart`, `api.freezed.dart`, and `frb_generated.dart`: generated Dart bridge bindings. +- Create `apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart`: small testable debounce helper for host-field prefetch scheduling. +- Modify `apps/chanora_flutter/lib/main.dart`: wire `_hostCtl` listener, settings-loaded prefetch, and disposal. +- Create `apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart`: Flutter/Dart tests for debounce and empty-host behavior. + +Keep all prefetch UI invisible. Do not prefetch bookmarks in bulk. + +--- + +### Task 1: Protocol Config Accepts Prefetched Socket + +**Files:** +- Modify: `crates/chanora_protocol/src/adapter.rs` + +- [ ] **Step 1: Add a failing protocol config test** + +Add this test inside the existing `#[cfg(test)] mod tests` in `crates/chanora_protocol/src/adapter.rs`: + +```rust +#[test] +fn connect_config_can_carry_prefetched_socket_address() { + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let cfg = ConnectConfig { + address: "example.com".to_string(), + nickname: "Tester".to_string(), + password: None, + identity: None, + ready_timeout: Duration::from_secs(1), + resolved_address: Some(addr), + }; + + assert_eq!(cfg.resolved_address, Some(addr)); +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cargo test -p chanora_protocol connect_config_can_carry_prefetched_socket_address` + +Expected: FAIL to compile with a message like `struct ConnectConfig has no field named resolved_address`. + +- [ ] **Step 3: Add the minimal config field** + +Update `ConnectConfig` in `crates/chanora_protocol/src/adapter.rs`: + +```rust +#[derive(Debug, Clone)] +pub struct ConnectConfig { + /// Server address: `hostname[:port]` or TSDNS name. + pub address: String, + /// Optional already-resolved socket address from core's invisible + /// prefetch cache. When present, the protocol layer skips address + /// resolution but still opens a normal TS3 connection only after + /// the user requested Connect. + pub resolved_address: Option, + /// Nickname to use on the server. + pub nickname: String, + /// Optional server password. + pub password: Option, + /// Optional pre-existing identity (base64 string accepted by + /// `tsclientlib::Identity::new_from_str`). If `None`, a fresh + /// identity is generated and **not persisted** — production callers + /// should provide one from secure identity storage. + pub identity: Option, + /// How long to wait for the initial state snapshot before + /// returning `ProtocolError::Timeout`. + pub ready_timeout: Duration, +} + +impl Default for ConnectConfig { + fn default() -> Self { + Self { + address: String::new(), + resolved_address: None, + nickname: "Chanora".to_string(), + password: None, + identity: None, + ready_timeout: Duration::from_secs(10), + } + } +} +``` + +- [ ] **Step 4: Run the focused test** + +Run: `cargo test -p chanora_protocol connect_config_can_carry_prefetched_socket_address` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/chanora_protocol/src/adapter.rs +git commit -m "feat(protocol): accept prefetched server address" +``` + +--- + +### Task 2: Protocol Connect Skips Resolver On Prefetch Hit + +**Files:** +- Modify: `crates/chanora_protocol/src/adapter.rs` + +- [ ] **Step 1: Add a failing helper test** + +Add a private helper next to `resolve_server_socket()` only after this failing test is added. First add this test in `#[cfg(test)] mod tests`: + +```rust +#[test] +fn server_socket_from_config_prefers_prefetched_address() { + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let cfg = ConnectConfig { + address: "example.com".to_string(), + resolved_address: Some(addr), + nickname: "Tester".to_string(), + password: None, + identity: None, + ready_timeout: Duration::from_secs(1), + }; + + assert_eq!(server_socket_from_config(&cfg), Some(addr)); +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cargo test -p chanora_protocol server_socket_from_config_prefers_prefetched_address` + +Expected: FAIL to compile with `cannot find function server_socket_from_config`. + +- [ ] **Step 3: Add the helper and wire connection task** + +Add this helper near `resolve_server_socket()`: + +```rust +fn server_socket_from_config(cfg: &ConnectConfig) -> Option { + cfg.resolved_address +} +``` + +Find the call in `connection_task` that currently resolves the address, shaped like: + +```rust +let server = resolve_server_socket(&cfg.address).await?; +``` + +Replace it with: + +```rust +let server = match server_socket_from_config(&cfg) { + Some(addr) => { + info!( + target: "chanora_protocol", + input = %cfg.address, + resolved = %addr, + "using prefetched server address" + ); + addr + } + None => resolve_server_socket(&cfg.address).await?, +}; +``` + +- [ ] **Step 4: Run protocol tests** + +Run: `cargo test -p chanora_protocol server_socket_from_config_prefers_prefetched_address` + +Expected: PASS. + +- [ ] **Step 5: Run broader protocol tests** + +Run: `cargo test -p chanora_protocol` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add crates/chanora_protocol/src/adapter.rs +git commit -m "fix(protocol): reuse prefetched server address" +``` + +--- + +### Task 3: Core Prefetch Cache Data Model + +**Files:** +- Modify: `core/chanora_core/src/lib.rs` + +- [ ] **Step 1: Write failing cache tests** + +Add these tests inside `#[cfg(test)] mod tests` in `core/chanora_core/src/lib.rs`: + +```rust +#[test] +fn resolution_prefetch_cache_returns_fresh_exact_match() { + let mut cache = ResolutionPrefetchCache::default(); + let now = std::time::Instant::now(); + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + + let generation = cache.begin(" Example.COM "); + cache.store_success(generation, " Example.COM ", addr, now); + + assert_eq!(cache.fresh_match("example.com", now), Some(addr)); +} + +#[test] +fn resolution_prefetch_cache_ignores_stale_entries() { + let mut cache = ResolutionPrefetchCache::default(); + let now = std::time::Instant::now(); + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + + let generation = cache.begin("example.com"); + cache.store_success(generation, "example.com", addr, now - RESOLUTION_PREFETCH_TTL - std::time::Duration::from_secs(1)); + + assert_eq!(cache.fresh_match("example.com", now), None); +} + +#[test] +fn resolution_prefetch_cache_ignores_different_hosts() { + let mut cache = ResolutionPrefetchCache::default(); + let now = std::time::Instant::now(); + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + + let generation = cache.begin("example.com"); + cache.store_success(generation, "example.com", addr, now); + + assert_eq!(cache.fresh_match("other.example.com", now), None); +} + +#[test] +fn resolution_prefetch_cache_ignores_stale_generation_completion() { + let mut cache = ResolutionPrefetchCache::default(); + let now = std::time::Instant::now(); + let first: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + let second: std::net::SocketAddr = "127.0.0.2:9987".parse().unwrap(); + + let old_generation = cache.begin("example.com"); + let new_generation = cache.begin("example.com"); + cache.store_success(new_generation, "example.com", second, now); + cache.store_success(old_generation, "example.com", first, now); + + assert_eq!(cache.fresh_match("example.com", now), Some(second)); +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cargo test -p chanora_core resolution_prefetch_cache --lib` + +Expected: FAIL to compile with missing `ResolutionPrefetchCache` and `RESOLUTION_PREFETCH_TTL`. + +- [ ] **Step 3: Add the cache model** + +Add near `NetworkDiagnostics` in `core/chanora_core/src/lib.rs`: + +```rust +const RESOLUTION_PREFETCH_TTL: Duration = Duration::from_secs(120); + +#[derive(Debug, Clone)] +struct ResolutionPrefetchEntry { + normalized_host: String, + resolved_address: std::net::SocketAddr, + completed_at: std::time::Instant, + generation: u64, +} + +#[derive(Debug, Default)] +struct ResolutionPrefetchCache { + latest_generation: u64, + entry: Option, + last_failure: Option, +} + +impl ResolutionPrefetchCache { + fn begin(&mut self, host: &str) -> u64 { + self.latest_generation = self.latest_generation.saturating_add(1); + self.last_failure = None; + let _ = normalize_prefetch_host(host); + self.latest_generation + } + + fn store_success( + &mut self, + generation: u64, + host: &str, + resolved_address: std::net::SocketAddr, + completed_at: std::time::Instant, + ) { + if generation != self.latest_generation { + return; + } + self.entry = Some(ResolutionPrefetchEntry { + normalized_host: normalize_prefetch_host(host), + resolved_address, + completed_at, + generation, + }); + self.last_failure = None; + } + + fn store_failure(&mut self, generation: u64, error: String) { + if generation != self.latest_generation { + return; + } + self.last_failure = Some(error); + } + + fn fresh_match(&self, host: &str, now: std::time::Instant) -> Option { + let normalized = normalize_prefetch_host(host); + let entry = self.entry.as_ref()?; + if entry.normalized_host != normalized { + return None; + } + if now.duration_since(entry.completed_at) > RESOLUTION_PREFETCH_TTL { + return None; + } + Some(entry.resolved_address) + } +} + +fn normalize_prefetch_host(host: &str) -> String { + host.trim().to_lowercase() +} +``` + +- [ ] **Step 4: Run focused core tests** + +Run: `cargo test -p chanora_core resolution_prefetch_cache --lib` + +Expected: all four tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add core/chanora_core/src/lib.rs +git commit -m "feat(core): add resolution prefetch cache" +``` + +--- + +### Task 4: Core Prefetch API And Connect Cache Lookup + +**Files:** +- Modify: `core/chanora_core/src/lib.rs` + +- [ ] **Step 1: Write failing session tests** + +Add this test in `#[cfg(test)] mod tests` in `core/chanora_core/src/lib.rs`: + +```rust +#[tokio::test] +async fn session_uses_fresh_prefetched_resolution_for_connect_config() { + let session = ChanoraSession::new(); + let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap(); + + { + let mut cache = session.resolution_prefetch.lock().await; + let generation = cache.begin("example.com"); + cache.store_success(generation, "example.com", addr, std::time::Instant::now()); + } + + let mut cfg = ConnectConfig::default(); + cfg.address = " example.com ".to_string(); + session.apply_prefetched_resolution(&mut cfg).await; + + assert_eq!(cfg.resolved_address, Some(addr)); +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cargo test -p chanora_core session_uses_fresh_prefetched_resolution_for_connect_config --lib` + +Expected: FAIL to compile with missing `resolution_prefetch` and `apply_prefetched_resolution`. + +- [ ] **Step 3: Add session field and constructor initialization** + +Add to `ChanoraSession`: + +```rust +/// Invisible server-address prefetch cache. Warmed by Flutter typing +/// but validated by Rust before Connect can reuse it. +resolution_prefetch: Arc>, +``` + +Initialize in `ChanoraSession::new()`: + +```rust +resolution_prefetch: Arc::new(Mutex::new(ResolutionPrefetchCache::default())), +``` + +- [ ] **Step 4: Add connect-time cache lookup helper** + +Add methods inside `impl ChanoraSession` before `connect()`: + +```rust +async fn apply_prefetched_resolution(&self, cfg: &mut ConnectConfig) { + let host = cfg.address.trim(); + if host.is_empty() { + return; + } + let resolved = { + let cache = self.resolution_prefetch.lock().await; + cache.fresh_match(host, std::time::Instant::now()) + }; + match resolved { + Some(addr) => { + info!( + target: "chanora_core", + host = %host, + resolved = %addr, + "connect using prefetched resolution" + ); + cfg.resolved_address = Some(addr); + } + None => { + info!(target: "chanora_core", host = %host, "connect prefetch miss or stale"); + } + } +} +``` + +In `connect()`, after identity resolution and before `ProtocolClient::connect(cfg.clone())`, add: + +```rust +self.apply_prefetched_resolution(&mut cfg).await; +``` + +- [ ] **Step 5: Add prefetch scheduling API in core** + +Add this method in `impl ChanoraSession`: + +```rust +pub async fn prefetch_server_resolution(&self, host: String) -> Result<(), CoreError> { + let normalized = normalize_prefetch_host(&host); + if normalized.is_empty() { + return Ok(()); + } + + let generation = { + let mut cache = self.resolution_prefetch.lock().await; + cache.begin(&normalized) + }; + let cache = self.resolution_prefetch.clone(); + tokio::spawn(async move { + info!(target: "chanora_core", host = %normalized, "resolution prefetch started"); + let result = resolve_prefetch_socket(&normalized).await; + let mut guard = cache.lock().await; + match result { + Ok(addr) => { + info!( + target: "chanora_core", + host = %normalized, + resolved = %addr, + "resolution prefetch result" + ); + guard.store_success(generation, &normalized, addr, std::time::Instant::now()); + } + Err(err) => { + warn!( + target: "chanora_core", + host = %normalized, + error = %err, + "resolution prefetch failed" + ); + guard.store_failure(generation, err.to_string()); + } + } + }); + Ok(()) +} +``` + +Add this helper outside `impl ChanoraSession`: + +```rust +async fn resolve_prefetch_socket(host: &str) -> Result { + let resolver = chanora_resolver::ChanoraResolver::new() + .map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed { + host: host.to_string(), + reason: format!("resolver initialization failed: {err}"), + }))?; + let resolved = resolver + .resolve_client_address(host) + .await + .map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed { + host: host.to_string(), + reason: err.to_string(), + }))?; + resolved + .parse::() + .map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed { + host: host.to_string(), + reason: format!("resolver returned invalid socket address '{resolved}': {err}"), + })) +} +``` + +- [ ] **Step 6: Run focused core tests** + +Run: `cargo test -p chanora_core session_uses_fresh_prefetched_resolution_for_connect_config --lib` + +Expected: PASS. + +- [ ] **Step 7: Run core tests** + +Run: `cargo test -p chanora_core --lib` + +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add core/chanora_core/src/lib.rs +git commit -m "feat(core): prefetch server resolution" +``` + +--- + +### Task 5: Bridge API Exposes Prefetch + +**Files:** +- Modify: `crates/chanora_bridge/src/api.rs` +- Regenerate: `crates/chanora_bridge/src/frb_generated.rs` +- Regenerate: `apps/chanora_flutter/lib/src/rust/api.dart` +- Regenerate: `apps/chanora_flutter/lib/src/rust/api.freezed.dart` +- Regenerate: `apps/chanora_flutter/lib/src/rust/frb_generated.dart` + +- [ ] **Step 1: Add bridge function** + +Add after `connect()` in `crates/chanora_bridge/src/api.rs`: + +```rust +/// Warm server address resolution for the active host field. This is +/// intentionally fire-and-forget from the UI perspective: it schedules +/// Rust-side prefetch work and never opens a TS3 session. +pub async fn prefetch_server_resolution(host: String) -> Result<(), BridgeError> { + runtime() + .spawn(async move { session().prefetch_server_resolution(host).await }) + .await + .map_err(|e| task_join_error("prefetch_server_resolution", e))??; + Ok(()) +} +``` + +- [ ] **Step 2: Regenerate flutter_rust_bridge bindings** + +Run from repo root: + +```bash +flutter_rust_bridge_codegen generate +``` + +Expected: generated files update and `apps/chanora_flutter/lib/src/rust/api.dart` contains: + +```dart +Future prefetchServerResolution({required String host}) => RustLib + .instance + .api + .crateApiPrefetchServerResolution(host: host); +``` + +- [ ] **Step 3: Run bridge/core compile check** + +Run: `cargo test -p chanora_bridge --lib` + +Expected: PASS. + +- [ ] **Step 4: Run Flutter analyzer smoke check** + +Run: `cd apps/chanora_flutter && flutter analyze` + +Expected: no new errors related to generated bindings. + +- [ ] **Step 5: Commit** + +```bash +git add crates/chanora_bridge/src/api.rs crates/chanora_bridge/src/frb_generated.rs apps/chanora_flutter/lib/src/rust/api.dart apps/chanora_flutter/lib/src/rust/api.freezed.dart apps/chanora_flutter/lib/src/rust/frb_generated.dart +git commit -m "feat(bridge): expose resolution prefetch" +``` + +--- + +### Task 6: Flutter Prefetch Scheduler Helper + +**Files:** +- Create: `apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart` +- Create: `apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart` + +- [ ] **Step 1: Write failing scheduler tests** + +Create `apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart`: + +```dart +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chanora_flutter/services/server_resolution_prefetch_scheduler.dart'; + +void main() { + test('debounces host edits and prefetches latest trimmed host', () async { + final calls = []; + final scheduler = ServerResolutionPrefetchScheduler( + delay: const Duration(milliseconds: 20), + prefetch: (host) async => calls.add(host), + ); + + scheduler.schedule(' first.example.com '); + scheduler.schedule(' second.example.com '); + await Future.delayed(const Duration(milliseconds: 35)); + + expect(calls, ['second.example.com']); + scheduler.dispose(); + }); + + test('skips empty hosts', () async { + final calls = []; + final scheduler = ServerResolutionPrefetchScheduler( + delay: const Duration(milliseconds: 10), + prefetch: (host) async => calls.add(host), + ); + + scheduler.schedule(' '); + await Future.delayed(const Duration(milliseconds: 25)); + + expect(calls, isEmpty); + scheduler.dispose(); + }); + + test('dispose cancels pending prefetch', () async { + final calls = []; + final scheduler = ServerResolutionPrefetchScheduler( + delay: const Duration(milliseconds: 30), + prefetch: (host) async => calls.add(host), + ); + + scheduler.schedule('example.com'); + scheduler.dispose(); + await Future.delayed(const Duration(milliseconds: 45)); + + expect(calls, isEmpty); + }); +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart` + +Expected: FAIL to compile with missing `server_resolution_prefetch_scheduler.dart`. + +- [ ] **Step 3: Implement scheduler** + +Create `apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart`: + +```dart +import 'dart:async'; + +typedef ServerResolutionPrefetch = Future Function(String host); + +class ServerResolutionPrefetchScheduler { + ServerResolutionPrefetchScheduler({ + required this.prefetch, + this.delay = const Duration(milliseconds: 700), + }); + + final ServerResolutionPrefetch prefetch; + final Duration delay; + + Timer? _timer; + bool _disposed = false; + + void schedule(String rawHost) { + if (_disposed) return; + _timer?.cancel(); + final host = rawHost.trim(); + if (host.isEmpty) return; + _timer = Timer(delay, () { + if (_disposed) return; + unawaited(prefetch(host)); + }); + } + + void dispose() { + _disposed = true; + _timer?.cancel(); + _timer = null; + } +} +``` + +- [ ] **Step 4: Run scheduler tests** + +Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart` + +Expected: PASS. + +- [ ] **Step 5: Format Dart files** + +Run: `cd apps/chanora_flutter && dart format lib/services/server_resolution_prefetch_scheduler.dart test/services/server_resolution_prefetch_scheduler_test.dart` + +Expected: files formatted, no errors. + +- [ ] **Step 6: Commit** + +```bash +git add apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart +git commit -m "feat(ui): add resolution prefetch scheduler" +``` + +--- + +### Task 7: Wire Prefetch Scheduler Into Home UI + +**Files:** +- Modify: `apps/chanora_flutter/lib/main.dart` + +- [ ] **Step 1: Add imports and state fields** + +In `apps/chanora_flutter/lib/main.dart`, add: + +```dart +import 'services/server_resolution_prefetch_scheduler.dart'; +``` + +Inside `_BetaHomeState`, add: + +```dart +late final ServerResolutionPrefetchScheduler _resolutionPrefetch; +``` + +- [ ] **Step 2: Initialize scheduler and listener** + +In `initState()`, before `_eventsSub = rust.eventsStream().listen(_onEvent);`, add: + +```dart +_resolutionPrefetch = ServerResolutionPrefetchScheduler( + prefetch: (host) => rust.prefetchServerResolution(host: host), +); +_hostCtl.addListener(_onHostEdited); +``` + +Add method in `_BetaHomeState`: + +```dart +void _onHostEdited() { + _resolutionPrefetch.schedule(_hostCtl.text); +} +``` + +- [ ] **Step 3: Schedule prefetch after settings load** + +In `_loadUiSettings()`, after setting host/nickname from settings and before the `catch`, add: + +```dart +if (settings.host.isNotEmpty) { + _resolutionPrefetch.schedule(settings.host); +} +``` + +Ensure this is not duplicated if an existing `_hostCtl.text = settings.host;` listener already schedules it. If both paths fire, keep only the explicit `_resolutionPrefetch.schedule(settings.host);` and temporarily remove/re-add the listener around `_hostCtl.text = settings.host`, or accept the duplicate because scheduler debounce collapses it. Prefer accepting the duplicate for minimal change. + +- [ ] **Step 4: Dispose scheduler and listener** + +In `dispose()`, before `_hostCtl.dispose();`, add: + +```dart +_hostCtl.removeListener(_onHostEdited); +_resolutionPrefetch.dispose(); +``` + +- [ ] **Step 5: Run focused Flutter tests** + +Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart test/services/connection_phase_state_test.dart` + +Expected: PASS. + +- [ ] **Step 6: Run analyzer** + +Run: `cd apps/chanora_flutter && flutter analyze` + +Expected: no new analyzer errors. + +- [ ] **Step 7: Commit** + +```bash +git add apps/chanora_flutter/lib/main.dart +git commit -m "feat(ui): prefetch active server address" +``` + +--- + +### Task 8: End-To-End Verification On Android + +**Files:** +- No source changes expected unless verification finds a bug. + +- [ ] **Step 1: Run focused Rust tests** + +Run: `cargo test -p chanora_resolver -p chanora_protocol -p chanora_core --lib` + +Expected: PASS. + +- [ ] **Step 2: Run focused Flutter tests** + +Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart test/services/connection_phase_state_test.dart test/services/ts3_server_link_test.dart` + +Expected: PASS. + +- [ ] **Step 3: Build Android debug APK** + +Run: `cd apps/chanora_flutter && flutter build apk --debug` + +Expected: `✓ Built build/app/outputs/flutter-apk/app-debug.apk`. + +- [ ] **Step 4: Install and launch through ADB** + +Run from `apps/chanora_flutter`: + +```bash +adb install -r "build/app/outputs/flutter-apk/app-debug.apk" +adb shell am force-stop app.chanora.chanora_flutter +adb logcat -c +adb shell monkey -p app.chanora.chanora_flutter -c android.intent.category.LAUNCHER 1 +``` + +Expected: install success and app launches. + +- [ ] **Step 5: Allow prefetch, connect, and inspect logs** + +Run from repo root: + +```bash +sleep 2 +adb shell input tap 354 1363 +sleep 8 +adb logcat -d -v time | rg "resolution prefetch|prefetched|client request resolution|server address resolved|FATAL|AndroidRuntime" +``` + +Expected: + +- No app `FATAL` lines. +- Logs include `resolution prefetch started` and either `resolution prefetch result` or safe failure. +- If prefetch completed before tap, logs include `connect using prefetched resolution` and protocol logs include `using prefetched server address`. +- If prefetch did not complete before tap, connect still succeeds through normal resolver path. + +- [ ] **Step 6: Inspect UI hierarchy** + +Run: + +```bash +adb shell uiautomator dump /sdcard/window.xml +adb exec-out cat /sdcard/window.xml +``` + +Expected: connected server view with channel count, not stuck on `Synchronizing...`. + +- [ ] **Step 7: Final status check** + +Run: `git status --short` + +Expected: no unintended source changes. Unrelated existing `config.json` may remain untracked and must not be committed. + +--- + +## Self-Review + +Spec coverage: + +- Active-field-only prefetch: Tasks 6 and 7. +- Rust-owned resolver cache and exact-match validation: Tasks 3 and 4. +- 2-minute TTL: Task 3. +- Invisible UI: Tasks 6 and 7 avoid any UI status changes. +- No bookmark fan-out: Task 7 wires only `_hostCtl` and settings-loaded host. +- No TS3 session before Connect: Task 4 only resolves address; Task 2 only uses resolved address during actual protocol connect. +- Diagnostics: Tasks 2 and 4 add logs for prefetch and cache hit/miss. +- Testing and Android smoke: Task 8. + +Completeness scan: no incomplete markers are used. Each code-changing task includes concrete code snippets and commands. + +Type consistency: `resolved_address` is added to protocol `ConnectConfig`, core uses `cfg.resolved_address`, bridge exposes `prefetch_server_resolution`, and Dart uses generated `prefetchServerResolution`. diff --git a/docs/superpowers/plans/2026-05-29-dv-evidence-pack.md b/docs/superpowers/plans/2026-05-29-dv-evidence-pack.md new file mode 100644 index 0000000..bf5a25b --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-dv-evidence-pack.md @@ -0,0 +1,114 @@ +# DV Evidence Pack Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a document set that lets a DV meeting review Chanora's current verification posture, traceability, release blockers, waivers, and evidence without implying incomplete work is complete. + +**Architecture:** The pack is documentation-only. Verification plans live under `docs/verification/`; release decision evidence lives under `docs/release/`; cross-document traceability lives under `docs/governance/`; security, privacy, and legal gate summaries live in their existing README-advertised folders. + +**Tech Stack:** Markdown, existing SysRS/SysDes/SRS baselines, implementation status report, CI workflow definitions. + +--- + +### Task 1: Create Verification Plan Set + +**Files:** +- Create: `docs/verification/verification-master-plan.md` +- Create: `docs/verification/swe4-unit-verification-plan.md` +- Create: `docs/verification/swe5-software-integration-verification-plan.md` +- Create: `docs/verification/swe6-software-verification-plan.md` +- Create: `docs/verification/sys4-system-integration-verification-plan.md` + +- [x] **Step 1: Write master plan** + +Create `docs/verification/verification-master-plan.md` with lifecycle scope, evidence rules, entry/exit criteria, current evidence sources, open gates, and reviewer decision framing. + +- [x] **Step 2: Write SWE.4 plan** + +Create `docs/verification/swe4-unit-verification-plan.md` with unit verification scope for Rust crates, Flutter services/widgets, diagnostics, storage, state reducers, audio DSP, and known unit-test gaps. + +- [x] **Step 3: Write SWE.5 plan** + +Create `docs/verification/swe5-software-integration-verification-plan.md` with cross-component integration scope for Flutter-bridge-core, protocol-state, audio-platform, secure storage, diagnostics export, resolver prefetch, and packaging hooks. + +- [x] **Step 4: Write SWE.6 plan** + +Create `docs/verification/swe6-software-verification-plan.md` with SRS-level acceptance scope and the SysRS-241 through SysRS-257 MVP acceptance matrix. + +- [x] **Step 5: Write SYS.4 plan** + +Create `docs/verification/sys4-system-integration-verification-plan.md` with system-level integration scope for external compatible servers, OS services, hardware, network, app stores, diagnostics, and release evidence. + +### Task 2: Create Release and DV Decision Records + +**Files:** +- Create: `docs/release/release-readiness-go-nogo-record.md` +- Create: `docs/release/dv-waiver-register.md` + +- [x] **Step 1: Write release readiness record** + +Create `docs/release/release-readiness-go-nogo-record.md` with current candidate metadata, decision state, platform readiness, verification status, legal/security/privacy gates, blockers, and meeting recommendation. + +- [x] **Step 2: Write waiver register** + +Create `docs/release/dv-waiver-register.md` with explicit waivers for DEC-012, Android Keystore DEK, iOS voice-processing mode, source-build-only desktop/iOS artifacts, state reducer coverage, and VAD deferral. + +### Task 3: Create Traceability and Gate Summaries + +**Files:** +- Create: `docs/governance/traceability-matrix.md` +- Create: `docs/security/security-privacy-legal-guideline.md` +- Create: `docs/security/dependency-and-supply-chain-report.md` +- Create: `docs/privacy/privacy-policy.md` +- Create: `docs/legal/trademark-and-attribution-review.md` + +- [x] **Step 1: Write traceability matrix** + +Create `docs/governance/traceability-matrix.md` summarizing SysRS to SysDes to SRS to verification coverage, including the MVP acceptance and verification handoff items. + +- [x] **Step 2: Write security/privacy/legal guideline** + +Create `docs/security/security-privacy-legal-guideline.md` summarizing release gates and evidence expectations for secure storage, diagnostics redaction, dependency review, privacy policy, and affiliation wording. + +- [x] **Step 3: Write dependency report** + +Create `docs/security/dependency-and-supply-chain-report.md` summarizing current CI checks and open evidence gaps without claiming DEC-012 completion. + +- [x] **Step 4: Write privacy policy baseline** + +Create `docs/privacy/privacy-policy.md` as an engineering release-candidate privacy baseline covering local storage, permissions, diagnostics, and the no automatic telemetry posture. + +- [x] **Step 5: Write trademark review** + +Create `docs/legal/trademark-and-attribution-review.md` with current non-affiliation wording requirement and open legal sign-off state. + +### Task 4: Verify Documentation Pack + +**Files:** +- Inspect all created files. + +- [x] **Step 1: Search for forbidden sentinel text** + +Run the sentinel-language scan over `docs/verification`, `docs/release`, `docs/governance`, `docs/security`, `docs/privacy`, and `docs/legal`. + +Expected: no matches introduced by the DV evidence pack except intentional historical references in source documents outside these folders. + +- [x] **Step 2: Confirm expected files exist** + +Run: `ls docs/verification docs/release docs/governance docs/security docs/privacy docs/legal` + +Expected: all DV evidence pack files are listed. + +- [x] **Step 3: Inspect git status and diff** + +Run: `git diff -- docs/verification docs/release docs/governance docs/security docs/privacy docs/legal docs/superpowers/plans/2026-05-29-dv-evidence-pack.md` + +Expected: only intended Markdown additions are present. + +--- + +## Self-Review + +- Spec coverage: covers verification plan set, release decision evidence, waiver register, traceability matrix, and minimum security/privacy/legal gate summaries. +- Placeholder scan: plan contains no incomplete instructions. +- Type consistency: document paths match the README-advertised folders and task file list. diff --git a/docs/superpowers/plans/2026-05-29-finish-dv-document-tree.md b/docs/superpowers/plans/2026-05-29-finish-dv-document-tree.md new file mode 100644 index 0000000..e20a105 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-finish-dv-document-tree.md @@ -0,0 +1,79 @@ +# Finish DV Document Tree Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fill the README-advertised document tree with baseline candidate documents so DV reviewers can navigate all required gates. + +**Architecture:** Keep canonical large baselines at current root paths and add README-path stubs or summaries where needed. Governance, release, security, privacy, legal, UI/UX, i18n, and references each get explicit baseline documents that state current evidence and open gates honestly. + +**Tech Stack:** Markdown documentation aligned to SysRS, SysDes, SRS, SAD, SDD, verification, release, and implementation status. + +--- + +### Task 1: Add Requirements Path Wrappers + +**Files:** +- Create: `docs/requirements/sysrs.md` +- Create: `docs/requirements/srs.md` + +- [x] **Step 1: Create README entry records that point to canonical root documents and summarize DV review anchors** + +These wrappers preserve README paths without duplicating the canonical baselines. + +### Task 2: Add Governance Baselines + +**Files:** +- Create: `docs/governance/document-index.md` +- Create: `docs/governance/document-naming-convention.md` +- Create: `docs/governance/baseline-approval-record.md` +- Create: `docs/governance/baseline-candidate-validation-report.md` +- Create: `docs/governance/document-review-report.md` +- Create: `docs/governance/product-decision-register.md` +- Create: `docs/governance/decision-impact-assessment.md` +- Create: `docs/governance/git-commit-message-convention.md` +- Create: `docs/governance/repo-format-validation-report.md` +- Create: `docs/governance/path-migration-map.md` + +- [x] **Step 1: Write governance records** + +Each record summarizes status for DV, identifies owner expectations, and avoids claiming final release approval. + +### Task 3: Add Remaining Release, Security, UI/UX, I18n, Reference Docs + +**Files:** +- Create: `docs/release/platform-release-policy.md` +- Create: `docs/security/threat-model.md` +- Create: `docs/security/secure-storage-audit-report.md` +- Create: `docs/security/diagnostic-redaction-audit-report.md` +- Create: `docs/ui-ux/material3-guideline.md` +- Create: `docs/ui-ux/material3-design-tokens.md` +- Create: `docs/ui-ux/material3-component-catalog.md` +- Create: `docs/ui-ux/adaptive-layout-platform-guide.md` +- Create: `docs/i18n/localization-architecture.md` +- Create: `docs/references/external-references.md` +- Create: `docs/references/aspice-swe2-swe3-integration-note.md` + +- [x] **Step 1: Write remaining baseline docs** + +Use concise DV-ready records that reference current implementation and open gaps. + +### Task 4: Verify Complete Tree + +**Files:** +- Inspect all created documents. + +- [x] **Step 1: Check README paths exist** + +Run a shell `test -f` command over every README-advertised path. + +- [x] **Step 2: Sentinel-language scan** + +Run the sentinel-language scan over `docs/requirements`, `docs/architecture`, `docs/verification`, `docs/release`, `docs/security`, `docs/privacy`, `docs/legal`, `docs/ui-ux`, `docs/i18n`, `docs/governance`, and `docs/references`; expect no matches. + +--- + +## Self-Review + +- Spec coverage: fills all README-advertised document paths except already existing files. +- Placeholder scan: plan contains no incomplete document instructions. +- Type consistency: file paths match README tree. diff --git a/docs/superpowers/plans/2026-05-29-state-sync-ui-settings-validation.md b/docs/superpowers/plans/2026-05-29-state-sync-ui-settings-validation.md new file mode 100644 index 0000000..ed1fd52 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-state-sync-ui-settings-validation.md @@ -0,0 +1,108 @@ +# State Sync and UI Settings Validation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Complete validation-backed state-sync evidence and UI settings persistence while updating DV documents. + +**Architecture:** State reducer work stays in `crates/chanora_state/src/lib.rs` with Rust unit tests. UI settings persistence stays in `apps/chanora_flutter/lib/services/ui_preferences_service.dart` with Flutter service tests; app-level theme application is wired in `apps/chanora_flutter/lib/main.dart` only if needed by the persisted setting. + +**Tech Stack:** Rust/cargo tests, Flutter/Dart, shared_preferences, Markdown documentation. + +--- + +### Task 1: State Reducer Validation + +**Files:** +- Modify: `crates/chanora_state/src/lib.rs` + +- [x] **Step 1: Write failing regression test** + +Add `channel_delete_removes_clients_in_deleted_channel` proving channel deletion removes clients assigned to that channel and emits client-removal deltas before the channel-removal delta. + +- [x] **Step 2: Verify RED** + +Run: `cargo test -p chanora_state channel_delete_removes_clients_in_deleted_channel --locked` + +Expected: FAIL because deleted-channel clients remain in state. + +- [x] **Step 3: Implement minimal reducer fix** + +In `StateEvent::ChannelDeleted`, collect clients whose `client.channel == id`, remove them from `clients` and `client_order`, then emit deterministic `ClientRemoved` deltas before `ChannelRemoved`. + +- [x] **Step 4: Verify GREEN** + +Run: `cargo test -p chanora_state channel_delete_removes_clients_in_deleted_channel --locked` + +Expected: PASS. + +- [x] **Step 5: Run full state crate tests** + +Run: `cargo test -p chanora_state --locked` + +Expected: all state crate tests pass. + +### Task 2: UI Settings Persistence + +**Files:** +- Modify: `apps/chanora_flutter/lib/services/ui_preferences_service.dart` +- Modify: `apps/chanora_flutter/test/services/ui_preferences_service_test.dart` +- Modify: `apps/chanora_flutter/lib/main.dart` + +- [x] **Step 1: Add failing tests for theme persistence** + +Add tests for default `system` theme mode, saving `dark`, saving `light`, and invalid stored value fallback to `system`. + +- [x] **Step 2: Verify RED** + +Run: `flutter test test/services/ui_preferences_service_test.dart` + +Expected: FAIL because `UiThemeMode`, `themeMode`, and `saveThemeMode` do not exist. + +- [x] **Step 3: Implement minimal service changes** + +Add `UiThemeMode`, `UiSettings.themeMode`, persisted key `ui.theme_mode`, and `saveThemeMode`. + +- [x] **Step 4: Verify GREEN** + +Run: `flutter test test/services/ui_preferences_service_test.dart` + +Expected: PASS. + +- [x] **Step 5: Wire app theme mode** + +Make `ChanoraApp` load persisted theme mode and pass `themeMode` into `MaterialApp`. + +- [x] **Step 6: Run focused Flutter tests** + +Run: `flutter test test/services/ui_preferences_service_test.dart` + +Expected: PASS. + +### Task 3: Documentation Updates + +**Files:** +- Modify: `docs/implementation-status-2026-05-28.md` +- Modify: `docs/release/dv-waiver-register.md` +- Modify: `docs/verification/swe4-unit-verification-plan.md` +- Modify: `docs/verification/swe6-software-verification-plan.md` +- Modify: `docs/architecture/sdd.md` + +- [x] **Step 1: Update implementation status** + +Mark `chanora_state` scaffold statement as superseded by reducer implementation/tests and mark UI settings persistence implemented for SharedPreferences scope. + +- [x] **Step 2: Update waiver and verification docs** + +Record reducer evidence and leave event replay as the remaining P1 state-sync gap. + +- [x] **Step 3: Verify docs** + +Run sentinel-language scan over edited docs. + +--- + +## Self-Review + +- Spec coverage: covers reducer evidence, UI settings persistence, and document updates. +- Placeholder scan: plan contains no incomplete implementation instructions. +- Type consistency: `UiThemeMode`, `themeMode`, and `saveThemeMode` names are used consistently. diff --git a/docs/superpowers/plans/2026-05-29-swe2-swe3-baselines.md b/docs/superpowers/plans/2026-05-29-swe2-swe3-baselines.md new file mode 100644 index 0000000..415f061 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-swe2-swe3-baselines.md @@ -0,0 +1,68 @@ +# SWE.2/SWE.3 Baselines Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add reviewable SWE.2/SAD and SWE.3/SDD baselines so the DV document chain is no longer missing the architecture and detailed design layers. + +**Architecture:** Keep SWE.2 in `docs/architecture/sad.md` and SWE.3 in `docs/architecture/sdd.md`, matching the README document tree. Update DV traceability and verification-plan wording to consume these baselines while preserving honest limitations for areas that still need deeper detail. + +**Tech Stack:** Markdown, existing SysDes/SRS baselines, current Flutter/Rust workspace structure. + +--- + +### Task 1: Write SWE.2 SAD Baseline + +**Files:** +- Create: `docs/architecture/sad.md` + +- [x] **Step 1: Create SAD with architecture views** + +Write sections for purpose, upstream sources, components, static view, runtime flows, interface catalogue, dependency rules, non-functional allocation, architectural decisions, verification handoff, traceability, and open architecture risks. + +### Task 2: Write SWE.3 SDD Baseline + +**Files:** +- Create: `docs/architecture/sdd.md` + +- [x] **Step 1: Create SDD with module designs** + +Write sections for purpose, upstream sources, module catalogue, detailed API/data/state design, persistence, diagnostics, platform adapters, build/release design, verification hooks, traceability, and open detailed-design risks. + +### Task 3: Update DV Traceability + +**Files:** +- Modify: `docs/governance/traceability-matrix.md` +- Modify: `docs/verification/verification-master-plan.md` + +- [x] **Step 1: Remove SAD/SDD missing limitation** + +Update traceability text so it says SAD and SDD baselines exist, with known depth limitations instead of missing-document limitations. + +- [x] **Step 2: Update verification plan inputs** + +Update verification master plan to reference SAD and SDD as current inputs for SWE.4/SWE.5. + +### Task 4: Verify SWE.2/SWE.3 Pack + +**Files:** +- Inspect created and updated docs. + +- [x] **Step 1: Sentinel-language scan** + +Run the sentinel-language scan over `docs/architecture`, `docs/governance`, and `docs/verification`. + +Expected: no matches introduced by this baseline pack. + +- [x] **Step 2: File presence check** + +Run: `ls docs/architecture` + +Expected: `sad.md` and `sdd.md` are listed. + +--- + +## Self-Review + +- Spec coverage: creates SAD and SDD baselines and updates DV traceability consumers. +- Placeholder scan: no incomplete instructions are present. +- Type consistency: document names match README paths. diff --git a/docs/superpowers/specs/2026-05-28-chanora-server-prefetch-crate-design.md b/docs/superpowers/specs/2026-05-28-chanora-server-prefetch-crate-design.md new file mode 100644 index 0000000..05e546f --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-chanora-server-prefetch-crate-design.md @@ -0,0 +1,116 @@ +# Chanora Server Prefetch Crate Design + +Date: 2026-05-28 + +## Goal + +Move server-resolution prefetch policy out of `chanora_core` into a focused Rust crate named `chanora_server_prefetch`, without changing connection behavior, Flutter APIs, or protocol dialing semantics. + +## Non-Goals + +- Do not change resolver behavior or DNS/SRV/TSDNS ordering. +- Do not change `ConnectConfig.resolved_address` semantics in `chanora_protocol`. +- Do not expose prefetch state in the UI. +- Do not prefetch bookmarks or additional hosts. +- Do not persist prefetched addresses. + +## Architecture + +Add a workspace member at `crates/chanora_server_prefetch`. + +Responsibilities: + +- Normalize server host keys by trimming and lowercasing. +- Track one active prefetch generation. +- Store at most one successful prefetched socket address. +- Reject stale async completions by generation. +- Return a prefetched address only for an exact normalized host match. +- Enforce the 2-minute freshness TTL. +- Resolve server addresses by calling `chanora_resolver::ChanoraResolver::resolve_client_address`. +- Log prefetch start, success, miss/stale, and failure diagnostics. + +Dependencies: + +- `chanora_resolver` for actual server address resolution. +- `tokio` for `Mutex` and spawned prefetch tasks. +- `tracing` for diagnostics. +- `thiserror` for a narrow `ServerPrefetchError` public error type. + +## Public API + +The crate exposes this small async owner type: + +```rust +pub struct ServerPrefetcher { ... } + +impl ServerPrefetcher { + pub fn new() -> Self; + pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError>; + pub async fn fresh_match(&self, host: &str) -> Option; +} +``` + +`prefetch` returns after scheduling work, preserving the current invisible, non-blocking behavior. Empty normalized hosts are ignored successfully. Resolution failures are stored only as diagnostics and do not affect connect semantics. + +## Core Integration + +`chanora_core` replaces its private prefetch cache fields and helpers with `ServerPrefetcher`. + +Core remains the trust boundary for connection config: + +- It clears any caller-provided `ConnectConfig.resolved_address` before lookup. +- It asks `ServerPrefetcher::fresh_match` for the current host. +- It sets `dial_cfg.resolved_address` only from a fresh exact cache hit. +- It stores supervisor/reconnect config with `resolved_address: None`. + +The Flutter bridge keeps calling the same core API, `prefetch_server_resolution(host)`. No Dart API change is intended. + +## Data Flow + +1. Flutter host editing schedules `ChanoraSession::prefetch_server_resolution(host)`. +2. Core delegates to `ServerPrefetcher::prefetch(host)`. +3. The prefetcher normalizes the host, increments generation, and spawns resolver work. +4. On success, the prefetcher stores the resolved socket address if the generation is still current. +5. On connect, core prepares `(stored_cfg, dial_cfg)`. +6. Core clears untrusted `resolved_address`, asks the prefetcher for a fresh exact match, and applies the result only to `dial_cfg`. +7. Protocol uses `dial_cfg.resolved_address` if present; otherwise it resolves normally. + +## Error Handling + +- Prefetch failures remain invisible to users. +- Prefetch failures are logged through `tracing`. +- If prefetch misses, is stale, or fails, connect falls back to normal protocol resolution. +- A caller-supplied `resolved_address` is never trusted by core. + +## Testing + +Move cache-policy tests from `chanora_core` into `chanora_server_prefetch`: + +- fresh exact match returns the socket address. +- stale entries are ignored. +- different hosts are ignored. +- stale generation completions are ignored. +- blank normalized hosts return `Ok(())` and do not update generation or spawn resolver work. + +Keep core tests for connection trust-boundary behavior: + +- untrusted `resolved_address` is cleared on cache miss. +- prepared stored/supervisor config has `resolved_address: None`. +- prepared dial config can receive a fresh prefetched address. + +Run at minimum: + +- `cargo test -p chanora_server_prefetch --lib` +- `cargo test -p chanora_core --lib` +- `cargo test -p chanora_protocol --lib` + +For final confidence, rerun the Android server connect smoke path that verifies prefetch logs and connected UI. + +## Acceptance Criteria + +- Workspace builds with the new crate member. +- `chanora_core` no longer owns the prefetch cache implementation. +- `chanora_server_prefetch` owns prefetch normalization, TTL, generation, storage, and resolver-backed warming. +- Public Flutter and Rust protocol behavior is unchanged. +- Existing server connect and reconnect safety tests pass. +- Android connect still reaches the connected server UI and does not get stuck in `Connecting` or `Synchronizing`. diff --git a/docs/superpowers/specs/2026-05-28-server-resolution-prefetch-design.md b/docs/superpowers/specs/2026-05-28-server-resolution-prefetch-design.md new file mode 100644 index 0000000..1f10cab --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-server-resolution-prefetch-design.md @@ -0,0 +1,203 @@ +# Server Resolution Prefetch Design + +Date: 2026-05-28 + +## Purpose + +Reduce perceived server join latency by resolving the active TeamSpeak server address before the user taps Connect. Prefetch must be invisible, conservative, and safe: it may warm resolver state, but it must not change connection semantics or surface background errors to the user. + +Recent Android testing showed resolver latency can dominate the first part of the connect flow. A prior fix bounded slow TS3 SRV discovery and removed Android's forced Cloudflare resolver. Prefetch builds on that by hiding remaining address-resolution work when the user has already entered or loaded a likely server address. + +## Goals + +- Prefetch only the active server address field. +- Keep the feature invisible to users. +- Reuse prefetched results only for exact normalized host matches. +- Keep prefetched results fresh for 2 minutes. +- Preserve today's Connect behavior when prefetch misses, fails, or is stale. +- Avoid prefetching all bookmarks. +- Avoid opening a TS3 session before the user taps Connect. + +## Non-Goals + +- No visible resolving, ready, or failed UI state. +- No bookmark fan-out prefetch. +- No persisted resolver cache across app launches. +- No password, channel, or permission validation during prefetch. +- No server reachability probe beyond address resolution. +- No connection warm-up or pre-authentication. + +## Chosen Approach + +Use a Rust-owned resolver prefetch cache with Flutter-owned scheduling. + +Flutter knows when the active host field changes, so it schedules prefetch requests. Rust owns resolver correctness, normalization, cache validity, and connect-time reuse. This keeps Flutter from depending on resolver internals and ensures Connect can independently decide whether a prefetched result is safe to use. + +Other approaches considered: + +- Flutter-only prefetch: rejected because it pushes resolver state into Dart and creates a weaker boundary between UI and connection behavior. +- Resolver-internal repeated-call cache only: rejected because it does not hide first-click latency from the active host field. + +## Behavior + +Prefetch starts for the active host value in two cases: + +- After `_loadUiSettings()` loads the last-used host into `_hostCtl`. +- After the user stops editing the host field for about 700 ms. + +The feature is invisible: + +- No SnackBars. +- No inline status text. +- No disabled Connect button. +- No user-facing error if prefetch fails. + +Connect behavior: + +- If the current normalized host exactly matches a fresh prefetched entry, Connect uses the cached resolved address. +- If the cache is missing, stale, failed, or for a different host, Connect resolves normally. +- Connect remains the only operation that opens a TS3 session. + +## Architecture + +### Flutter Scheduling + +`_BetaHomeState` owns the host text field. It should add a listener to `_hostCtl` and manage a short debounce timer. + +Responsibilities: + +- Trim the host input before scheduling. +- Skip empty values. +- Reset the debounce timer on each edit. +- Call a bridge prefetch API after about 700 ms of idle typing. +- Schedule one prefetch after settings load if the loaded host is non-empty. +- Dispose the listener and timer with the widget state. + +Flutter does not store resolved addresses and does not decide whether Connect can use a prefetched result. + +### Bridge API + +Add a fire-and-forget bridge function shaped like: + +```text +prefetch_server_resolution(host: String) -> Result<(), BridgeError> +``` + +The bridge call should return after the prefetch task has been accepted by the Rust runtime. It must not wait for resolution to complete. Background completion or failure is reported only through diagnostics/logging. + +### Rust Resolver Cache + +Rust stores a small prefetch cache owned near the session/resolver boundary. A single latest-host entry is enough for v1, because the design only prefetches the active field. + +Cache entry fields: + +- Normalized input host. +- Resolved `host:port` address. +- Resolution method. +- Completion timestamp. +- Generation or request id. +- Optional sanitized failure metadata for diagnostics. + +The cache TTL is 2 minutes. + +### Connect Integration + +Connect should ask Rust for a fresh exact-match prefetched result before running normal resolution. + +Rules: + +- Exact normalized host match is required. +- Entry age must be at most 2 minutes. +- Failed entries must not block normal connect resolution. +- Stale entries must be ignored. +- Missing cache must behave exactly like today. + +## Data Flow + +1. App starts. +2. `_loadUiSettings()` loads the last-used host into `_hostCtl`. +3. Flutter schedules invisible prefetch for that host. +4. User edits the host field. +5. Flutter cancels the pending debounce timer and starts a new one. +6. After 700 ms idle, Flutter calls Rust prefetch with the latest trimmed host. +7. Rust normalizes and resolves the host through the same resolver path used by Connect. +8. Rust stores the result if it still matches the latest generation for that normalized host. +9. User taps Connect. +10. Rust Connect checks the cache for a fresh exact-match result. +11. Cache hit: Connect uses the prefetched address. +12. Cache miss/stale/failure: Connect resolves normally. + +## Cancellation And Staleness + +Cancellation can be logical rather than hard task cancellation. + +- Flutter prevents obsolete debounce timers from firing. +- Rust tags requests by normalized host and generation. +- Late completions for stale generations must not replace newer successful entries. +- Duplicate prefetches for the same normalized host may coalesce or refresh the same entry. + +This avoids complexity while preventing old input values from poisoning the cache. + +## Error Handling + +Prefetch failures are diagnostic-only. + +- Empty host: skip prefetch. +- Invalid host shape: skip or fail silently with debug diagnostics. +- Resolver failure: store optional sanitized failure metadata for diagnostics only. +- Connect after failure: normal connect path runs and surfaces errors as it does today. +- App resume and network changes: no special invalidation in v1; TTL handles staleness. +- Disconnect: cache may remain because it is independent of the TS3 session. + +## Diagnostics + +Add privacy-safe logs for: + +- Prefetch started. +- Prefetch result. +- Prefetch failed. +- Connect using prefetched resolution. +- Connect prefetch miss or stale entry. + +Do not log passwords, channel passwords, or nickname. Host and resolved address are acceptable because resolver/connect logging already includes them today. + +## Testing + +Rust tests: + +- Fresh exact-match prefetched result is reusable. +- Stale prefetched result is ignored. +- Different normalized host is ignored. +- Failed prefetch does not block normal resolution. +- Late stale generation cannot overwrite a newer cache entry. + +Flutter tests should cover the scheduling logic through a small testable helper if wiring directly through `_BetaHomeState` would be brittle: + +- Host edits debounce prefetch scheduling. +- Empty host does not prefetch. +- Settings-loaded host schedules one prefetch. + +Manual Android smoke test: + +- Install debug APK. +- Launch app. +- Wait for last-used host prefetch or type host and wait past debounce. +- Tap Connect. +- Confirm UI reaches connected server view. +- Confirm logcat shows either a prefetch cache hit or safe fallback behavior. + +## Acceptance Criteria + +- Typing or loading a valid host can warm resolver state before Connect. +- Connect never fails because prefetch failed. +- Connect never uses a prefetched result for a different normalized host. +- Prefetched entries older than 2 minutes are ignored. +- No visible UI is added for prefetch state. +- Bookmarks are not prefetched in bulk. +- Android debug build and focused resolver/Flutter tests pass. + +## Implementation Notes + +- Prefer a single latest-host cache unless implementation reveals an existing cache abstraction that makes a tiny map simpler. +- Prefer minimal bridge API surface: one prefetch call and connect-time internal cache lookup. +- Keep the resolver cache near existing Rust session/connect code so future non-Flutter clients can benefit from the same behavior. diff --git a/docs/superpowers/specs/2026-05-29-state-sync-ui-settings-validation-design.md b/docs/superpowers/specs/2026-05-29-state-sync-ui-settings-validation-design.md new file mode 100644 index 0000000..0a03681 --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-state-sync-ui-settings-validation-design.md @@ -0,0 +1,70 @@ +# State Sync and UI Settings Validation Design + +**Date:** 2026-05-29 +**Status:** Approved for implementation +**Scope:** P0/P1 validation-based completion for state-sync evidence and UI settings persistence + +## 1. Goal + +Close the current DV/P0-P1 gaps for reducer/state-sync evidence and UI settings persistence with tests first, minimal behavior changes, and updated documentation evidence. + +## 2. State-Sync Design + +`chanora_state` remains the reducer owner. The validation pass adds focused tests for known reducer contracts rather than broad refactoring. Missing behavior is implemented only when a test proves a gap. + +Required evidence covers: + +| Contract | Evidence | +|---|---| +| Snapshot creates ready state and deterministic normalized order | Existing and expanded reducer tests | +| Reconnect discards stale state and reconnect snapshot replaces state | Existing reducer tests | +| Disconnected/lost states suppress live deltas | Existing reducer tests | +| Duplicate IDs are normalized deterministically | Existing reducer tests | +| Unknown client voice activity is ignored | Existing reducer tests | +| Channel deletion removes clients in deleted channel | New reducer regression test and implementation | +| Same event sequence produces same state and deltas | Existing reducer determinism test | + +## 3. UI Settings Design + +`UiPreferencesService` remains a Flutter service backed by `shared_preferences`. This is the minimal P0/P1-complete implementation because the current app already uses SharedPreferences and no current behavior requires SQLite-backed UI settings. + +`UiSettings` gains a typed `themeMode` field with values: + +| Value | Meaning | +|---|---| +| `system` | Follow platform theme | +| `light` | Force light theme | +| `dark` | Force dark theme | + +The service persists the selected theme mode, falls back to `system` for invalid stored values, and preserves independent saves for host and nickname. + +## 4. App Wiring + +`ChanoraApp` becomes stateful enough to load and apply persisted theme mode. `_BetaHome` continues to load/save host and nickname through `UiPreferencesService`. UI controls for selecting theme mode are out of this slice unless already present; this slice provides persistence and app-level application. + +## 5. Documentation Updates + +After tests pass: + +| Document | Update | +|---|---| +| `docs/implementation-status-2026-05-28.md` | Mark reducer scaffold statement stale/resolved and UI settings persistence implemented for SharedPreferences scope | +| `docs/release/dv-waiver-register.md` | Close or soften reducer waiver; keep event replay as P1 gap | +| `docs/verification/swe4-unit-verification-plan.md` | Record reducer test evidence and UI settings tests | +| `docs/verification/swe6-software-verification-plan.md` | Update state sync and UI settings DV status | +| `docs/architecture/sdd.md` | Record UI settings persistence design | + +## 6. Validation + +Run focused tests: + +```text +cargo test -p chanora_state --locked +flutter test test/services/ui_preferences_service_test.dart +``` + +Run wider checks if touched app-shell behavior requires it: + +```text +flutter test --exclude-tags e2e +``` diff --git a/docs/additional_docs/sysdes.md b/docs/sysdes.md similarity index 100% rename from docs/additional_docs/sysdes.md rename to docs/sysdes.md diff --git a/docs/additional_docs/sysrs.md b/docs/sysrs.md similarity index 100% rename from docs/additional_docs/sysrs.md rename to docs/sysrs.md diff --git a/docs/ts3_development_manual_en_v6.md b/docs/ts3_development_manual_en_v6.md deleted file mode 100644 index 3226008..0000000 --- a/docs/ts3_development_manual_en_v6.md +++ /dev/null @@ -1,4616 +0,0 @@ -# TeamSpeak 3 Development Manual - -**Self-contained engineering manual based on YaTQA resources and the ReSpeak repositories** - -Version: 1.1 - v4 coverage audit edition -Language: English -Scope: TeamSpeak 3 server administration tooling, ServerQuery clients, TS3-compatible client/bot development, identity tooling, statistics processing, and migration/backup automation. - - -> v6 update: Appendix D performs a full link-by-link coverage audit of the German YaTQA resources page. Section 15 includes the complete YaTQA How-to-DNS decision chart as native text, tables, and structured decision rules only - no PNG or raster flowchart is required. Section 16 includes the expanded details from YaTQA's `protokolle` page: Protobuf, updates, file transfer, TSDNS, server nicknames, DNS/SRV behavior, Blacklist/Blacklist2, Weblist, badges, and the historical TLD appendix. - - -## 1. Purpose and Scope - -This manual consolidates the technical knowledge needed to build TeamSpeak 3 development tools without repeatedly returning to the original websites. It is based on: - -- YaTQA resource documents, especially the German technical pages under `https://yat.qa/ressourcen/` and the English pages under `https://yat.qa/resources/`. -- ReSpeak repositories: - - `ReSpeak/tsclientlib` - - `ReSpeak/ts3stats` - - `ReSpeak/tsdeclarations` - - `ReSpeak/Qint` - - `ReSpeak/MahTsIdentity` -- Official TeamSpeak 3 support articles for operational baseline information such as ports, snapshots, DNS SRV, and server setup. - -The document is written for developers who want to implement one or more of the following: - -1. A ServerQuery administration system. -2. A TeamSpeak 3-compatible bot or custom client. -3. A typed command and event schema for TS3. -4. Snapshot backup, migration, and recovery tooling. -5. Identity generation, vanity UID search, or security-level improvement. -6. Statistics reports from TS3 and TS3AudioBot logs. -7. A desktop client architecture similar to Qint. - -This manual is intentionally practical. It provides algorithms, protocol behavior, implementation architecture, schema examples, pseudocode, and troubleshooting guidance. - -### 1.1 Important limitations - -Some source areas are inherently incomplete: - -- YaTQA explicitly notes that it does not provide a complete ClientQuery document. -- Several YaTQA tools are old downloads; some may no longer be reachable or useful on modern systems. -- Official TS3 plugin documentation is mostly distributed through the TeamSpeak 3 Plugin SDK package, not as a complete public web manual. -- ReSpeak projects are open source references, but not all of them are actively maintained or suitable as production dependencies without review. - -Where public details are thin, this manual states that explicitly rather than inventing behavior. - - -*** - - -## 2. Source Inventory - -### 2.1 YaTQA resources - -The YaTQA resources page is the strongest public source for undocumented or poorly documented TeamSpeak 3 behavior. The relevant resources are: - -| Resource | Primary development value | -|---|---| -| Web-based tools | UID, avatar filename, icon ID, and cache folder conversions | -| Backup without snapshot permissions | Practical migration when snapshot rights are missing | -| Server error codes | Query error dictionary and error classification | -| Permission IDs | Permission constants, grant IDs, and historical numbering behavior | -| Client versions | Historical TeamSpeak 3 client build numbers and timestamps | -| Definitions and algorithms | Codecs, permission calculation, Query connection details, icon/avatar/cache algorithms, snapshots, BBCode | -| Server Query comments | Corrections and behavior notes for ServerQuery commands | -| Server Query notify | Event subscription behavior and event field lists | -| Variable parameters | Which client/channel/server variables are visible or editable in which commands | -| Voice client anti-flood | Anti-flood points, decay, and command costs | -| Security level | Identity proof-of-work calculation and optimization notes | -| Snapshots | Snapshot text format, included data, missing data, and hash rules | -| Other protocols | Protobuf, update protocol, file transfer, TSDNS, DNS, blacklist, weblist, badges | -| Badge list | Badge GUIDs, asset naming, and metadata patterns | - -### 2.2 ReSpeak repositories - -| Repository | Role | -|---|---| -| `tsdeclarations` | Machine-readable protocol, message, error, permission, version, and badge declarations. Best source for code generation. | -| `tsclientlib` | Rust runtime library for TeamSpeak 3-compatible clients and bots. Contains protocol, state, and example usage. | -| `Qint` | Modern alternative TeamSpeak client reference using Tauri/frontend/backend architecture and `tsclientlib`. | -| `ts3stats` | Offline statistics pipeline for TeamSpeak 3 and TS3AudioBot logs. | -| `MahTsIdentity` | Identity export, vanity UID search, and security-level improvement tool using Rust parallelism. | - - -*** - - -## 3. System Architecture for a TS3 Development Stack - -A complete TS3 tooling ecosystem should be split into layers: - -```text -+--------------------------------------------------------------+ -| Applications | -| - Admin web UI | -| - Desktop client | -| - Bot | -| - Stats report generator | -| - Identity tool | -+-------------------------+------------------------------------+ -| Domain services | Query/event/snapshot/permission | -| | identity/DNS/file-transfer modules | -+-------------------------+------------------------------------+ -| Protocol implementation | ServerQuery parser and builder | -| | UDP packet protocol | -| | Protobuf, Base64, hashes, escaping | -+-------------------------+------------------------------------+ -| Transport | TCP Query, SSH Query, UDP voice, | -| | file transfer TCP, HTTP(S), DNS | -+-------------------------+------------------------------------+ -| Data declarations | Errors, permissions, messages, | -| | versions, badges, enums | -+--------------------------------------------------------------+ -``` - -Recommended module layout for a new implementation: - -```text -ts3-toolkit/ - crates or packages/ - ts3-types/ # IDs, enums, errors, permission constants - ts3-query/ # ServerQuery protocol parser/builder - ts3-events/ # Notify event parser and state reducer - ts3-permissions/ # Permission evaluation engine - ts3-snapshots/ # Snapshot parser/writer/rehash - ts3-identity/ # Identity import/export/security level - ts3-protocol/ # UDP packet protocol, protobuf, crypto hooks - ts3-files/ # File-transfer token handling and file ops - ts3-dns/ # SRV/TSDNS resolution - ts3-stats/ # Log ingestion and reporting - ts3-admin-app/ # UI/API application layer -``` - -The most robust strategy is: - -1. Generate or centralize constants from `tsdeclarations`-style data. -2. Implement ServerQuery as a strongly typed command system. -3. Implement Query event subscriptions and state reducers. -4. Implement permission evaluation separately from display logic. -5. Implement snapshots and file migration separately because snapshots are not full backups. -6. Add identity and security-level tooling as an offline worker. -7. Add stats as a batch pipeline rather than as a core runtime dependency. - - -*** - - -## 4. Server Setup and Operational Baseline - -### 4.1 Default ports - -A development environment should account for these default TeamSpeak 3 ports: - -| Purpose | Protocol | Default port | -|---|---:|---:| -| Voice traffic | UDP | 9987 | -| File transfer | TCP | 30033 | -| Raw ServerQuery | TCP | 10011 | -| ServerQuery over SSH | TCP | 10022 | -| WebQuery HTTP | TCP | 10080 | -| WebQuery HTTPS | TCP | 10443 | -| TSDNS | TCP | 41144 | - -For minimal local development, voice + ServerQuery are usually enough: - -```bash -# Start a TS3 server after accepting the license -export TS3SERVER_LICENSE=accept -./ts3server -``` - -On first startup, capture: - -- `serveradmin` password. -- Initial virtual server admin token. -- Server ID (`sid`) and port. - -Use the token from a normal TS3 client via the Permissions menu to make the first admin identity. - -### 4.2 ServerQuery bootstrap - -A minimal raw ServerQuery session: - -```text -login serveradmin YOUR_PASSWORD -use sid=1 -virtual -servernotifyregister event=server -servernotifyregister event=channel id=0 -``` - -Important YaTQA behavior note: use `-virtual` when selecting the server. Older documentation implied that `use` may automatically start a virtual server. YaTQA observed that it does not reliably do so; using `-virtual` is the safe practice and has no downside when the server is already running. - -### 4.3 Recommended development configuration - -```yaml -server: - host: 127.0.0.1 - voice_port: 9987 - filetransfer_port: 30033 - query_port: 10011 - query_ssh_port: 10022 - server_id: 1 -query: - username: serveradmin - password_env: TS3_QUERY_PASSWORD - use_virtual: true - command_delay_ms: 350 - line_ending: "LF_CR" -subscriptions: - - event: server - - event: channel - id: 0 -logging: - commands: true - responses: true - events: true -security: - never_log_query_passwords: true - never_log_identity_private_keys: true -``` - -The 350 ms delay is conservative but practical. YaTQA recommends a 350 ms command delay for large backup jobs to avoid anti-flood bans. For your own tools, implement adaptive pacing rather than hard-coding one global delay. - - -*** - - -## 5. ServerQuery Protocol - -### 5.1 Transport and line endings - -Raw ServerQuery is a TCP text protocol. A subtle but important point from YaTQA is that line endings use LF-CR (`0x0A 0x0D`), not normal CR-LF. - -```python -EOL = b"\n\r" -``` - -Always send `quit` before closing a Query socket so that the server does not treat the disconnect as an abnormal timeout. - -### 5.2 Command format - -A command consists of: - -```text -command [parameters] [options] -``` - -Examples: - -```text -serverlist -serverinfo -clientlist -uid -groups -voice -serveredit virtualserver_name=My\sServer virtualserver_maxclients=64 -``` - -Parameter escaping is mandatory. Typical Query escaping rules include: - -| Raw character | Escaped form | -|---|---| -| Space | `\s` | -| Slash | `\/` | -| Backslash | `\\` | -| Pipe | `\p` | -| Bell | `\a` | -| Backspace | `\b` | -| Form feed | `\f` | -| Newline | `\n` | -| Carriage return | `\r` | -| Tab | `\t` | -| Vertical tab | `\v` | - -Implementation skeleton: - -```python -ESCAPE = { - "\\": "\\\\", - "/": "\\/", - " ": "\\s", - "|": "\\p", - "\a": "\\a", - "\b": "\\b", - "\f": "\\f", - "\n": "\\n", - "\r": "\\r", - "\t": "\\t", - "\v": "\\v", -} - -UNESCAPE = {v: k for k, v in ESCAPE.items()} - -def ts3_escape(value: str) -> str: - return "".join(ESCAPE.get(ch, ch) for ch in value) -``` - -### 5.3 Response format - -Most Query responses contain zero or more rows followed by an error line: - -```text -clid=5 client_nickname=Alice -error id=0 msg=ok -``` - -Rows may be separated by `|`: - -```text -clid=1 client_nickname=Alice|clid=2 client_nickname=Bob -error id=0 msg=ok -``` - -Parser strategy: - -1. Read until an `error id=...` line. -2. Split data lines by unescaped `|`. -3. Split row fields by unescaped spaces. -4. Split key/value by the first `=`. -5. Unescape values. -6. Parse error ID and message separately. - -Do not assume the response has data. Many successful commands return only `error id=0 msg=ok`. - -### 5.4 Return codes - -YaTQA notes that commands can include `return_code`. The server echoes it in the error line, allowing clients to correlate responses when sending multiple commands concurrently. - -Example: - -```text -serverinfo return_code=req42 -error id=0 msg=ok return_code=req42 -``` - -Recommendation: build your Query client around a request ID system even if you initially send commands serially. - -### 5.5 Query implementation skeleton - -```python -import socket -import time - -class Ts3QueryClient: - def __init__(self, host, port=10011, delay=0.35): - self.host = host - self.port = port - self.delay = delay - self.sock = None - - def connect(self): - self.sock = socket.create_connection((self.host, self.port), timeout=10) - self._read_until_prompt_or_banner() - - def close(self): - if self.sock: - self.sock.sendall(b"quit\n\r") - self.sock.close() - - def command(self, name, **params): - line = self._build_command(name, params) - self.sock.sendall(line.encode("utf-8") + b"\n\r") - time.sleep(self.delay) - return self._read_response() - - def _build_command(self, name, params): - parts = [name] - for key, value in params.items(): - if value is True: - parts.append(f"-{key}") - elif value is False or value is None: - continue - else: - parts.append(f"{key}={ts3_escape(str(value))}") - return " ".join(parts) -``` - -### 5.6 Key Query commands - -#### Login and server selection - -```text -login serveradmin PASSWORD -use sid=1 -virtual -``` - -YaTQA note: if `login` fails for a non-trivial reason, an already logged-in session may be logged out. - -#### Virtual server creation - -```text -servercreate virtualserver_name=Test\sServer virtualserver_port=9988 virtualserver_maxclients=32 -``` - -Return values typically include server ID, port, and initial token. Store the token securely. - -#### Virtual server deletion - -```text -serverdelete sid=2 -``` - -#### Server editing - -```text -serveredit virtualserver_name=New\sName virtualserver_maxclients=64 -``` - -Common editable server fields include: - -- `virtualserver_name` -- `virtualserver_welcomemessage` -- `virtualserver_maxclients` -- `virtualserver_password` -- `virtualserver_hostmessage` -- `virtualserver_hostmessage_mode` -- `virtualserver_hostbanner_url` -- `virtualserver_hostbanner_gfx_url` -- `virtualserver_hostbanner_gfx_interval` -- `virtualserver_hostbutton_tooltip` -- `virtualserver_hostbutton_url` -- `virtualserver_hostbutton_gfx_url` -- `virtualserver_download_quota` -- `virtualserver_upload_quota` -- `virtualserver_max_download_total_bandwidth` -- `virtualserver_max_upload_total_bandwidth` -- `virtualserver_antiflood_points_tick_reduce` -- `virtualserver_antiflood_points_needed_command_block` -- `virtualserver_antiflood_points_needed_ip_block` -- `virtualserver_needed_identity_security_level` -- log toggles such as `virtualserver_log_client`, `virtualserver_log_query`, etc. - -#### Token / privilege key commands - -Historical command names may vary: - -- `privilegekeyadd` -- `privilegekeylist` -- `privilegekeydelete` -- `privilegekeyuse` - -Aliases documented by YaTQA include token-style names: - -- `tokenadd` -- `tokenlist` -- `tokendelete` -- `tokenuse` - -Example: - -```text -privilegekeyadd tokentype=0 tokenid1=6 tokenid2=0 tokendescription=Admin\sToken -``` - -`tokencustomset` is nested and must be escaped carefully. Treat it as an encoded key-value string embedded inside another key-value command. - - -*** - - -## 6. Error Handling - -Do not expose generic `Query failed` errors. Build a central error dictionary. Important examples from the YaTQA server error code table: - -| Decimal | Hex | Meaning | Typical fix | -|---:|---:|---|---| -| 0 | 0x0000 | ok | No action | -| 256 | 0x0100 | command not found | Check command name and server version | -| 512 | 0x0200 | invalid clientID | Refresh `clid`; do not persist session-scoped client IDs | -| 513 | 0x0201 | nickname in use | Choose another nickname | -| 520 | 0x0208 | invalid login or password | Check Query credentials | -| 522 | 0x020A | client version outdated | Update client or check compatibility | -| 524 | 0x020C | client is flooding | Slow down commands; check anti-flood settings | -| 768 | 0x0300 | invalid channelID | Refresh channel tree | -| 773 | 0x0305 | cannot delete default channel | Change default channel first | -| 1027 | 0x0403 | server max clients reached | Increase slots or wait | -| 1538 | 0x0602 | invalid parameter | Validate parameter name and type | -| 1540 | 0x0604 | convert error | Check numeric formats and signed/unsigned conversions | -| 2568 | 0x0A08 | insufficient client permissions | Inspect failed permission ID | -| 2572 | 0x0A0C | permission error | Check permission power/needed power | - -Implementation recommendation: - -```python -class Ts3Error(Exception): - def __init__(self, code, msg, failed_permid=None): - super().__init__(f"TS3 error {code}: {msg}") - self.code = code - self.msg = msg - self.failed_permid = failed_permid -``` - -When `failed_permid` is present, map it back to a permission name using your permission table. - - -*** - - -## 7. Notify Events and Event-Driven State - -### 7.1 Subscribing to events - -`servernotifyregister` subscribes a Query client to events. - -Examples: - -```text -servernotifyregister event=server -servernotifyregister event=channel id=0 -servernotifyregister event=textserver -servernotifyregister event=textchannel -servernotifyregister event=textprivate -servernotifyregister event=tokenused -``` - -Important behavior: - -- Subscriptions are lost on logout. -- Subscriptions are lost when switching to another server. -- Each event must be subscribed separately. -- `event=channel` requires `id`. -- `id=0` for channel events means all existing and future channels. -- Some event combinations cause duplicate events. Deduplicate in your state reducer. - -### 7.2 Common event types - -#### `notifycliententerview` - -Used when a client enters view, connects, or becomes visible. - -Important fields include: - -- `cfid` - source channel ID, often 0 on server entry. -- `ctid` - target channel ID. -- `reasonid` - reason code. -- `clid` - session-scoped client ID. -- `client_unique_identifier` -- `client_nickname` -- `client_database_id` -- `client_channel_group_id` -- `client_servergroups` -- `client_type` - 0 voice, 1 query. -- `client_flag_avatar` -- `client_talk_power` -- `client_icon_id` -- `client_country` -- `client_badges` - -State reducer action: - -```python -def on_client_enter(state, event): - clid = int(event["clid"]) - state.clients[clid] = event - if "ctid" in event: - state.client_channel[clid] = int(event["ctid"]) -``` - -#### `notifyclientleftview` - -Used when a client leaves, is kicked, is banned, or becomes invisible. - -Important fields: - -- `cfid` -- `ctid` - often 0 when leaving server. -- `reasonid` -- `invokerid` -- `invokername` -- `invokeruid` -- `reasonmsg` -- `bantime` -- `clid` - -#### `notifyclientmoved` - -Used for client channel movement. - -Important fields: - -- `clid` -- `ctid` -- `cfid` -- `reasonid` -- invoker fields when applicable. - -Deduplicate if the same move arrives from multiple subscriptions. - -#### `notifyserveredited` - -Includes changed server fields plus invoker metadata. YaTQA notes that not every changed server property necessarily appears in the event payload. - -#### `notifychanneledited`, `notifychannelcreated`, `notifychanneldeleted`, `notifychannelmoved` - -Use these to maintain a local channel tree. - -#### `notifytextmessage` - -Text message event. Distinguish server, channel, and private subscriptions. - -#### `notifytokenused` - -Useful when an external system provisions privilege keys and needs to detect consumption. - -### 7.3 State model - -A robust state model: - -```python -class Ts3State: - def __init__(self): - self.server = {} - self.channels = {} # cid -> channel object - self.clients = {} # clid -> client object - self.client_channel = {} # clid -> cid - self.groups = {} - self.permissions = {} -``` - -Do not store `clid` as a persistent user identifier. It is session-scoped. Use: - -- `client_unique_identifier` for identity-level persistence. -- `client_database_id` for server-local account persistence. -- `clid` only for currently connected sessions. - - -*** - - -## 8. Permission System - -### 8.1 Permission IDs and grant IDs - -TeamSpeak permission IDs changed historically. YaTQA explains that older versions encoded hierarchy in hex digits, but newer permission versions use mostly sequential IDs. - -Grant IDs are calculated by adding `32768` to the base permission ID in the modern scheme. - -```python -def grant_permission_id(permission_id: int) -> int: - return permission_id + 32768 -``` - -Example: - -```text -base permission: i_channel_join_power -grant permission: i_needed_modify_power_channel_join_power or base_id + 32768 -``` - -Always query current server permissions with `permissionlist` or use versioned declaration data. Do not hard-code old IDs if the server version is unknown. - -### 8.2 Permission sources - -YaTQA describes effective permissions as a per-permission evaluation over multiple sources. The sources are, from broad to specific: - -1. Server groups. -2. Client permissions at server level. -3. Channel permissions, unless skipped. -4. Channel group permissions, unless skipped. -5. Client permissions at channel level. - -Each permission is evaluated independently. - -### 8.3 Skip and negate behavior - -Important flags: - -- `skip` - causes lower channel/channel-group layers to be skipped. -- `negate` - causes a permission from one group to override other group contributions. - -Pseudo-evaluation: - -```python -def evaluate_permission(user, channel, perm): - candidates = [] - - # 1. server group permissions - sg_perms = collect_server_group_permissions(user.server_groups, perm) - sg_effective = merge_group_permissions(sg_perms) - if sg_effective: - candidates.append(sg_effective) - - # 2. direct client permission on server - client_server_perm = get_client_server_permission(user.dbid, perm) - if client_server_perm: - candidates.append(client_server_perm) - - skip_channel_layers = determine_skip_flag(sg_effective, client_server_perm) - - if not skip_channel_layers: - # 3. channel permission - ch_perm = get_channel_permission(channel.cid, perm) - if ch_perm: - candidates.append(ch_perm) - - # 4. channel group permission - cg_perm = get_channel_group_permission(user.channel_group[channel.cid], perm) - if cg_perm: - candidates.append(cg_perm) - - # 5. direct client permission on channel - client_channel_perm = get_client_channel_permission(user.dbid, channel.cid, perm) - if client_channel_perm: - candidates.append(client_channel_perm) - - return most_specific_applicable(candidates) -``` - -### 8.4 Power and needed power - -Many actions use a power/needed-power pair: - -- Actor has `i_*_power`. -- Target or object requires `i_*_needed_power`. -- Action succeeds only if actor power >= needed power. - -Examples: - -- `i_client_kick_from_server_power` vs. target required kick power. -- `i_channel_join_power` vs. `i_channel_needed_join_power`. -- `i_group_member_add_power` vs. group needed member add power. - -### 8.5 Upgrade behavior - -TeamSpeak permission upgrades may depend on metadata permissions such as `i_group_auto_update_type`. If you customize template or default groups, verify that permission upgrades still work as intended. - -### 8.6 Recommended implementation - -Build a permission engine as a standalone library with: - -- Versioned permission constants. -- Grant ID mapping. -- Permission assignment records. -- Effective permission evaluator. -- Explanation trace. - -An explanation trace is critical for admin UI: - -```json -{ - "permission": "i_channel_join_power", - "effective_value": 75, - "source": "server_group", - "group_id": 6, - "skip": false, - "negate": false, - "required": 50, - "allowed": true -} -``` - - -*** - - -## 9. Variable Parameters - -YaTQA's variable-parameters document is effectively a matrix showing which values appear in which commands and which values are editable. - -### 9.1 Client variables - -Common client fields: - -| Field | Meaning | Typical commands | -|---|---|---| -| `clid` | Session-scoped client ID | `clientlist`, notify events | -| `cid` | Current channel ID | `clientlist`, notify events | -| `client_unique_identifier` | Stable identity UID | `clientlist -uid`, `clientinfo` | -| `client_database_id` | Server-local client DB ID | `clientinfo`, `clientdbinfo` | -| `client_nickname` | Display nickname | `clientlist`, `clientinfo`, `clientupdate`, `clientedit` | -| `client_version` | Client version string | `clientinfo` | -| `client_platform` | Platform string | `clientinfo` | -| `client_input_muted` | Microphone muted state | voice/client state | -| `client_output_muted` | Output muted state | voice/client state | -| `client_away` | Away flag | `clientupdate` | -| `client_away_message` | Away message | `clientupdate` | -| `client_flag_avatar` | Avatar hash marker | `clientinfo` | -| `client_icon_id` | Icon ID | `clientinfo` | -| `client_badges` | Badge metadata string | notify events / client info | - -### 9.2 Channel variables - -Common channel fields: - -| Field | Meaning | -|---|---| -| `cid` | Channel ID | -| `pid` | Parent channel ID | -| `channel_name` | Channel name | -| `channel_topic` | Topic | -| `channel_description` | Description | -| `channel_password` | Password | -| `channel_codec` | Codec | -| `channel_codec_quality` | Codec quality | -| `channel_maxclients` | Channel max clients | -| `channel_maxfamilyclients` | Family max clients | -| `channel_order` | Sort order | -| `channel_flag_permanent` | Permanent channel flag | -| `channel_flag_semi_permanent` | Semi-permanent channel flag | -| `channel_flag_temporary` | Temporary flag | -| `channel_needed_join_power` | Required join power | -| `channel_icon_id` | Channel icon | - -### 9.3 Server variables - -Common server fields: - -| Field | Meaning | -|---|---| -| `virtualserver_name` | Server display name | -| `virtualserver_unique_identifier` | Server UID | -| `virtualserver_port` | Voice port | -| `virtualserver_maxclients` | Slot count | -| `virtualserver_password` | Password | -| `virtualserver_welcomemessage` | Welcome text | -| `virtualserver_codec_encryption_mode` | Codec encryption mode | -| `virtualserver_hostmessage` | Host message | -| `virtualserver_hostmessage_mode` | Host message display mode | -| `virtualserver_antiflood_points_tick_reduce` | Anti-flood decay per tick | -| `virtualserver_antiflood_points_needed_command_block` | Command block threshold | -| `virtualserver_antiflood_points_needed_ip_block` | IP block threshold | -| `virtualserver_needed_identity_security_level` | Required identity security level | - -Implementation rule: visibility does not imply mutability. Use a command declaration table to validate edit operations. - - -*** - - -## 10. Anti-Flood System - -YaTQA documents the voice-client and ClientQuery anti-flood system as a point budget. - -### 10.1 Core algorithm - -Each client has flood points. - -- Client starts at 0 points. -- Every 0.5 seconds, subtract `virtualserver_antiflood_points_tick_reduce` points. -- Points never go below 0. -- Each action adds a command-specific point cost. -- If points reach/exceed `virtualserver_antiflood_points_needed_command_block`, commands are blocked. -- If points reach/exceed `virtualserver_antiflood_points_needed_ip_block`, the IP is blocked. - -Pseudo-code: - -```python -def apply_action(client, action_cost, server_config): - if not client.has_permission("b_client_ignore_antiflood"): - client.flood_points += action_cost - - if (action_cost >= 0 and - not client.has_permission("b_client_ignore_antiflood") and - client.flood_points >= server_config.command_block_threshold): - raise CommandBlocked("flood prevention") - - execute_action() -``` - -Decay: - -```python -def antiflood_tick(client, tick_reduce): - client.flood_points = max(0, client.flood_points - tick_reduce) -``` - -### 10.2 Important costs - -Examples from YaTQA: - -| Action | Approximate flood points | -|---|---:| -| Connect to server | 80, then removed after connection establishment | -| `channelsubscribe` | 15 | -| `channelunsubscribe` | 5 | -| `channelsubscribeall` | 20 | -| `channelunsubscribeall` | 25 | -| `banadd` | 25 | -| `banclient` | 25 | -| `bandel` | 5 | -| `bandelall` | 5 | -| `complainadd` | 25 | -| `complaindel` | 5 | -| `complaindelall` | 25 | -| `permissionlist` if not cached | 5 | -| `permoverview` | 5 | -| `servergroupaddperm` | 5 | - -### 10.3 Practical design - -For a Query or ClientQuery app: - -- Add a command scheduler. -- Track approximate point cost per command class. -- Use a minimum delay for bulk operations. -- Retry only after backoff when receiving error 524. -- Prefer cached data over repeated heavy queries. -- Avoid `channelsubscribeall` loops in large servers unless necessary. - -Example scheduler: - -```python -class FloodBudget: - def __init__(self, threshold=75, tick_reduce=5): - self.points = 0 - self.threshold = threshold - self.tick_reduce = tick_reduce - self.last_tick = time.time() - - def wait_for_budget(self, cost): - while self.estimated_points_after_decay() + cost >= self.threshold: - time.sleep(0.5) - self.points += cost -``` - - -*** - - -## 11. Identity and Security Level - -### 11.1 Identity model - -A TeamSpeak identity contains a keypair and a 64-bit offset/nonce. The public key contributes to the client unique identifier. The security level is a Hashcash-like proof-of-work value. - -### 11.2 Security level calculation - -YaTQA describes the security level as the number of leading zero bits in a SHA-1 hash computed from: - -```text -public_key + decimal_string(unsigned_64_bit_number) -``` - -Important bit-order detail from YaTQA: - -- SHA-1 result is 160 bits. -- Byte order is big-endian. -- Bit order within a byte is treated little-endian for counting in TeamSpeak's definition. - -Conceptual algorithm: - -```python -import hashlib - -def security_hash(public_key: str, offset: int) -> bytes: - payload = (public_key + str(offset)).encode("ascii") - return hashlib.sha1(payload).digest() - -def count_leading_zero_bits_teamspeak(digest: bytes) -> int: - count = 0 - for b in digest: - # TeamSpeak's described bit order is unusual; implement and test carefully. - for bit_index in range(8): - if (b >> bit_index) & 1 == 0: - count += 1 - else: - return count - return count -``` - -### 11.3 Search strategy - -To increase a security level: - -1. Keep the identity keypair fixed. -2. Iterate candidate offsets. -3. Compute SHA-1 for each offset. -4. Count leading zero bits. -5. Stop when required level is reached. - -Parallel strategy: - -```python -# Split the u64 search space into thread-local ranges. -# Each worker reports best offset found so far. -# Stop when target level found if stop-after-first is enabled. -``` - -`MahTsIdentity` demonstrates this kind of workflow with Rust dependencies such as `rayon`, `clap`, `sha-1`, and ReSpeak protocol crates. - -### 11.4 Vanity UID search - -A vanity UID search tries to find an identity whose UID matches a pattern. `MahTsIdentity` supports wildcard-like matching where `_` acts as a wildcard. - -Conceptual usage: - -```bash -mah_ts_identity MY_UID_PATTERN -mah_ts_identity AB_CD_ --threads 8 -mah_ts_identity --bench MY_UID -mah_ts_identity -i "IDENTITY_STRING" --level 32 -mah_ts_identity --export -i "IDENTITY_STRING" -``` - -### 11.5 Security guidance - -- Never log private identity data. -- Treat exported identities like passwords. -- Run level searches offline or in background workers. -- Do not perform identity proof-of-work in a request path. -- Benchmark hardware before promising target levels. - - -*** - - -## 12. Snapshot Format and Backup - -### 12.1 What snapshots include - -YaTQA documents snapshots as text representations of virtual servers. - -Included: - -- Virtual server settings except port. -- Keypair-related server identity data. -- Channel tree. -- Client database entries. -- Local server groups and channel groups. -- Client, channel, and group permissions. -- Group assignments. - -Not included: - -- Server port. -- Uploaded files. -- Icons and avatars as file content. -- Bans. -- Complaints. -- Offline messages, though unread counts may remain. - -### 12.2 Snapshot structure - -A snapshot begins with: - -```text -hash=| -``` - -The hash is a Base64-encoded SHA-1 of the data after the `|` separator. - -Verification: - -```python -import base64, hashlib - -def verify_snapshot(snapshot: str) -> bool: - prefix, data = snapshot.split("|", 1) - assert prefix.startswith("hash=") - expected = prefix[len("hash="):] - actual = base64.b64encode(hashlib.sha1(data.encode("utf-8")).digest()).decode("ascii") - return expected == actual -``` - -Rehash: - -```python -def rehash_snapshot(data: str) -> str: - h = base64.b64encode(hashlib.sha1(data.encode("utf-8")).digest()).decode("ascii") - return f"hash={h}|{data}" -``` - -### 12.3 Server fields in snapshots - -Common snapshot fields mirror `serverinfo`, for example: - -- `virtualserver_unique_identifier` -- `virtualserver_name` -- `virtualserver_welcomemessage` -- `virtualserver_maxclients` -- `virtualserver_password` -- `virtualserver_created` -- `virtualserver_codec_encryption_mode` -- `virtualserver_keypair` -- `virtualserver_hostmessage` -- `virtualserver_hostmessage_mode` -- `virtualserver_default_server_group` -- `virtualserver_default_channel_group` -- `virtualserver_default_channel_admin_group` -- `virtualserver_max_download_total_bandwidth` -- `virtualserver_max_upload_total_bandwidth` -- `virtualserver_antiflood_points_tick_reduce` -- `virtualserver_antiflood_points_needed_command_block` -- `virtualserver_antiflood_points_needed_ip_block` - -Dynamic runtime values such as current online clients are not meaningful backup state. - -### 12.4 Backup strategies - -| Strategy | Use case | Captures files? | Requires host access? | -|---|---|---:|---:| -| Snapshot | Logical copy of one virtual server | No | No | -| YaTQA no-snapshot backup | Tenant-level rescue with limited permissions | Partial/Pro-dependent | No | -| Full machine copy | Disaster recovery or exact migration | Yes | Yes | - -Full machine migration should include: - -- `ts3server.sqlitedb` -- `files/*` -- `query_ip_allowlist` or equivalent allow file -- `query_ip_denylist` or equivalent deny file -- server `.ini` files -- `tsdns/tsdns_settings.ini` if TSDNS is used - -### 12.5 Snapshot security - -Snapshot deployment can bypass ordinary per-setting permission checks if the user has snapshot deployment permission. Treat snapshot files as privileged artifacts. - - -*** - - -## 13. File Transfer - -TeamSpeak file transfer is not simply ServerQuery over TCP. Query commands initialize uploads/downloads and return file-transfer parameters such as file transfer key, host, and port. The actual file data is then transferred over the file-transfer TCP port. - -Typical flow: - -1. Use Query command such as `ftinitupload` or `ftinitdownload`. -2. Receive transfer key and connection details. -3. Open file-transfer TCP connection. -4. Send/use the transfer key as expected by the file-transfer protocol. -5. Stream bytes. -6. Confirm success or error. - -Implementation advice: - -- Keep file-transfer code separate from Query command code. -- Apply path escaping rules to remote file paths. -- Be careful with channel-specific file paths and permissions. -- Snapshots do not include file content, so file transfer is required for full backups. - - -*** - - -## 14. Identifier Algorithms - -### 14.1 Avatar filenames - -YaTQA's web tools and definitions document explain avatar filename conversion. Conceptually, avatar filenames use the client UID transformed into a hash-like filename with an `avatar_` prefix. - -Practical rule: - -```text -avatar filename = "avatar_" + encoded/hash form of client_unique_identifier -``` - -Some YaTQA notes refer to TeamSpeak-specific base64-decoding and `[a-p]` alphabet rendering, while the web tool summary presents the avatar filename as `avatar_` plus a hash of the UID. Therefore, when exact binary compatibility is required, test against real files created by the official client and keep the algorithm isolated behind tests. - -### 14.2 Icon filenames and IDs - -Icons are identified by 32-bit numbers. The same bit pattern can appear as signed or unsigned. - -```python -def to_signed_32(x): - x &= 0xffffffff - return x if x < 0x80000000 else x - 0x100000000 - -def to_unsigned_32(x): - return x & 0xffffffff -``` - -Use this for server, channel, and client icon ID handling. - -### 14.3 Cache directory names - -Server cache and chat directory names are based on the server UID. YaTQA's cache tool describes this as essentially Base64 encoding/decoding. - -```python -import base64 - -def server_uid_to_cache_dir(uid: str) -> str: - return base64.b64encode(uid.encode("utf-8")).decode("ascii") -``` - -Test against real `%APPDATA%/TS3Client/cache` entries when strict client compatibility is needed. - - -*** - - -## 15. DNS, SRV, and TSDNS - -### 15.1 Recommended SRV records - -Use `_ts3._udp` for voice service discovery: - -```dns -_ts3._udp.example.com. 86400 IN SRV 0 5 9987 voice.example.com. -``` - -If using TSDNS indirection: - -```dns -_tsdns._tcp.example.com. 86400 IN SRV 0 5 41144 tsdns.example.com. -``` - -### 15.2 Resolution strategy - -A robust resolver should attempt: - -1. Direct host:port if the user provides a port. -2. `_ts3._udp.` SRV lookup. -3. TSDNS lookup if configured or required. -4. A/AAAA fallback to default port 9987. - -YaTQA notes that implicit historical TSDNS behavior changed across client versions. Do not rely on old fallback behavior. Configure SRV explicitly when possible. - -### 15.3 TSDNS purpose - -TSDNS maps human-friendly names to TeamSpeak server addresses and ports. It is useful when: - -- The voice server uses a non-default port. -- You want multiple named TS3 servers under one domain. -- You need indirection outside normal SRV behavior. - - -### 15.4 YaTQA "How to DNS" decision chart - written specification - -The YaTQA resources page includes a German flowchart titled **"Welche Art von DNS passt zu deinem TeamSpeak-3-Server?"** ("Which type of DNS fits your TeamSpeak 3 server?"). The chart is an operational decision tree for choosing between plain A/AAAA/CNAME records, `_ts3._udp` SRV records, simple TSDNS, SRV-assisted TSDNS, or subdomain-based layouts. This subsection writes that chart down explicitly so implementers and operators do not have to rely on the image. - -The original resource is a graphical PNG flowchart. In this manual, the decision chart is deliberately **not embedded as a PNG**. It is represented as native document content: a decision table, a numbered decision algorithm, and DNS templates. This keeps the manual searchable, editable, accessible to screen readers, and usable in generated DOCX/PDF formats without relying on a raster image. - -#### 15.4.0 Text-native decision chart - -| Node | Question / condition | Yes branch | No branch | Recommendation if terminal | -|---|---|---|---|---| -| 0 | Are you trying to use a complicated SRV-TSDNS setup without a concrete need? | Do not do it; use simple TSDNS or a simpler DNS method. | Continue. | Avoid unnecessary SRV-TSDNS. | -| 1 | How many TeamSpeak virtual servers do you operate under the relevant name/domain? | One virtual server -> Node 2. | More than one virtual server -> Node 6. | - | -| 2 | For one virtual server: does the server use the standard TS3 voice port `9987`? | Node 3. | Node 4. | - | -| 3 | Standard port: do all services under the relevant domain run on the same machine as the TS3 server? | Use A/AAAA or CNAME. | Use `_ts3._udp` SRV. | - | -| 4 | Non-standard port: are users willing to type a port number manually? | Node 5. | Use `_ts3._udp` SRV. | - | -| 5 | Manual port accepted: do all services under the relevant domain run on the same machine as the TS3 server? | Use A/AAAA or CNAME and tell users to connect as `host:port`. | Use `_ts3._udp` SRV. | - | -| 6 | Multiple virtual servers: are users willing to type port numbers manually? | Node 7. | Use subdomains with `_ts3._udp` SRV records for every subdomain. | - | -| 7 | Multiple servers with manual ports: on how many machines are the TS3 servers operated? | One machine -> Node 8. | Multiple machines -> use subdomains with A/AAAA or CNAME per server host. | - | -| 8 | One machine: do all services under the relevant domain run on the same machine as the TS3 server? | Use A/AAAA or CNAME and explicit ports for non-default virtual servers. | Use SRV TSDNS if you deliberately operate TSDNS indirection. | - | - -Text-only flow representation: - -```text -START -| -+-- Needlessly complicated SRV-TSDNS idea? -| +-- yes -> Do not use it; prefer simple TSDNS, SRV TS3, or A/AAAA/CNAME. -| +-- no -> Continue. -| -+-- How many virtual servers? - | - +-- One virtual server - | | - | +-- Uses standard port 9987? - | | - | +-- yes - | | +-- All services under this domain run on the same machine? - | | +-- yes -> Use A/AAAA or CNAME. - | | +-- no -> Use SRV TS3 (`_ts3._udp`). - | | - | +-- no - | +-- Are users willing to type the port manually? - | +-- no -> Use SRV TS3 (`_ts3._udp`). - | +-- yes - | +-- All services under this domain run on the same machine? - | +-- yes -> Use A/AAAA or CNAME plus `host:port`. - | +-- no -> Use SRV TS3 (`_ts3._udp`). - | - +-- More than one virtual server - | - +-- Are users willing to type port numbers? - | - +-- no -> Use subdomains with SRV TS3 for every subdomain. - | - +-- yes - +-- Are TS3 servers operated on one machine or several? - +-- one machine - | +-- All services under this domain run on the TS3 machine? - | +-- yes -> Use A/AAAA or CNAME, explicit ports where needed. - | +-- no -> Use SRV TSDNS. - | - +-- several machines -> Use subdomains with A/AAAA or CNAME per server. -``` - - -#### 15.4.1 Vocabulary used by the chart - -| Chart wording | Meaning in English | Engineering interpretation | -|---|---|---| -| `A/AAAA oder CNAME` | A/AAAA or CNAME | Use normal DNS host records. This works best when users connect to a hostname whose resolved host is the TS3 host, and either the server uses the default UDP voice port `9987` or users are willing to type a port manually. | -| `SRV TS3` | TS3 SRV record | Use `_ts3._udp.` SRV records so the client can discover the target host and voice port automatically. This is the preferred modern solution for non-default ports. | -| `TSDNS` | TeamSpeak DNS helper | Use TeamSpeak's TSDNS service, usually on TCP `41144`, to map a user-visible name to a TeamSpeak endpoint. | -| `SRV TSDNS` | SRV-assisted TSDNS | Use `_tsdns._tcp.` to locate the TSDNS service, then ask TSDNS for the final TeamSpeak endpoint. This is useful when TSDNS itself is not hosted at the obvious domain host. | -| `Subdomains` | One DNS name per server | Create a distinct subdomain such as `alpha.example.com`, `beta.example.com`, `guild.example.com`, etc., and then apply A/AAAA/CNAME or SRV records per subdomain. | -| `Portnummern` | Port numbers | Whether users are willing to type `host:port` manually in the TeamSpeak client. | - -#### 15.4.2 Decision tree translated from the image - -Start with the question: **How many TeamSpeak virtual servers do you have?** - -1. **Do not use this complex setup unless you need it.** - - The chart contains an explicit warning path: **"Tu's nicht!"** ("Don't do it!"). - - If you are trying to use SRV-TSDNS only because it looks advanced, the chart recommends: **Use simple TSDNS without SRV TSDNS**. - - Practical interpretation: do not introduce SRV-TSDNS unless you actually need indirection for a TSDNS service. For a normal server, `_ts3._udp` or A/AAAA/CNAME is simpler and easier to debug. - -2. **If you have exactly one virtual server:** - 1. Ask: **Does the server use the standard port `9987`?** - - **Yes:** ask whether all services under the relevant domain run on the same machine as the TS3 server. - - **Yes:** use **A/AAAA or CNAME**. - - **No:** use **SRV TS3**. - - **No:** ask whether you are willing for users to type the port manually. - - **Yes:** ask whether all services under the relevant domain run on the same machine as the TS3 server. - - **Yes:** use **A/AAAA or CNAME**, but users must connect with `host:port` unless another discovery mechanism is added. - - **No:** use **SRV TS3**. - - **No:** use **SRV TS3**. - -3. **If you have more than one virtual server:** - 1. Ask: **Do you like port numbers?** In other words, are users expected to type ports manually? - - **No:** use **subdomains with SRV TS3 for every subdomain**. - - Example: `alpha.example.com`, `beta.example.com`, and `staff.example.com` each get their own `_ts3._udp` SRV record pointing to the correct host and port. - - **Yes:** ask on how many machines the TS3 servers are operated. - - **Only one machine:** ask whether all services under the relevant domain run on the same machine as the TS3 server. - - **Yes:** use **A/AAAA or CNAME** and require users to specify ports for non-default virtual servers. - - **No:** use **SRV TSDNS**. - - **More than one machine:** use **subdomains with A/AAAA or CNAME for each server**. - - Practical interpretation: each physical server gets a hostname, and users either use default ports or explicit `host:port` entries unless SRV is added per subdomain. - -#### 15.4.3 DNS record templates for each chart recommendation - -**A/AAAA record - IPv4/IPv6 host mapping** - -```dns -ts3.example.com. 3600 IN A 203.0.113.10 -ts3.example.com. 3600 IN AAAA 2001:db8::10 -``` - -Use this when the TeamSpeak service is on the host represented by the name and the port situation is simple. If the server is not on `9987`, users must connect with `ts3.example.com:PORT` unless SRV or TSDNS is also used. - -**CNAME - alias to another hostname** - -```dns -ts3.example.com. 3600 IN CNAME voice-host.provider.example. -``` - -Use this when you do not want to expose or maintain the provider's long hostname. Do not point a CNAME directly at an IP address; CNAME targets must be names. - -**SRV TS3 - recommended for non-default ports** - -```dns -_ts3._udp.example.com. 3600 IN SRV 0 5 9987 voice1.example.net. -_ts3._udp.alpha.example.com. 3600 IN SRV 0 5 9987 host-a.example.net. -_ts3._udp.beta.example.com. 3600 IN SRV 0 5 9990 host-a.example.net. -``` - -Use this when users should type only the domain or subdomain, while DNS tells the client which host and UDP port to connect to. The SRV target should be a hostname with its own A/AAAA records. - -**Simple TSDNS - without SRV TSDNS** - -```text -# Conceptual TSDNS mapping, not a DNS-zone record: -example.com=voice1.example.net:9987 -alpha.example.com=voice1.example.net:9990 -``` - -Use this only when you deliberately operate a TSDNS service and understand its client-version compatibility. For most modern deployments, `_ts3._udp` SRV records are easier to operate and easier to test. - -**SRV TSDNS - locate the TSDNS service through DNS SRV** - -```dns -_tsdns._tcp.example.com. 3600 IN SRV 0 5 41144 tsdns.example.net. -``` - -Use this when the TSDNS service is not hosted at the obvious domain target or when domain-level routing must be delegated to a separate TSDNS endpoint. The TSDNS service then returns the actual TeamSpeak host and port. - -**Subdomains with SRV TS3** - -```dns -_ts3._udp.public.example.com. 3600 IN SRV 0 5 9987 host1.example.net. -_ts3._udp.staff.example.com. 3600 IN SRV 0 5 9991 host1.example.net. -_ts3._udp.events.example.com. 3600 IN SRV 0 5 9987 host2.example.net. -``` - -This is the chart's cleanest recommendation for multiple virtual servers when users should not type port numbers. - -**Subdomains with A/AAAA or CNAME for each server** - -```dns -server1.example.com. 3600 IN A 203.0.113.10 -server2.example.com. 3600 IN A 203.0.113.11 -server3.example.com. 3600 IN CNAME rented-ts3.provider.example. -``` - -Use this for multiple physical hosts when manual ports are acceptable or when each subdomain maps clearly to one machine. - -#### 15.4.4 Implementation guidance for a resolver or admin UI - -A TeamSpeak administration tool should expose the chart as a wizard instead of forcing the operator to understand every DNS mechanism. A practical UI can ask exactly these questions: - -1. How many virtual servers do you operate under this domain? -2. Does the selected server use UDP voice port `9987`? -3. Are users allowed to type a port manually? -4. Do web, mail, and other services for this domain run on the same host as TeamSpeak? -5. Are the TeamSpeak servers on one physical host or multiple physical hosts? -6. Do you already operate a TSDNS service? - -The wizard can then emit either plain DNS records, SRV records, or a TSDNS mapping. Validate the output with these checks: - -- Every SRV target has at least one A or AAAA record. -- SRV targets are hostnames, not IP literals. -- `_ts3._udp` records use UDP and the actual voice port. -- `_tsdns._tcp` records use TCP and normally port `41144`. -- If using A/AAAA/CNAME only with a non-default port, the documentation shown to users must include `host:port`. -- If using multiple virtual servers on one IP, either use ports explicitly or use SRV records per subdomain. - -#### 15.4.5 Quick recommendation table - -| Scenario | Recommended solution | User enters | -|---|---|---| -| One virtual server, default port, TS3 host is also the domain host | A/AAAA or CNAME | `example.com` or `ts3.example.com` | -| One virtual server, default port, TS3 host differs from domain host | SRV TS3 | `example.com` | -| One virtual server, non-default port, users accept manual port | A/AAAA/CNAME plus documented port | `example.com:9990` | -| One virtual server, non-default port, users should not type port | SRV TS3 | `example.com` | -| Multiple virtual servers, users should not type ports | Subdomains with SRV TS3 | `alpha.example.com`, `beta.example.com` | -| Multiple virtual servers on one host, users accept ports | A/AAAA/CNAME plus documented ports | `example.com:9987`, `example.com:9990` | -| Multiple physical hosts, simple host naming desired | Subdomains with A/AAAA or CNAME | `server1.example.com`, `server2.example.com` | -| Advanced delegation to a separate name-resolution service | SRV TSDNS | `example.com` or service-specific names | - - -*** - - - -## 16. TeamSpeak 3 Auxiliary Protocols - Complete YaTQA `protokolle` Coverage - -This chapter expands the earlier protocol notes into a self-contained engineering specification derived from YaTQA's `Protokolle von TeamSpeak 3` resource. The YaTQA page explicitly states that it covers the protocols TeamSpeak uses **besides Query and voice**. These protocols are not optional knowledge if you want to build a real TS3-compatible ecosystem: update checking, file transfer, address resolution, blacklists, public server listing, badges, and several metadata formats all depend on them. - -The protocols covered here are: - -- TeamSpeak's restricted Protobuf/varint usage. -- Historical and current update metadata formats. -- File transfer channel bootstrap. -- TSDNS and DNS/SRV resolution behavior. -- Server nicknames introduced in client 3.1.6+. -- Blacklist v1 and Blacklist2. -- Weblist server reporting and client querying. -- Badge list format and badge asset resolution. -- TLD/public-suffix handling differences across client versions. - -### 16.1 Protobuf dialect used by TeamSpeak - -YaTQA describes a small subset of Protocol Buffers used in several TeamSpeak metadata files and service requests. The implemented data types are effectively: - -| Wire type | Meaning | Implementation use | -|---:|---|---| -| 0 | BigNum / Varint | Unsigned integer, status enum, timestamp, port, revision. | -| 1 | 64-bit fixed integer | Recognized by Protobuf; YaTQA notes TeamSpeak does not generally use all standard Protobuf types. | -| 2 | Length-delimited bytes | Strings, binary IP addresses, nested Protobuf records. | -| 5 | 32-bit fixed integer | Available wire type. | -| 3,4 | Deprecated | Do not generate. | -| 6,7 | Free/unused | Do not generate. | - -#### 16.1.1 BigNum / Varint writer - -TeamSpeak's BigNum is identical to the common unsigned varint encoding: write the low seven bits per byte; set the high bit when another byte follows. - -```pseudo -function write_big_num(x: unsigned integer) -> bytes: - out = [] - while true: - if x < 128: - out.append(byte(x)) - return out - out.append(byte((x & 0x7F) | 0x80)) - x = x >> 7 -``` - -#### 16.1.2 BigNum / Varint reader - -```pseudo -function read_big_num(buffer, offset=0) -> (value, new_offset): - value = 0 - shift = 0 - pos = offset - while true: - b = buffer[pos] - pos += 1 - value = value | ((b & 0x7F) << shift) - if (b & 0x80) == 0: - return value, pos - shift += 7 - if shift > MAX_SUPPORTED_BITS: - raise DecodeError('varint too large') -``` - -Implementation requirements: - -- Reject unterminated varints when input ends before a byte with high bit clear. -- Limit maximum width to the integer type your implementation supports. -- Do not interpret BigNum as signed unless the specific higher-level field says so. - -#### 16.1.3 Length-delimited binary field - -A length-delimited field is: - -```text -BigNum length || length bytes of payload -``` - -The payload is often UTF-8 text, sometimes binary data, and sometimes another nested Protobuf message. Do not assume text unless the field definition says it is text. - -#### 16.1.4 File/message structure - -A TeamSpeak Protobuf-like stream is a repeated sequence of: - -```text -field_key || field_value -``` - -The field key itself is a BigNum. Decode it as: - -```pseudo -wire_type = field_key & 7 -field_number = field_key >> 3 -``` - -Repeated fields are allowed. YaTQA notes that TeamSpeak assigns the field number once, but repeated identifiers occur when arrays/lists are stored. Your decoder must therefore store fields as `field_number -> list(values)` rather than assuming each field appears once. - -#### 16.1.5 Minimal generic decoder skeleton - -```pseudo -function decode_ts_protobuf(buffer): - offset = 0 - result = map>() - while offset < len(buffer): - key, offset = read_big_num(buffer, offset) - field_number = key >> 3 - wire_type = key & 7 - - if wire_type == 0: - value, offset = read_big_num(buffer, offset) - else if wire_type == 1: - value = read_u64_le(buffer, offset) - offset += 8 - else if wire_type == 2: - n, offset = read_big_num(buffer, offset) - value = buffer[offset : offset + n] - offset += n - else if wire_type == 5: - value = read_u32_le(buffer, offset) - offset += 4 - else: - raise DecodeError('unsupported/deprecated TeamSpeak wire type') - - result[field_number].append(Value(wire_type, value)) - return result -``` - -### 16.2 Update protocol and update metadata - -TeamSpeak's update protocol changed over time. An implementation that needs compatibility with old clients or needs to reproduce historical behavior should understand all generations, but a modern application should prefer HTTPS-downloaded version metadata and signature validation rather than the oldest UDP-only mechanisms. - -#### 16.2.1 Historical UDP update probes - -YaTQA describes three historical update query styles: - -| Client generation | Outbound request | Destination | Response style | -|---|---|---|---| -| Up to 3.0.0-beta7 | UDP payload `1` | `update.teamspeak.com:17384` | Unknown response format. | -| 3.0.0-beta8 era | UDP payload `2` | `update.teamspeak.com:17384` | String like `1321432557,1,1,1`; first field is version timestamp/build, remaining fields are flags, likely validity booleans for stable/beta/alpha. | -| Around client 3.0.3 | UDP payload `3` | `update.teamspeak.com:17384` | Comma-separated string containing stable, beta, alpha client versions and an old server version; each record stores timestamp and name separated by `#`; final field is `29`. | - -#### 16.2.2 Current metadata file structure - -Later clients download a version metadata file from TeamSpeak version endpoints. YaTQA states that these files use the Protobuf dialect described above. The logical structure is: - -```text -field 1: BigNum, always 2 -field 2: repeated length-delimited nested record, one per version category - nested field 1: length-delimited string = version category name - nested field 2: BigNum = build number / build timestamp - nested field 3: length-delimited string = human version string -field 3: BigNum, always 2 -``` - -A robust parser should not hard-code exactly three version categories. It should parse all field-2 nested entries and expose them by category name. - -```pseudo -function parse_version_metadata(bytes): - root = decode_ts_protobuf(bytes) - records = [] - for nested_bytes in root[2]: - n = decode_ts_protobuf(nested_bytes.bytes) - records.append({ - 'name': utf8(n[1][0].bytes), - 'build': n[2][0].varint, - 'version': utf8(n[3][0].bytes), - }) - return records -``` - -#### 16.2.3 Updater image compression - -YaTQA notes that updater image files may appear to use `compress` from their extension, but the file format is actually gzip. Later names suggest LZMA2, but 7-Zip reportedly cannot open them, while INI references still point to gzip. Treat update image decompression as a versioned compatibility concern: - -1. Identify the update file generation. -2. Try gzip when the generation matches the documented old/current updater format. -3. Reject unknown formats rather than guessing. -4. Verify digital signatures or hashes before installing. - -#### 16.2.4 Anydate engineering lesson - -YaTQA's Anydate tool exists because the official updater only updates to the newest available version. A version-switching development utility should therefore not rely on the official updater UX. It should: - -- Fetch version metadata. -- Select exact target version by platform and architecture. -- Download the corresponding archive/image. -- Extract into a test directory, not over the user's production client. -- Require explicit user confirmation before touching profile/AppData directories because old clients can corrupt newer client data. - -### 16.3 File transfer protocol - -TeamSpeak file transfer is bootstrapped by ServerQuery/ClientQuery commands but the actual file bytes are transferred on a separate connection. - -#### 16.3.1 Bootstrap sequence - -Upload: - -```text -1. Control channel: ftinitupload ... -2. Server returns: token/key + IP + port + other transfer metadata. -3. Client opens TCP connection to returned IP:port. -4. Client writes exactly the received key/token bytes. -5. Client immediately writes raw file data. -``` - -Download: - -```text -1. Control channel: ftinitdownload ... -2. Server returns: token/key + IP + port + file size metadata. -3. Client opens TCP connection to returned IP:port. -4. Client writes exactly the received key/token bytes. -5. Server streams raw file bytes to client. -``` - -Critical details: - -- The key is sent exactly as received. -- No escaping is performed. -- Nothing is prefixed before the key. -- Nothing is appended after the key before file data begins. -- After the key, upload sends bytes; download receives bytes. - -#### 16.3.2 Transfer implementation skeleton - -```pseudo -function upload_file(control, local_path, remote_path, channel_id): - meta = control.command('ftinitupload', { - 'clientftfid': allocate_transfer_id(), - 'name': remote_path, - 'cid': channel_id, - 'size': file_size(local_path), - }) - socket = tcp_connect(meta.host, meta.port) - socket.write(meta.ftkey.raw_bytes) - stream_file_to_socket(local_path, socket) - socket.close() - -function download_file(control, remote_path, local_path, channel_id): - meta = control.command('ftinitdownload', { - 'clientftfid': allocate_transfer_id(), - 'name': remote_path, - 'cid': channel_id, - }) - socket = tcp_connect(meta.host, meta.port) - socket.write(meta.ftkey.raw_bytes) - stream_socket_to_file(socket, local_path, expected_size=meta.size) -``` - -#### 16.3.3 Engineering cautions - -- File transfer port is separate from the voice port and raw ServerQuery port. Do not reuse your Query socket. -- Treat the key/token as binary-safe data even if it looks printable. -- A partial upload may appear in directory listings with incomplete size metadata until finalized. -- File transfer paths are TeamSpeak virtual paths. Do not map them directly to local filesystem paths without sanitization. - -### 16.4 TSDNS protocol - -TSDNS is TeamSpeak's name-to-server resolution helper. It is not the same as DNS SRV, though DNS SRV can point to a TSDNS server. - -#### 16.4.1 Wire format - -To query a TSDNS server: - -```text -lowercase(input_name) -encode as UTF-8 or CESU-8 -append bytes: 0x0A 0x0D 0x0D 0x0D 0x0A -send over TCP to port 41144 -``` - -In escaped form: - -```text -\n\r\r\r\n -``` - -The response is text. Possible meanings: - -| Response | Meaning | -|---|---| -| `host:port` or IP/host result | Use this resolved target. | -| `$PORT` as returned port | Preserve the port specified by the user instead of overriding it. | -| `404` | No result known or deliberately configured as missing. | -| No response | Client continues the resolution hierarchy in non-SRV TSDNS modes; in SRV TSDNS this can cause overall failure if only one TSDNS server exists. | - -#### 16.4.2 Query algorithm - -```pseudo -function query_tsdns(tsdns_host, input_name): - q = lowercase_unicode(input_name) - payload = encode_utf8_or_cesu8(q) + bytes([0x0A,0x0D,0x0D,0x0D,0x0A]) - s = tcp_connect(tsdns_host, 41144, timeout=5s) - s.write(payload) - response = s.read_until_close_or_timeout() - if response == '404': - return NotFound - return parse_host_port_or_port_token(response) -``` - -#### 16.4.3 When to use TSDNS - -YaTQA's practical conclusion is that plain TSDNS alone is not very useful. The useful case is **SRV TSDNS**: when you host other services such as a website on the domain, and multiple TeamSpeak servers with different ports are hosted elsewhere or need domain-level routing. - -### 16.5 Server nicknames, client 3.1.6+ - -TeamSpeak client 3.1.6 introduced server nicknames. A nickname is used when the user enters a name without dots. Instead of treating it as a local computer name, the client calls: - -```text -https://named.myteamspeak.com/lookup?name= -``` - -Important behavior: - -- HTTPS is required. HTTP is not accepted. -- The lookup is case-insensitive on the myTeamSpeak service. -- The response is a UTF-8 domain string. -- A port may be appended with `:`. -- If no port is returned, the default voice port 9987 is assumed. -- After nickname lookup returns a domain, the normal TeamSpeak address-resolution pipeline runs as though the user typed that domain. -- Edits to nickname entries may take time to become effective. - -Implementation strategy: - -```pseudo -function resolve_server_nickname(input): - if '.' in input: - return input - response = https_get('https://named.myteamspeak.com/lookup?name=' + url_encode(input)) - if response.ok and response.body is nonempty: - return utf8(response.body) - return input // or fail, depending on client policy -``` - -### 16.6 DNS, SRV, and address resolution - -TeamSpeak address resolution has changed repeatedly and is not a perfect RFC implementation. YaTQA's key observation is that the client fires several lookups at once, waits up to about five seconds, and uses the first complete solution in a fixed priority order. - -#### 16.6.1 Resolution priority - -The priority order is: - -1. **SRV TS3**: query `_ts3._udp.`. The SRV result's port overrides the user's typed port. -2. **SRV TSDNS**: query `_tsdns._tcp.`, where the candidate domain depends on the client version and public-suffix logic. -3. **TSDNS**: query candidate domains on TCP port 41144. -4. **Plain DNS**: query AAAA/A records for the input domain. CNAME is only followed implicitly by the resolver. - -Once a complete solution is found, no later resolution method is tried if the actual connection fails. Do not implement address-resolution fallback as "try next method after connection failure" unless intentionally deviating from TeamSpeak behavior. - -#### 16.6.2 SRV record rules - -A TS3 SRV record typically looks like: - -```dns -_ts3._udp.example.com. 3600 IN SRV 0 5 9987 voice.example.com. -``` - -YaTQA notes the following compatibility quirks: - -- Older TS3 behavior did not fully support all SRV features such as weight/priority/dot semantics; later behavior changed. -- SRV targets should be hostnames, not IP addresses. TS3 3.0 accepted IP targets incorrectly; TS3 3.1 fixed IP targets but still accepted CNAME targets although this is also not strictly RFC-correct. -- SRV is the easiest way to specify a non-default port. - -Recommended engineering behavior: - -- Prefer `_ts3._udp` for normal modern deployments. -- Treat SRV priority and weight according to standard DNS rules in your own implementation unless strict old-client compatibility is required. -- Reject SRV records pointing directly to IP addresses if implementing standards-compliant behavior; optionally add a compatibility flag to accept them. - -#### 16.6.3 DNS/TSDNS version matrix - -YaTQA includes a version matrix. Rewritten as implementation guidance: - -| Client version | A/AAAA behavior | SRV behavior | TSDNS behavior | Local computer names | -|---|---|---|---|---| -| Up to 3.0.0-beta37 | ASCII lowercase; limited behavior | No SRV support in early rows | TSDNS partially supported | Yes | -| 3.0.0-rc1 | ASCII lowercase + UTF-8 encoding in parts | Supported | Supported | Yes | -| 3.0.0-rc2 to 3.0.7 | Punycode for DNS A; UTF-8/ASCII lowercase elsewhere | Supported | Supported | Yes | -| 3.0.8 to 3.0.16 | Punycode for A/AAAA; lowercase/UTF-8 transformations for SRV/TSDNS | Supported | Supported | Yes | -| 3.0.17 to 3.0.19.4 | Punycode and Unicode lowercase paths | Supported | Supported | Yes | -| 3.0.20 | UTF-8 after lowercase for A/SRV/TSDNS; no AAAA lookup except literal IPv6 | SRV TSDNS only | Plain TSDNS removed | No | -| 3.1-beta1 to 3.1-beta2 | UTF-8 after lowercase | SRV TSDNS only | Plain TSDNS removed | No | -| 3.1-beta3 | Punycode restored for A/SRV, lowercase+UTF-8 for TSDNS | Warning/changed behavior for IP validity | Plain TSDNS behavior changed | No | -| 3.1-beta4 and later | Punycode after lowercase for DNS and SRV; lowercase+UTF-8 plus TSDNS suffix | Modern style | Modern style | No | - -Interpretation for a new implementation: - -- Use standard IDNA/Punycode for DNS names. -- Lowercase before DNS queries to match TS behavior. -- Encode TSDNS query body as UTF-8 unless deliberately emulating CESU-8-era clients. -- Do not resolve bare local computer names as TS3 server names in modern mode. - -#### 16.6.4 Candidate domains for SRV TSDNS and TSDNS - -Version-dependent candidate selection: - -| Client generation | Candidate generation | -|---|---| -| Up to 3.0.19.4 | Build a list of up to the four highest levels of the input domain and query from the lower level upward. Public-suffix/TLD entries are skipped but still count toward the four-level limit. A bare top-level domain may still be queried through SRV even if listed. | -| 3.0.20 to 3.1-beta1 | Plain TSDNS removed. SRV TSDNS queries the second-level domain of the input. This fails for domains delegated only at the third level. Bare top-level domains produce no SRV lookup. | -| 3.1-beta2 | Plain TSDNS removed. SRV TSDNS queries the highest input level that is not in the embedded public-suffix list. | -| 3.1-beta3 and later | TSDNS and SRV TSDNS query the two highest levels of the input domain that are not in the public-suffix list, beginning with the highest level. | - -#### 16.6.5 Choosing the correct DNS system - -YaTQA gives a decision table. Rewritten for engineering use: - -| Scenario | Services on same IP | Services on different IPs | -|---|---|---| -| One TS3 server on standard port | A or CNAME is sufficient. | SRV TS3 is preferred. | -| One TS3 server on non-standard port, user should not type port | SRV TS3. | SRV TS3. | -| Multiple TS3 servers or one non-standard port server where port indication is acceptable | A or CNAME plus explicit port can work. | SRV TSDNS if you need the same domain to route to TS3 servers hosted elsewhere. | - -Practical recommendation: - -- Use `_ts3._udp` whenever possible. -- Use SRV TSDNS only when one domain must route multiple TeamSpeak services while another service such as a website is hosted on a different IP. -- Avoid plain TSDNS as the only mechanism for modern deployments. - -### 16.7 Blacklist v1 - -Blacklist v1 is a UDP service. - -#### 16.7.1 Request - -```text -Destination: blacklist.teamspeak.com:17385 UDP -Payload: ip4: -``` - -Examples: - -```text -ip4:212.224.114.71 -ip4:84.200.62.245 -``` - -YaTQA's example for IPv6 intentionally shows no response; do not rely on v1 blacklist for IPv6 compatibility. - -#### 16.7.2 Response - -The response is 13 ASCII characters: - -```text -x,13371337133 -``` - -`x` is the status: - -| x | Status | Client behavior | -|---|---|---| -| 0 | Blacklisted | Refuse connection. | -| 1 | OK | Permit connection. | -| 2 | Greylisted | Permit or warn depending on client policy; official client displays a warning. | - -If the service does not answer, YaTQA states that the client still connects. A compatible implementation should fail open, not fail closed, for blacklist v1 service outages. - -### 16.8 Blacklist2, client 3.1.6+ - -Blacklist2 is an HTTPS service using a TeamSpeak-specific Protobuf payload. - -#### 16.8.1 HTTP request - -```http -POST /check HTTP/1.1 - -Host: blacklist2.teamspeak.com - -User-Agent: TeamHttp/1.1 - -Connection: keep-alive - -Content-Type: application/x-ts3blacklist - -Content-Length: - - - - -``` - -`Connection: close` may work, but the official client uses `keep-alive` and YaTQA notes the service can be picky about some fields. - -#### 16.8.2 Logical request fields - -`BlacklistInfoRequest`: - -| Field no. | Name | Type | Required? | Notes | -|---:|---|---|---|---| -| 1 | `ip_address` | length-delimited binary | Required | IP address in binary form; IPv6 requires canonicalization compatibility. | -| 2 | `domain_name` | length-delimited UTF-8 | Optional | Domain or nickname used by the user. Explicitly UTF-8, not CESU-8. | -| 3 | `virtual_server_id` | length-delimited string | Required | Server UID, usual Base64 string. | -| 4 | `public_license_id` | length-delimited Base64 | Optional / server 3.1.0+ | Same for all servers on a license and all clients; exact computation unknown in YaTQA notes. | -| 5 | `port` | BigNum | Optional but recommended | Voice port. | -| 6 | `timestamp` | BigNum | Optional but recommended | Unix timestamp UTC. | -| 7 | `valid_token_present` | BigNum | Unused in observed behavior | Name from source; YaTQA did not observe functional variation. | -| 8 | `slots_ok` | BigNum | Unused in observed behavior | Name from source; YaTQA observed value 1 and could not correlate it with server version/license/slot cases. | - -Binary request construction: - -```pseudo -payload = protobuf() -payload.add_length_delimited(1, ip_to_binary(resolved_ip)) -if user_supplied_domain: - payload.add_length_delimited(2, utf8(user_supplied_domain)) -payload.add_length_delimited(3, ascii(server_uid_base64)) -if public_license_id_available: - payload.add_length_delimited(4, ascii(public_license_id_base64)) -payload.add_varint(5, port) -payload.add_varint(6, unix_timestamp_utc()) -payload.add_varint(7, 0_or_unknown) -payload.add_varint(8, 1) -``` - -#### 16.8.3 Response fields - -Blacklist2 returns a small Protobuf response. YaTQA observed four BigNum fields: - -| Field | Meaning | -|---:|---| -| 1 | `status_ip` | -| 2 | `status_domain`; may include wildcard bans for subdomains. | -| 3 | `status_virtual_server_id` | -| 4 | `status_public_license_id` (spelled inconsistently in YaTQA's source notes) | - -Status enum: - -| Value | Meaning | -|---:|---| -| 1 | INVALID | -| 2 | BLACKLISTED | -| 3 | GREYLISTED | -| 4 | NOT_LISTED / OK / field not supplied | - -Connection decision: - -```pseudo -function blacklist2_decision(response): - statuses = parse_statuses(response) - if any(status == BLACKLISTED for status in statuses): - return DENY - if any(status == GREYLISTED for status in statuses): - return WARN_OR_ALLOW - return ALLOW -``` - -YaTQA notes that once one field is blacklisted, the official client refuses the connection. Results appear to be cached, likely by IP. This matters when only a domain is blacklisted: after the domain check fails, connecting to the raw IP may still be blocked until cache expiry or client restart. - -#### 16.8.4 IPv6 canonicalization bug in TS 3.1.6/3.1.7 - -YaTQA documents a TeamSpeak-specific bug in Blacklist2 IPv6 canonicalization: - -- Omitted zero groups in compressed IPv6 were filled with `3030` instead of `0`/`0000` (`0x30` is ASCII `'0'`). -- If the seventh block was omitted, the eighth block was replaced with `3030`. -- The same IPv6 address could therefore produce different Blacklist2 queries depending on how the user abbreviated it. -- YaTQA says this was fixed in 3.1.8-beta1. - -For compatibility testing, keep test cases for compressed and uncompressed IPv6 forms: - -```text -2a01:7e0:0:417:59:1337:cad:5e -2a01:7e0::417:59:1337:cad:5e -2a05:8b81:1000:17d:0:0:0:0 -2a05:8b81:1000:17d:: -``` - -A modern implementation should canonicalize IPv6 correctly, but a bug-compatible mode can be useful when reproducing old-client traffic. - -### 16.9 Weblist server reporting - -The Weblist server protocol is a UDP protocol to: - -```text -weblist.teamspeak.com:2010 -``` - -#### 16.9.1 Common packet header - -Every server-reporting packet has: - -| Offset/size | Field | Description | -|---|---|---| -| 1 byte | Version? | Always `1`. | -| 2 bytes | Packet number | Sequential number, identical in request and response, begins at 1. | -| 1 byte | Packet type | `1` for key request, `2` for data update. | -| variable | Payload | Optional depending on packet type. | - -Byte order: - -- Multi-byte integer fields are little-endian. -- Bit order for flags is described by YaTQA as big-endian. - -#### 16.9.2 Update handshake - -```text -1. Server sends key-request packet, no payload. -2. Weblist returns random 32-bit key. A fresh key is returned each time. -3. Server sends data update using that key. -4. Weblist returns one payload byte. -``` - -#### 16.9.3 Data update payload - -| Field | Size | Meaning | -|---|---:|---| -| key | 4 bytes | Key received in step 2. | -| port | 2 bytes | Voice port. | -| slots | 2 bytes | Maximum slots. | -| clients | 2 bytes | Connected clients, excluding Query clients. | -| flags | 1 byte | Lower details below. | -| name_len | 1 byte | Server-name length in bytes; 0 if unchanged. | -| name | variable | Server name encoded UTF-8 or CESU-8. | - -Flags byte: - -| Bits | Meaning | -|---|---| -| first 6 bits | Always 0. | -| next bit | 1 if guests can create a channel, regardless of channel type; 0 otherwise. | -| final bit | 1 if server has a password; 0 otherwise. | - -Response payload: - -| Response byte | Meaning | -|---:|---| -| 0 | OK. | -| 7 | Weblist server asks for the server name again, e.g. because it forgot state. | - -#### 16.9.4 Retry and timing behavior - -YaTQA documents the official server's timing as: - -- Attempt Weblist update every 10 minutes. -- If no response, retry after 1.3 seconds. -- If still no response, retry again 2 seconds later. -- The packet ID is not incremented for those retries. -- If all attempts fail, the server waits until the next normal 10-minute interval. -- Shutting down a virtual server is not reported to the Weblist. - -Implementation notes: - -```pseudo -function report_weblist_loop(): - every 10 minutes: - packet_id += 1 - for delay in [0s, 1.3s, 2s]: - if delay > 0: sleep(delay) - send_update_with_same_packet_id(packet_id) - if receive_ok(): break -``` - -### 16.10 Weblist client query - -YaTQA only notes that Weblist client queries use TCP to: - -```text -weblist.teamspeak.com:2010 -``` - -The public YaTQA document does not provide a full client query packet specification. Treat this as an intentionally incomplete area: a compatible client should not require Weblist querying for direct server connection. Implement it as an optional discovery service only if you independently document the wire format. - -### 16.11 Badge metadata protocol - -TeamSpeak badges are stored in server/client-visible data as GUIDs, but to render them a client also needs the asset base filename/URL metadata. - -#### 16.11.1 Storage and cache behavior - -YaTQA states: - -- Badges are stored on the server as GUIDs. -- The client also needs a filename/base URL for the images. -- Images are cached under `cache\badges`. -- The badge metadata list is binary encoded. -- The list is stored unchanged as a `varchar` in `settings.db`. -- The same `badges` table stores a timestamp. According to YaTQA, the name is misleading: it is not the timestamp of the list itself, but the time when the list should be downloaded again. -- In the documented 3.1 beta era, refresh was 24 hours after download. - -#### 16.11.2 Badge list Protobuf structure - -The badge metadata list uses the Protobuf dialect: - -```text -field 1: BigNum revision? (not badge count; YaTQA observed value 15 with 13 badges) -field 2: BigNum Unix timestamp, likely last modification time -field 3: repeated length-delimited nested badge record - nested field 1: GUID string, 36 chars including 4 hyphens and 32 hex digits - nested field 2: badge name, UTF-8 - nested field 3: URL base - nested field 4: description, UTF-8 - nested field 5: BigNum Unix timestamp, likely last modification time - nested field 6: BigNum unknown, observed values 1..3 -``` - -Parser: - -```pseudo -function parse_badge_list(bytes): - root = decode_ts_protobuf(bytes) - revision = root[1][0].varint - modified = root[2][0].varint - badges = [] - for rec_bytes in root[3]: - r = decode_ts_protobuf(rec_bytes.bytes) - badges.append({ - 'guid': utf8(r[1][0].bytes), - 'name': utf8(r[2][0].bytes), - 'asset_base': utf8(r[3][0].bytes), - 'description': utf8(r[4][0].bytes), - 'modified': r[5][0].varint, - 'unknown_rank_or_type': r[6][0].varint, - }) - return { 'revision': revision, 'modified': modified, 'badges': badges } -``` - -#### 16.11.3 Asset URL construction - -For each badge `asset_base`: - -| UI location | Suffix | -|---|---| -| Badge window / larger client display | `_64.png` | -| Server tree small icon | `.svg` in later clients; older clients used `_16.png`. | -| myTeamSpeak detail/vector display | `_details.svg`; older documentation mentions `.svg` for an earlier variant. | - -Renderer behavior: - -```pseudo -function badge_assets(base): - return { - 'large_png': base + '_64.png', - 'tree_svg': base + '.svg', - 'legacy_tree_png': base + '_16.png', - 'details_svg': base + '_details.svg', - } -``` - -#### 16.11.4 Unknown badges - -A robust badge renderer must gracefully handle a GUID not found in the current metadata list: - -- Display the GUID or a generic badge placeholder. -- Queue a badge metadata refresh. -- Do not crash the tree renderer or user list. -- Keep the raw GUID in the state model so metadata can be resolved later. - -#### 16.11.5 Disabling badges on a server instance - -YaTQA describes a binary-patching trick to disable `client_badges` by editing the server executable and replacing one character in the `client_badges` string. This is not a recommended engineering practice for production systems. For development documentation, the important takeaway is not to implement this patch, but to recognize that badge display is ultimately controlled by a server-visible variable name and client interpretation. - -### 16.12 Public-suffix/TLD appendix behavior - -YaTQA includes historical notes and a large old TLD list used by TS3 3.0.x. This matters because address resolution and SRV TSDNS candidate generation depend on deciding which labels are public suffixes and which labels are registrable domains. - -#### 16.12.1 TS 3.1-beta2+ behavior - -YaTQA states that TeamSpeak 3.1 generally does not resolve top-level domains. In 3.1-beta2, the client embedded a public-suffix-like list. The list itself is stored in Punycode, even though clients 3.0.20 to 3.1-beta2 did not fully support Punycode; 3.1-beta3 restored Punycode support. - -Implementation recommendation: - -- Use the current Public Suffix List for modern behavior. -- Add compatibility fixtures for old TeamSpeak candidate-domain behavior. -- Do not rely on YaTQA's old 3.0.x TLD list as a current public suffix list. - -#### 16.12.2 TS 3.0.20 and 3.1-beta1 behavior - -These versions did not look up top-level domains. This caused problems for domains delegated at the third level such as `.il`, `.ua`, `.za`, and historically `.uk`, `.nz`, `.au`, `.ni` because second-level selection could be wrong. - -#### 16.12.3 TS 3.0.x old embedded TLD list - -YaTQA says the 3.0.x list in client 3.0.18.1 and likely through 3.0.19.4 was from early October 2007. It includes obsolete or old entries such as `.an` and `.gb`, lacks many later TLDs, and contains old IDN test domains that no longer exist. You should only use this list for historical compatibility tests. - -Representative entries listed by YaTQA include: - -```text -aero arpa asia biz cat com coop edu gov info int jobs mil mobi museum name net org pro tel travel -ac ad ae af ag ai al am an ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bv bw by bz -ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr -ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp -ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lu lt lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt -mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru -rw sa sb sc sd se sg sh si sj sk sl sm sn so sr st su sv sy sz tc td tf tg tj tk tl tm tn tp to tr tt tv tw tz ua ug -uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw -``` - -Representative IDN/Punycode entries include: - -```text -xn--fiqs8s xn--fiqz9s xn--fzc2c9e2c xn--j6w193g xn--kprw13d xn--kpry57d -xn--mgbaam7a8h xn--mgbayh7gpa xn--mgberp4a5d4ar xn--o3cw4h xn--p1ai xn--pgbs0dh -xn--wgbh1c xn--wgbl6a xn--xkc2al3hye2a xn--ygbi2ammx -xn--0zwm56d xn--11b5bs3a9aj6g xn--80akhbyknj4f xn--9t4b11yi5a xn--deba0ad -xn--g6w251d xn--hgbk6aj7f53bba xn--hlcj6aya9esc7a xn--jxalpdlp xn--kgbechtv xn--zckzah -``` - -### 16.13 Implementation checklist for auxiliary protocol support - -Use this checklist when building a TS3-compatible client or administration tool. - -| Area | Must implement | Should implement | Compatibility notes | -|---|---|---|---| -| Protobuf | Varint, length-delimited, nested records, repeated fields. | Generic unknown-field preservation. | Used by updates, Blacklist2, badge list. | -| Updates | Parse modern Protobuf metadata. | Historical UDP probes only for old-client testing. | Verify signatures/hashes before installing. | -| File transfer | `ftinit*` bootstrap + raw TCP token + bytes. | Resume/partial validation. | Do not escape token or file payload. | -| TSDNS | TCP 41144 query ending `0A 0D 0D 0D 0A`. | `$PORT`, `404`, no-response semantics. | Modern clients rely more on explicit SRV. | -| Server nickname | HTTPS lookup for dotless names. | Caching and retry policy. | HTTPS only. | -| DNS/SRV | `_ts3._udp`, `_tsdns._tcp`, A/AAAA, Punycode. | Version-specific compatibility modes. | Do not continue to later resolution methods after one complete solution fails to connect if strict TS behavior is needed. | -| Blacklist v1 | UDP `ip4:` request, status 0/1/2. | Fail-open on timeout. | Mostly historical. | -| Blacklist2 | HTTPS POST + Protobuf request/response. | IP/domain/UID/license status model. | Cache behavior can affect domain-vs-IP retry. | -| Weblist server | UDP key request + data update. | Retry timing and packet-id reuse. | Optional unless implementing server reporting. | -| Weblist client | Optional. | Document separately if implemented. | YaTQA public page is incomplete here. | -| Badges | Protobuf badge list parse, GUID-to-asset mapping. | Metadata cache refresh and unknown-GUID handling. | Asset suffixes differ by UI location and era. | - -### 16.14 Integration with the rest of this manual - -The auxiliary protocol layer should not be mixed directly into your ServerQuery parser. Architect it as separate modules: - -```text -core/ - query/ # text ServerQuery protocol - voice_client/ # encrypted UDP client protocol (if implementing full voice client) - file_transfer/ # ftinit + raw TCP file transfer - resolver/ # nickname, DNS SRV, SRV TSDNS, TSDNS, A/AAAA - protobuf/ # BigNum and nested message decoder/encoder - metadata/ - updates/ # version metadata parser - badges/ # badge list parser/cache - blacklist/ # blacklist v1/v2 checks - weblist/ # server reporting/client discovery -``` - -Testing guidance: - -- Create golden byte fixtures for Protobuf varint edge cases: 0, 1, 127, 128, 255, 300, 16384, max u32, max u64. -- Create TSDNS fixtures with normal result, `$PORT`, `404`, and timeout/no response. -- Create DNS fixtures for standard port, non-standard port, multiple servers, website-on-different-IP, and IDN/Punycode domains. -- Create Blacklist2 fixtures for OK, greylisted, blacklisted by IP, blacklisted by domain, blacklisted by server UID, and malformed response. -- Create badge fixtures with known GUID, unknown GUID, old `_16.png`, new `.svg`, and detail SVG. -- Create file-transfer tests that verify the token bytes are sent exactly once and not escaped. - - -*** - - -## 17. Low-Level Client Protocol Notes from ReSpeak - -The ReSpeak `tsdeclarations` and `tsclientlib` projects provide public insight into the non-Query client protocol. - -Key points from `ts3protocol.md` and the Rust stack: - -- Packet size ceiling is around 500 bytes. -- Large command packets may be fragmented. -- Packet compression uses QuickLZ level 1 in relevant paths. -- Packet encryption uses EAX-style authenticated encryption. -- The protocol has explicit ACK, low-priority ACK, and Pong semantics. -- Connection initiation includes a puzzle / proof step to reduce abuse. -- Connection creation is expensive compared with normal message sending. - -Architecture: - -```text -UDP transport - -> packet framing - -> encryption/decryption - -> compression/decompression - -> ACK/retransmission handling - -> packet type dispatch - -> state/bookkeeping updates - -> application events -``` - -If you implement a full custom client, do not start with UI. Start with: - -1. Packet parser/serializer tests. -2. Crypto handshake tests. -3. ACK/retransmit tests. -4. Bookkeeping/state tests. -5. Only then add UI/audio features. - - -*** - - -## 18. ReSpeak Repository Implementation Guide - -### 18.1 `tsdeclarations` - -Purpose: machine-readable TS3 metadata. - -Important files: - -| File | Use | -|---|---| -| `Errors.csv` | Generate error enum and message dictionary | -| `Permissions.csv` | Generate permission constants and grant mapping | -| `Messages.toml` | Generate command/request/response types | -| `Book.toml` | Generate state/bookkeeping structures | -| `Enums.toml` | Generate enums | -| `MessagesToBook.toml` | Map protocol messages to state updates | -| `BookToMessages.toml` | Map state needs to protocol messages | -| `Versions.csv` | Historical versions | -| `Badges.csv` | Badge metadata | -| `ts3protocol.md` | Protocol notes | - -Recommended code generation pipeline: - -```text -CSV/TOML declarations - -> parser - -> intermediate schema - -> generated language types - -> command builders - -> response parsers - -> documentation tables -``` - -Generated command example: - -```rust -pub struct ServerEdit { - pub virtualserver_name: Option, - pub virtualserver_maxclients: Option, - pub virtualserver_password: Option, - // ... -} -``` - -Generated error example: - -```rust -pub enum Ts3ErrorCode { - Ok = 0, - CommandNotFound = 256, - ClientInvalidId = 512, - ClientIsFlooding = 524, - ParameterInvalid = 1538, - ParameterConvert = 1540, - PermissionInsufficient = 2568, -} -``` - -### 18.2 `tsclientlib` - -Purpose: Rust library for custom TS3 clients and bots. - -Architecture: - -```text -tsproto-types # primitive protocol types -tsproto-structs # protocol structures -tsproto-packets # packet framing -tsproto # low-level protocol runtime -ts-bookkeeping # server state model -tsclientlib # high-level API -``` - -Common client pattern: - -```rust -use tsclientlib::{Connection, DisconnectOptions, Identity, StreamItem}; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let identity = Identity::new_from_str("IDENTITY_STRING")?; - let mut conn = Connection::build("127.0.0.1") - .identity(identity) - .log_commands(true) - .connect()?; - - while let Some(event) = conn.events().next().await { - let event = event?; - if matches!(event, StreamItem::BookEvents(_)) { - break; - } - } - - let state = conn.get_state()?; - println!("Connected to {}", state.server.name); - conn.disconnect(DisconnectOptions::new())?; - Ok(()) -} -``` - -Design cautions: - -- Implement reconnect policy yourself if not provided by your chosen version. -- Treat initial state as incomplete until bookkeeping events arrive. -- Keep identity loading separate from connection logic. -- Log protocol events during development but redact secrets. - -### 18.3 `Qint` - -Purpose: full alternative client reference. - -Architecture: - -```text -Frontend UI - -> Tauri desktop/mobile shell - -> Rust backend - -> tsclientlib - -> protocol/audio/state layers -``` - -Development lessons: - -- Separate frontend UI state from TS3 protocol state. -- Use a backend command/event bridge rather than letting the UI speak protocol directly. -- Platform differences matter: desktop and Android may need different audio backends. -- Treat Qint as a reference/fork base, not as a guaranteed maintained dependency. - -### 18.4 `ts3stats` - -Purpose: offline statistics and report generation. - -Pipeline: - -1. Put TeamSpeak server logs in `Logs/`. -2. Optionally put TS3AudioBot logs in `BotLogs/`. -3. Create `Settings.py`. -4. Run `CreateTimeGraphs.py`. -5. Open `Result/index.html`. - -Settings template: - -```python -from datetime import timedelta - -vips = ["Admin", "Bot"] -merges = [["AdminLaptop", "AdminDesktop"]] -maxUsers = 50 -botStats = True -inputFolder = "Logs" -inputFolderBot = "BotLogs" -outputFolder = "Result" -tempFolder = "temp" -slotLength = timedelta(minutes=10) -minTime = timedelta(hours=10) -minConnects = 8 -``` - -Architecture: - -```text -log files - -> parser - -> normalized sessions/events - -> aggregation slots - -> diagram modules - -> Jinja2 templates - -> static HTML report -``` - -### 18.5 `MahTsIdentity` - -Purpose: identity operations. - -Capabilities: - -- Export identity. -- Search vanity UID patterns. -- Use `_` as wildcard in patterns. -- Benchmark search speed. -- Improve identity security level. -- Use parallelism with configurable thread count. - -Conceptual commands: - -```bash -mah_ts_identity --export -i "IDENTITY_STRING" -mah_ts_identity MY_UID -mah_ts_identity AB_CD_ --threads 8 -mah_ts_identity --bench MY_UID -mah_ts_identity -i "IDENTITY_STRING" --level 32 -``` - -Design lesson: security-level and vanity searches are CPU-bound parallel workloads. Keep them separate from the main client runtime. - - -*** - - -## 19. Statistics and Analytics - -### 19.1 Log ingestion model - -For server log analytics, normalize logs into events: - -```json -{ - "timestamp": "2026-05-23T12:00:00Z", - "type": "client_connected", - "client_uid": "...", - "client_name": "Alice", - "channel_id": 12 -} -``` - -Then build sessions: - -```json -{ - "client_uid": "...", - "display_name": "Alice", - "connected_at": "...", - "disconnected_at": "...", - "duration_seconds": 3600 -} -``` - -### 19.2 Useful reports - -- User online time by day/week/month. -- Peak concurrent clients. -- Average session duration. -- Channel popularity. -- Bot usage statistics. -- VIP/user group activity. -- New vs. returning users. -- Longest sessions. - -### 19.3 Report architecture - -Use an offline pipeline: - -```text -raw logs -> parse -> normalize -> aggregate -> render HTML/PDF -``` - -Do not run heavy analytics in your live bot process. Use a separate job. - - -*** - - -## 20. Desktop Client Architecture - -A modern TS3 client can follow the Qint-style split: - -```text -UI frontend - - channel tree - - user list - - chat panels - - settings - - file browser - - channel editor - -Backend service - - connection manager - - identity manager - - protocol runtime - - state reducer - - audio bridge - - file-transfer worker - -Protocol libraries - - tsclientlib or equivalent - - declaration-generated types -``` - -### 20.1 State synchronization - -Recommended event flow: - -```text -Protocol event - -> backend reducer - -> normalized app state - -> frontend event emit - -> UI render -``` - -Do not let UI components directly mutate protocol state. They should request backend actions. - -### 20.2 File browser - -A file browser needs: - -- Channel file list query. -- Permission checks. -- File-transfer token request. -- TCP transfer worker. -- Progress events. -- Cancellation. - -### 20.3 Channel editor - -A channel editor should: - -- Load editable fields. -- Validate field types and limits. -- Use `channeledit` only with mutable fields. -- Display failed permission when returned. - - -*** - - -## 21. Implementation Checklist - -### 21.1 Minimum ServerQuery admin tool - -- [ ] TCP Query transport with LF-CR line endings. -- [ ] Escape/unescape functions. -- [ ] Response parser with row and error handling. -- [ ] Login and server selection with `-virtual`. -- [ ] Command builder with typed parameter validation. -- [ ] Error dictionary. -- [ ] Command scheduler with anti-flood delay. -- [ ] `serverinfo`, `clientlist`, `channellist`, `servergrouplist` support. -- [ ] Event subscriptions and reducer. -- [ ] Permission display with failed permission mapping. - -### 21.2 Full administration system - -- [ ] Virtual server create/delete/edit. -- [ ] Client database search and info. -- [ ] Channel create/edit/delete/move. -- [ ] Server and channel groups. -- [ ] Permission assignment and evaluation. -- [ ] Privilege key lifecycle. -- [ ] Ban and complaint management. -- [ ] File-transfer module. -- [ ] Snapshot create/deploy/verify. -- [ ] Backup/migration planner. -- [ ] Audit log. - -### 21.3 Custom TS3 client or bot - -- [ ] Identity loader/generator. -- [ ] UDP protocol connection. -- [ ] Handshake and puzzle handling. -- [ ] Packet encryption/decryption. -- [ ] Compression/decompression. -- [ ] Fragmentation support. -- [ ] ACK/retransmit logic. -- [ ] Bookkeeping state model. -- [ ] Audio I/O if voice is required. -- [ ] Reconnect strategy. -- [ ] UI/backend boundary if desktop client. - -### 21.4 Identity tool - -- [ ] Import/export identity format. -- [ ] Public key extraction. -- [ ] UID generation. -- [ ] Security level calculation. -- [ ] Parallel offset search. -- [ ] Vanity pattern matcher. -- [ ] Benchmarking. -- [ ] Secret redaction. - -### 21.5 Stats processor - -- [ ] Log parser. -- [ ] Bot log parser. -- [ ] Session reconstruction. -- [ ] User merge aliases. -- [ ] VIP highlighting. -- [ ] Aggregation slots. -- [ ] HTML report rendering. -- [ ] Static asset packaging. - - -*** - - -## 22. Troubleshooting - -### 22.1 No reply from server - -Likely causes: - -- Server is not running. -- Wrong host. -- Wrong port. -- UDP 9987 blocked. -- Non-default port omitted from the client address. - -Fix: - -- Test host reachability. -- Confirm server process. -- Confirm voice port. -- Use `host:port` explicitly. -- Check firewall/NAT. - -### 22.2 Invalid client ID (`512`) - -Likely cause: persisted `clid` from a previous session. - -Fix: - -- Refresh with `clientlist`. -- Store `client_unique_identifier` or `client_database_id` for persistence. - -### 22.3 Invalid parameter (`1538`) - -Likely causes: - -- Typo in parameter name. -- Parameter not valid for that command. -- Attempt to edit a read-only field. - -Fix: - -- Validate against generated declarations. -- Check YaTQA variable matrix behavior. - -### 22.4 Convert error (`1540`) - -Likely causes: - -- Invalid numeric format. -- Signed/unsigned mismatch. -- Value outside allowed range. -- Attempt to send `-1` where unsigned value is expected. - -Fix: - -- Validate integer ranges. -- Convert icon IDs using signed/unsigned 32-bit conversion. - -### 22.5 Client is flooding (`524`) - -Likely cause: commands sent too quickly or expensive actions repeated. - -Fix: - -- Add delay/backoff. -- Track anti-flood budget. -- Cache expensive command results. - -### 22.6 Insufficient permissions (`2568`) - -Fix: - -- Extract `failed_permid` if returned. -- Map to permission name. -- Show required permission to admin. -- Check power/needed power pairs. - -### 22.7 Snapshot deploy fails - -Likely causes: - -- Hash mismatch. -- Encoding problem. -- Snapshot from incompatible server version. -- Missing permission. -- Snapshot contains old or corrupted data. - -Fix: - -- Recompute hash. -- Normalize line endings. -- Verify UTF-8/CESU-8 handling. -- Test on staging server. - - -*** - - -## 23. Security Best Practices - -- Store Query credentials in environment variables or secret stores. -- Never log Query passwords. -- Never log identity private keys. -- Treat snapshots as privileged secrets. -- Restrict ServerQuery by IP allowlist. -- Prefer least-privilege Query users over `serveradmin` for applications. -- Redact tokens from logs after creation. -- Rate-limit public admin APIs to protect the TS3 server from command floods. -- Validate all user-provided server/channel/client names before passing to Query commands. -- Keep file-transfer paths constrained and normalized. - - -*** - - -## 24. Recommended Engineering Plan - -### Phase 1: Data and declarations - -- Import errors and permissions. -- Define command schemas. -- Build escape/unescape and parser tests. - -### Phase 2: Query client - -- Implement transport. -- Implement login/use/serverinfo/clientlist/channellist. -- Implement typed command builder. -- Implement error handling. - -### Phase 3: Event-driven state - -- Add notifications. -- Build state reducer. -- Add deduplication. -- Add reconnect and resubscribe behavior. - -### Phase 4: Admin features - -- Add channels, groups, permissions, tokens, bans, complaints. -- Add permission explanations. -- Add audit log. - -### Phase 5: Backup and migration - -- Add snapshot create/deploy/verify. -- Add file-transfer backup. -- Add full migration checklist. - -### Phase 6: Identity and client protocol - -- Add identity import/export. -- Add security-level worker. -- If needed, integrate `tsclientlib` or equivalent UDP protocol stack. - -### Phase 7: Analytics - -- Add log ingestion. -- Add report generation. -- Add scheduled offline jobs. - - -*** - - -## 25. Reference URLs - -Primary resources: - -- https://yat.qa/ressourcen/ -- https://yat.qa/resources/ -- https://yat.qa/resources/server-error-codes/ -- https://yat.qa/resources/permission-ids/ -- https://yat.qa/resources/client-versions/ -- https://yat.qa/ressourcen/definitionen-und-algorithmen/ -- https://yat.qa/ressourcen/server-query-kommentare/ -- https://yat.qa/ressourcen/server-query-notify/ -- https://yat.qa/ressourcen/variablen-parameter/ -- https://yat.qa/ressourcen/voice-client-anti-flood/ -- https://yat.qa/ressourcen/sicherheitsstufe/ -- https://yat.qa/ressourcen/snapshots/ -- https://yat.qa/ressourcen/protokolle/ -- https://yat.qa/ressourcen/abzeichen-badges/ - -ReSpeak repositories: - -- https://github.com/ReSpeak/tsclientlib -- https://github.com/ReSpeak/ts3stats -- https://github.com/ReSpeak/tsdeclarations -- https://github.com/ReSpeak/Qint -- https://github.com/ReSpeak/MahTsIdentity/ - -Official support references: - -- TeamSpeak 3 support category -- TeamSpeak 3 ports -- TeamSpeak 3 server setup -- TeamSpeak 3 DNS SRV records -- TeamSpeak 3 snapshots -- TeamSpeak 3 migration -- TeamSpeak 3 plugin SDK guidance - - -*** - - -## 26. Final Notes - -This manual should be sufficient to start serious TeamSpeak 3 development without re-reading the source websites for every implementation choice. For exact binary compatibility in the low-level UDP client protocol, the best path is still to use or study `tsclientlib` and `tsdeclarations` directly, because the protocol includes encryption, compression, fragmentation, acknowledgement, and handshake behavior that must be tested against a real server. - -For administration tooling, YaTQA's Query notes and variable matrices are the most important sources of real behavior. For runtime clients, ReSpeak's Rust stack is the strongest public implementation reference. For migration and recovery, always remember that snapshots are not complete backups. - - -*** - - -# Expanded Engineering Specification (Version 2) - -This version expands the earlier manual into a more implementation-oriented reference. It is written to be used as a build specification for a TeamSpeak 3-compatible administration system, ServerQuery client, bot framework, identity utility, migration tool, and log statistics pipeline. - -## 21. Design Goals and Non-Goals - -### 21.1 Design goals - -A complete TS3 development stack should support: - -1. **ServerQuery automation**: login, server selection, virtual server CRUD, channel CRUD, client queries, permission management, message sending, file transfer setup, snapshots, tokens, bans, complaints, and log reads. -2. **Event-driven state tracking**: maintain an in-memory model of servers, channels, clients, groups, permissions, and text events using `servernotifyregister` and periodic reconciliation. -3. **Typed protocol declarations**: generate message builders, response parsers, event models, and validation metadata from machine-readable schemas such as `tsdeclarations`. -4. **Permission explanation**: compute and explain effective permissions, not just read assigned permissions. -5. **Safe backup and restore**: distinguish between logical snapshots, file system assets, instance database copies, and YaTQA-style partial backup when snapshot permission is missing. -6. **Identity tooling**: import/export identities, compute UID, compute and raise security level, and optionally search vanity UID prefixes or patterns. -7. **Statistics tooling**: parse server logs and bot logs into time ranges, session graphs, client presence records, and usage summaries. -8. **Compatibility awareness**: handle old TS3 quirks such as CESU-8, inconsistent Query documentation, historical DNS/TSDNS resolution differences, and snapshot hash recalculation. - -### 21.2 Non-goals - -A self-contained TS3-compatible system still should not pretend to replace proprietary or unavailable components where public material is incomplete. Specifically: - -- The official TeamSpeak voice codec implementation and exact proprietary client behavior cannot be fully reconstructed from YaTQA alone. -- ClientQuery is not documented as thoroughly as ServerQuery in the public YaTQA resources. -- Some YaTQA downloads are historical binaries or inaccessible in a crawler; their existence and intended function can be documented, but their source cannot be reconstructed honestly without access to the downloads. -- Official Plugin SDK details are outside the public YaTQA material and should be taken from the TeamSpeak Plugin SDK package when building native plugins. - -## 22. Recommended Project Layout - -A production-grade project should be modular. The following layout works for Rust, TypeScript, Go, Java, C#, or Python with minor naming changes. - -```text -teamtalk-ts3-stack/ - docs/ - manual.md - protocol-notes.md - migration-runbooks.md - declarations/ - errors.csv - permissions.csv - messages.toml - events.toml - badges.csv - versions.csv - packages/ - ts3-types/ - ids, enums, typed wrappers, common model objects - ts3-query/ - raw text codec, escaping, parser, command builder - ts3-events/ - notify event parser and state reducers - ts3-permissions/ - permission calculation and explanation engine - ts3-snapshot/ - snapshot parser, serializer, hash verifier, migrator - ts3-files/ - file transfer token workflow, icon/avatar helpers - ts3-identity/ - identity import/export, UID/security-level calculations - ts3-dns/ - SRV/TSDNS resolver, resolution trace logs - ts3-stats/ - log parser, sessions, graphs, report generation - ts3-client-runtime/ - connection lifecycle, event bus, reconnection, state cache - apps/ - admin-web/ - bot/ - desktop-client/ - cli/ - tests/ - protocol-fixtures/ - snapshot-fixtures/ - query-responses/ - log-fixtures/ -``` - -A key principle is that **protocol declarations are data**, not hand-coded constants scattered through the application. Error IDs, permission IDs, message schemas, badge GUIDs, and enum mappings should live in a single declarations package and be consumed by code generation. - -## 23. Core Data Types - -Use strongly typed wrappers even if the wire protocol uses plain integers. It prevents mixing session-scoped IDs with persistent IDs. - -```text -ServerId integer, virtual server ID inside the server instance -ServerPort integer, UDP voice port -ChannelId integer, channel ID, persistent within a virtual server snapshot but may remap on restore -ClientId integer, session-scoped client ID; invalid after disconnect -ClientDatabaseId integer, persistent database ID within a virtual server -ClientUid string, persistent identity UID -ServerGroupId integer -ChannelGroupId integer -PermissionId integer -PermissionName string, e.g. b_virtualserver_modify_name -Token string, privilege key string -IconId signed/unsigned 32-bit value, depending on context -FileTransferId 0..65535 client-provided transfer correlation ID -``` - -### 23.1 Session IDs vs persistent IDs - -The most common implementation bug is confusing `clid` with `client_database_id` or `client_unique_identifier`. - -- `clid`: current online session only. Never store as a durable key. -- `client_database_id`: durable inside one virtual server. Changes if the client is imported into another server database. -- `client_unique_identifier`: identity-level stable ID. Best cross-server user identity key. - -### 23.2 Server, channel, and file paths - -Use explicit path types: - -```text -VirtualServerPath: - sid: ServerId - port: ServerPort optional - -ChannelFilePath: - channel_id: ChannelId - path: string - -IconPath: - /icon_ - -AvatarPath: - /avatar_ -``` - -Do not assume all file commands encode avatar paths in exactly the same way. YaTQA notes that avatar handling differs between upload/download/info and deletion semantics, so build helpers per command rather than one global avatar path function. - -## 24. ServerQuery Wire Format - -ServerQuery is line-oriented text over TCP or SSH Query. A robust implementation should treat it as a protocol, not as string concatenation. - -### 24.1 Command structure - -```text -command [parameter=value ...] [array-row|array-row ...] -``` - -Typical examples: - -```text -login serveradmin password -use sid=1 -virtual -serverinfo -serveredit virtualserver_name=Example\sServer virtualserver_maxclients=64 -clientlist -uid -groups -away -voice -times -info -country -ip -badges -``` - -### 24.2 Response structure - -Typical successful response: - -```text -key=value key=value|key=value key=value -error id=0 msg=ok -``` - -Typical failed response: - -```text -error id=2568 msg=insufficient\sclient\spermissions failed_permid=... -``` - -Some commands return arrays separated by `|`. Some commands return empty values. Some parameters may be absent if permissions are insufficient. - -### 24.3 Escaping model - -Implement escaping as a reversible codec. A practical codec table: - -| Plain character | Query escape | -|---|---| -| space | `\s` | -| slash `/` | `\/` | -| backslash `\` | `\\` | -| pipe `|` | `\p` | -| bell | `\a` | -| backspace | `\b` | -| form feed | `\f` | -| newline | `\n` | -| carriage return | `\r` | -| tab | `\t` | -| vertical tab | `\v` | - -Decode after tokenization, not before, because `\p` represents a literal pipe inside a value and must not split the row. - -### 24.4 Robust tokenizer pseudocode - -```pseudo -function parse_response_line(line): - rows = split_unescaped(line, '|') - parsed_rows = [] - for row in rows: - tokens = split_unescaped(row, ' ') - object = {} - flags = [] - for token in tokens: - if token == '': continue - if token starts with '-': flags.append(token[1:]) - else: - key, raw_value = split_first(token, '=') - object[key] = query_unescape(raw_value or '') - parsed_rows.append({ fields: object, flags: flags }) - return parsed_rows -``` - -### 24.5 Command builder pseudocode - -```pseudo -function build_command(name, params, flags=[], rows=[]): - parts = [name] - for flag in flags: - parts.append('-' + flag) - if rows is empty: - for key, value in params: - parts.append(key + '=' + query_escape(to_string(value))) - else: - serialized_rows = [] - for row in rows: - row_parts = [] - for key, value in row: - row_parts.append(key + '=' + query_escape(to_string(value))) - serialized_rows.append(join(row_parts, ' ')) - parts.append(join(serialized_rows, '|')) - return join(parts, ' ') + '\n' -``` - -### 24.6 Return codes for correlation - -Every command may include `return_code`. Use it for pipelining or correlation: - -```text -serverinfo return_code=req_000123 -``` - -When parsing, attach the returned `return_code` from the final error line to the original command future/promise. - -### 24.7 Connection lifecycle - -Recommended state machine: - -```text -Disconnected - -> TCPConnected - -> BannerReceived - -> Authenticated optional - -> ServerSelected optional - -> Subscribed optional - -> Closing - -> Disconnected -``` - -Rules: - -- Read the initial server banner before sending commands. -- Prefer explicit `quit` before closing. -- Use `use sid= -virtual` as a stable server selection pattern. -- Resubscribe notifications after login, relogin, or server switch. -- Reconcile state with periodic `serverinfo`, `channellist`, and `clientlist` because notifications can be lost during reconnects. - -## 25. ServerQuery Command Families - -This section provides implementation targets for a complete administration client. - -### 25.1 Instance-level commands - -```text -version -hostinfo -instanceinfo -bindinglist -serverlist -servercreate -serverdelete -serverstart -serverstop -serverprocessstop -``` - -Minimum behavior: - -- `serverlist` should support flags for UID, short view, all servers, and only running servers depending on server version. -- `servercreate` returns at least `sid`, `virtualserver_port`, and initial token. -- `serverdelete` must stop a server before deletion if required by server state. -- `serverstart` and `serverstop` operate on virtual servers by `sid`. - -### 25.2 Virtual server commands - -```text -use -serverinfo -serverrequestconnectioninfo -serveredit -servertemppasswordadd -servertemppassworddel -servertemppasswordlist -servergrouplist -servergroupadd -servergroupdel -servergroupcopy -servergroupaddperm -servergroupdelperm -servergrouppermlist -``` - -Key implementation notes: - -- `serverinfo` returns more connection and packet statistics than some related commands. -- Some quota and bandwidth values may appear as huge unsigned 64-bit values representing `-1` in another interpretation. Display them as “unlimited” when appropriate rather than blindly showing 18446744073709551615. -- `serveredit` accepts many fields; generate its builder from declarations. - -### 25.3 Channel commands - -```text -channellist -channelinfo -channelfind -channelcreate -channeldelete -channeledit -channelmove -channelpermlist -channeladdperm -channeldelperm -``` - -State model: - -```text -Channel { - cid - pid - order - name - topic - description - codec - codec_quality - maxclients - maxfamilyclients - password_flag - permanent_flag - semi_permanent_flag - default_flag - icon_id - delete_delay -} -``` - -### 25.4 Client commands - -```text -clientlist -clientinfo -clientfind -clientmove -clientkick -clientpoke -clientupdate -clientedit -clientdblist -clientdbinfo -clientdbfind -clientdbedit -clientgetdbidfromuid -clientgetnamefromuid -clientgetnamefromdbid -clientgetuidfromclid -``` - -State model: - -```text -ClientSession { - clid - cid - client_database_id - client_unique_identifier - client_nickname - client_type - client_servergroups - client_channel_group_id - client_talk_power - client_is_talker - client_is_priority_speaker - client_icon_id - client_country - client_badges -} -``` - -### 25.5 Messaging commands - -```text -sendtextmessage targetmode=<1|2|3> target= msg= -``` - -Target modes commonly map to: - -```text -1 = private client -2 = channel -3 = server -``` - -Always escape message text. For long messages, enforce Query line limits and consider splitting. - -### 25.6 Permission commands - -```text -permissionlist -permidgetbyname -permoverview -permget -permfind -servergroupaddperm -servergroupdelperm -clientaddperm -clientdelperm -channeladdperm -channeldelperm -channelgroupaddperm -channelgroupdelperm -channelclientaddperm -channelclientdelperm -``` - -Recommended API: - -```pseudo -PermissionAssignment { - scope: ServerGroup | ServerClient | Channel | ChannelGroup | ChannelClient - scope_id: integer(s) - permid: PermissionId - permsid: PermissionName optional - value: integer or boolean - skip: boolean - negated: boolean - grant: integer optional -} -``` - -### 25.7 Tokens / privilege keys - -Commands: - -```text -privilegekeyadd -privilegekeydelete -privilegekeylist -privilegekeyuse -``` - -Legacy aliases: - -```text -tokenadd -tokendelete -tokenlist -tokenuse -``` - -Custom token metadata should be escaped twice when using parameterized strings inside parameterized Query values. - -## 26. Notify Event System - -A high-quality bot or client should be event driven. - -### 26.1 Subscription lifecycle - -```text -servernotifyregister event=server -servernotifyregister event=channel id=0 -servernotifyregister event=textserver -servernotifyregister event=textchannel -servernotifyregister event=textprivate -servernotifyregister event=tokenused -``` - -Rules: - -- Subscriptions are per Query connection. -- Subscriptions disappear after logout, relogin, server switch, or disconnection. -- Subscribe one event class at a time. -- Rebuild subscriptions after reconnect. - -### 26.2 Event parser - -Event lines start with a notification name, e.g.: - -```text -notifycliententerview cfid=0 ctid=1 reasonid=0 clid=5 client_unique_identifier=... -notifyclientleftview cfid=1 ctid=0 reasonid=8 reasonmsg=left\sthe\sserver clid=5 -notifyclientmoved ctid=12 reasonid=1 clid=5 -notifytextmessage targetmode=1 msg=hello invokerid=5 invokername=Alice invokeruid=... -``` - -Parser design: - -```pseudo -function parse_notify(line): - name, rest = split_first(line, ' ') - rows = parse_response_line(rest) - return NotifyEvent(name=name, rows=rows) -``` - -### 26.3 State reducer examples - -```pseudo -on notifycliententerview(event): - client = client_from_fields(event.fields) - state.clients[client.clid] = client - state.channels[client.cid].clients.add(client.clid) - -on notifyclientleftview(event): - clid = event.fields.clid - remove_client_from_all_channels(clid) - delete state.clients[clid] - -on notifyclientmoved(event): - target = event.fields.ctid - for clid in parse_array(event.fields.clid): - move_client(clid, target) - -on notifychanneledited(event): - cid = event.fields.cid - update_changed_channel_fields(cid, event.fields) - if description_or_password_missing: - schedule channelinfo(cid) -``` - -### 26.4 Reconciliation strategy - -Notifications are not a replacement for periodic state sync. Use this strategy: - -```text -At connect: - serverinfo - channellist -topic -flags -voice -limits -icon - clientlist -uid -groups -away -voice -times -info -country -ip -badges - servergrouplist - channelgrouplist - -On event: - apply reducer - if event incomplete -> schedule targeted query - -Every N minutes: - refresh clientlist and channellist - compare state hash - repair drift -``` - -## 27. Permission Engine Specification - -### 27.1 Permission assignment scopes - -Effective permission is evaluated per permission. Do not calculate all permissions as one combined object without per-permission conflict logic. - -Sources in priority/evaluation model: - -1. Server groups -2. Client permissions on server level -3. Channel permissions -4. Channel group permissions -5. Client permissions on channel level - -### 27.2 Server group resolution - -When multiple server groups assign the same permission: - -```pseudo -function resolve_server_groups(assignments): - negated = filter(assignments, a.negated) - if negated is not empty: - candidates = negated - else: - candidates = assignments - return assignment_with_highest_effective_value_or_priority(candidates) -``` - -YaTQA notes the special behavior of negated groups. Implement negation explicitly and test it with multiple group memberships. - -### 27.3 Skip logic - -```pseudo -function has_skip_after_server_layer(server_group_effective, client_server_assignment): - if client_server_assignment exists: - return client_server_assignment.skip - else: - return server_group_effective.skip -``` - -If skip is true, ignore channel and channel group assignments. Channel-client assignments still need separate treatment according to scope because they are the most specific layer. - -### 27.4 Effective permission calculation pseudocode - -```pseudo -function effective_permission(client, channel, permission): - sg_assigns = all server-group assignments for client's groups and permission - sg_effective = resolve_server_groups(sg_assigns) - - server_client = assignment(client.dbid, permission, scope=ServerClient) - - if server_client exists: - current = server_client - skip = server_client.skip - else: - current = sg_effective - skip = sg_effective.skip if sg_effective else false - - if not skip: - channel_assignment = assignment(channel.cid, permission, scope=Channel) - if channel_assignment exists: - current = channel_assignment - - channel_group_assignment = assignment(client.channel_group_id, channel.cid, permission, scope=ChannelGroup) - if channel_group_assignment exists: - current = channel_group_assignment - - channel_client = assignment(client.dbid, channel.cid, permission, scope=ChannelClient) - if channel_client exists: - current = channel_client - - return current.value or default(permission) -``` - -### 27.5 Power / needed power pairs - -Many TS3 permissions are enforced as power comparisons: - -```text -i_client_kick_power >= i_client_needed_kick_power -i_client_ban_power >= i_client_needed_ban_power -i_group_modify_power >= i_group_needed_modify_power -i_permission_modify_power >= i_needed_modify_power_ -``` - -When displaying “why can’t I do this?” always show both sides: - -```text -Action: edit channel -Your i_channel_modify_power: 50 -Target channel i_channel_needed_modify_power: 75 -Result: denied because 50 < 75 -``` - -### 27.6 Grant permissions - -For permission ID `P`, grant permission is historically derived by adding 32768 in modern versions. The permission name form often uses `i_needed_modify_power_...`. A permission editor must verify grant power before allowing edits. - -## 28. Anti-Flood Model - -### 28.1 State variables - -```text -points_tick_reduce -points_needed_command_block -points_needed_ip_block -client_current_points -client_has_b_client_ignore_antiflood -client_has_b_client_ignore_bans -``` - -### 28.2 Tick model - -Every 0.5 seconds: - -```pseudo -client_current_points = max(0, client_current_points - points_tick_reduce) -``` - -### 28.3 Action model - -```pseudo -function perform_action(client, action_cost): - if not client.ignore_antiflood: - client.points += action_cost - - if action_cost >= 0 and not client.ignore_antiflood: - if client.points >= points_needed_command_block: - throw Error(524, 'client is flooding') - - execute_action() -``` - -### 28.4 Typical costs from YaTQA notes - -| Action family | Example | Points | -|---|---:|---:| -| Subscribe one channel/family | `channelsubscribe` | 15 | -| Unsubscribe one channel/family | `channelunsubscribe` | 5 | -| Subscribe all | `channelsubscribeall` | 20 | -| Unsubscribe all | `channelunsubscribeall` | 25 | -| Add ban | `banadd` | 25 | -| Ban connected client | `banclient` | 25 | -| Delete ban | `bandel` | 5 | -| List bans | `banlist` | 25 | -| Add complaint | `complainadd` | 25 | -| Delete complaint | `complaindel` | 5 | -| List complaints | `complainlist` | 25 | -| Permission list if uncached | `permissionlist` | 5 | -| Permission overview | `permoverview` | 5 | - -### 28.5 Client-side rate limiter - -```pseudo -class AntiFloodLimiter: - points = 0 - last_update = now() - - def decay(): - ticks = floor((now() - last_update) / 0.5s) - points = max(0, points - ticks * tick_reduce) - last_update += ticks * 0.5s - - def reserve(cost): - decay() - if points + cost >= command_block_threshold: - sleep_until_safe(cost) - points += cost -``` - -For admin tools, rate limiting should be enabled even when the current identity usually has ignore-antiflood rights because permissions can differ across servers. - -## 29. Snapshot Format - -### 29.1 Snapshot purpose - -A snapshot is a text representation of a virtual server state. It is useful for logical clone/restore but is not a full instance backup. - -Included: - -- Virtual server settings, except port assignment. -- Channels. -- Client database. -- Server groups and channel groups. -- Permission assignments. -- Group memberships. - -Excluded: - -- Uploaded files. -- Icons and avatars. -- Bans. -- Complaints. -- Offline messages. -- Voice server port assignment. - -### 29.2 Top-level structure - -```text -hash=| -``` - -Validation: - -```pseudo -function verify_snapshot(snapshot): - prefix, data = split_first(snapshot, '|') - expected = parse_hash(prefix) - actual = base64(sha1(bytes(data))) - return expected == actual -``` - -When modifying snapshot text, recalculate `hash=`. Be careful with encoding. Historical data may be CESU-8 or malformed UTF-8. - -### 29.3 Deployment behavior - -```text -serversnapshotdeploy -``` - -Behavior: - -- If a virtual server is selected with `use`, deployment may replace that selected server. -- If no virtual server is selected, deployment may create a new virtual server and return `sid` and `virtualserver_port`. -- With mapping options, deployment can return old-to-new channel mappings. - -### 29.4 Snapshot parser architecture - -```pseudo -Snapshot { - header_hash: string - virtual_server: Map - channels: List> - clients: List> - server_groups: List> - channel_groups: List> - permissions: List> - memberships: List> - raw_sections: List -} -``` - -Because snapshot sections can vary by server version, build a permissive parser: - -1. Verify hash if possible. -2. Tokenize rows using Query-style escaping. -3. Preserve unknown fields. -4. Preserve section order. -5. Re-serialize unknown fields unchanged unless explicitly transformed. - -### 29.5 Migration checklist - -Before migration: - -- Export snapshot. -- Copy `files/*` if file data matters. -- Copy icons and avatars. -- Record server port, because snapshot may not preserve it directly. -- Export or record bans and complaints separately if needed. - -After migration: - -- Deploy snapshot. -- Restore files. -- Restore icons and avatars. -- Check default groups. -- Check host banner, host button, and quotas. -- Check file transfer permissions. -- Check channel password flags. -- Run client and channel list comparison. - -## 30. File Transfer Protocol - -### 30.1 Query setup phase - -Upload: - -```text -ftinitupload clientftfid= name= cid= cpw= size= overwrite=<0|1> resume=<0|1> -``` - -Download: - -```text -ftinitdownload clientftfid= name= cid= cpw= seekpos= -``` - -The server responds with a file transfer token, IP, and port. - -### 30.2 Binary transfer phase - -Client opens a connection to returned IP/port and sends the token. Then: - -- For upload: stream raw bytes to server. -- For download: read raw bytes from server. - -No Query escaping is applied to the binary payload. - -### 30.3 Transfer correlation - -`clientftfid` is client-provided and should be unique per transfer in that client. Use a 16-bit counter and wrap safely. - -```pseudo -next_clientftfid = (next_clientftfid + 1) mod 65536 -``` - -### 30.4 File listing caveats - -`ftgetfilelist` may return stale or partial sizes immediately after upload. Build retry logic: - -```pseudo -function wait_until_file_stable(path): - last = null - stable_count = 0 - while stable_count < 2: - info = ftgetfileinfo(path) - if info.size == last: - stable_count += 1 - else: - stable_count = 0 - last = info.size - sleep(500ms) -``` - -## 31. Icon, Avatar, and Cache Algorithms - -### 31.1 Avatar filenames - -YaTQA describes avatar filenames as derived from the client unique identifier. Use a dedicated function instead of duplicating ad hoc string logic. - -```pseudo -function avatar_filename(client_uid): - return 'avatar_' + base64_hash_client_uid(client_uid) -``` - -Because exact legacy hash/base64 handling may vary across implementations, validate against known fixtures from a real TS3 client cache before deploying. - -### 31.2 Icon IDs - -Icons are identified through 32-bit values. The same bit pattern can appear as signed or unsigned. - -```pseudo -function as_signed_32(u): - if u >= 2^31: return u - 2^32 - return u - -function as_unsigned_32(i): - if i < 0: return i + 2^32 - return i -``` - -Store both forms: - -```text -IconId { - raw_u32 - signed_i32 - filename -} -``` - -### 31.3 Cache directory names - -Server cache folders are derived from server UIDs, essentially using Base64 transformation. Keep this in a separate module because cache paths are platform-specific: - -```text -Windows: %APPDATA%\TS3Client\cache -Windows: %APPDATA%\TS3Client\chats -``` - -## 32. Identity and Security Level - -### 32.1 Identity concepts - -A TS3 identity contains key material used to derive the client UID. A server does not rely on username/password for normal voice identity. Instead, authentication is based on the identity key and proof mechanisms. - -### 32.2 Security level - -Security level is proof-of-work: number of leading zero bits in a SHA-1 hash derived from public key material plus a nonce-like integer. - -```pseudo -function security_level(public_key_bytes, counter_string): - digest = sha1(public_key_bytes + ascii(counter_string)) - return count_leading_zero_bits_little_bit_order_within_big_endian_bytes(digest) -``` - -The bit-order detail is important. YaTQA describes big-endian byte order with little-endian bit order for counting leading zeros. - -### 32.3 Searching for a higher level - -```pseudo -function improve_security_level(identity, target_level): - counter = identity.counter - while true: - digest = sha1(identity.public_key + str(counter)) - if leading_zero_count(digest) >= target_level: - identity.counter = counter - return identity - counter += 1 -``` - -Parallel search: - -```pseudo -thread 0: counters 0, N, 2N, ... -thread 1: counters 1, N+1, 2N+1, ... -... -thread N-1: counters N-1, 2N-1, 3N-1, ... -``` - -MahTsIdentity uses Rust parallelism and supports vanity UID searches as well as security-level improvement. The same architecture applies in any language. - -### 32.4 Vanity UID search - -```pseudo -function search_vanity(pattern): - while not cancelled: - identity = generate_random_identity() - uid = compute_uid(identity) - if matches(pattern, uid): - return identity -``` - -Support wildcards: - -```text -_ or ? = any single character -literal characters = exact match -``` - -### 32.5 Operational guidance - -- Do not generate extremely high target levels unnecessarily; cost grows exponentially. -- Store identities encrypted at rest. -- Keep identity export/import compatible with the official client format where possible. -- Never log full private identity material. - -## 33. Protobuf and Auxiliary Protocols - -### 33.1 Varint / BigNum - -YaTQA describes TeamSpeak’s Protobuf-related BigNum/Varint behavior. Implement standard unsigned varint: - -```pseudo -function write_varint(x): - while true: - if x < 128: - write_byte(x) - return - write_byte((x & 0x7F) | 0x80) - x = x >> 7 - -function read_varint(): - shift = 0 - result = 0 - while true: - b = read_byte() - result |= (b & 0x7F) << shift - if (b & 0x80) == 0: - return result - shift += 7 -``` - -### 33.2 Field key - -```pseudo -wire_type = key & 7 -field_number = key >> 3 -``` - -Common wire types: - -```text -0 = varint -1 = 64-bit -2 = length-delimited -5 = 32-bit -``` - -### 33.3 Length-delimited field - -```pseudo -length = read_varint() -payload = read_bytes(length) -``` - -Payload may itself be nested Protobuf. - -### 33.4 Update metadata - -Modern TS3 clients use version metadata downloaded from TeamSpeak version endpoints. Historical versions used UDP messages to update servers. For a modern-compatible updater: - -1. Download version metadata. -2. Parse Protobuf entries. -3. Identify stable/beta/alpha client version records. -4. Compare build number and version string. -5. Download platform-specific updater image or installer if needed. - -Keep this as a compatibility feature, not as a security-sensitive auto-updater unless you verify signatures and transport security. - -## 34. DNS and TSDNS Resolution - -### 34.1 Preferred SRV record - -Use `_ts3._udp.` SRV records to map a user-friendly domain to host and port. - -Example: - -```text -_ts3._udp.example.com. 3600 IN SRV 0 5 9987 voice.example.com. -``` - -Resolution algorithm: - -```pseudo -function resolve_ts3(address): - if address contains explicit port: - return host, port - - srv = query_srv('_ts3._udp.' + address) - if srv exists: - return srv.target, srv.port - - tsdns = query_srv('_tsdns._tcp.' + parent_domain(address)) - if tsdns exists: - return query_tsdns(tsdns.target, address) - - return address, 9987 -``` - -### 34.2 TSDNS - -TSDNS maps names to `host:port` pairs. Modern clients should not rely on old implicit search behavior. Configure explicit `_tsdns._tcp` when TSDNS is required. - -### 34.3 Resolver trace - -For debugging, expose a resolver trace: - -```json -{ - "input": "voice.example.com", - "explicit_port": false, - "srv_query": "_ts3._udp.voice.example.com", - "srv_result": null, - "tsdns_query": "_tsdns._tcp.example.com", - "tsdns_result": "tsdns.example.com:41144", - "final": "1.2.3.4:9987" -} -``` - -## 35. Blacklist and Blacklist2 - -YaTQA documents blacklist-related protocols as part of non-query TS3 protocols. A safe implementation should treat blacklist calls as an external service integration. - -### 35.1 Data model - -```text -BlacklistQuery { - server_ip_v4 optional - server_ip_v6 optional - domain optional - server_uid optional - license_key_hash optional -} - -BlacklistResult { - blocked: boolean - reason optional - source: Blacklist | Blacklist2 - raw_response -} -``` - -### 35.2 Guidance - -- Do not block local administration purely because a blacklist service is unavailable. -- Cache negative results briefly. -- Treat positive blacklist results as security-sensitive and display details only to authorized admins. -- Keep raw responses for debugging, but avoid logging license material. - -## 36. Badges - -### 36.1 Badge data model - -```text -Badge { - guid: UUID - name: string - description: string - asset_base_url: string - details_svg optional - icon_64_png optional - icon_16_png optional - icon_svg optional - introduced optional - removed optional - redemption_code optional -} -``` - -### 36.2 Client rendering - -Client badge rendering should: - -1. Parse `client_badges` string into badge GUIDs and optional metadata. -2. Resolve GUID to local declaration table. -3. Choose best asset size for UI context. -4. Cache remote images. -5. Display unknown badge GUIDs gracefully. - -### 36.3 Badge declaration source - -Use `tsdeclarations/Badges.csv` as a machine-readable base and supplement with YaTQA badge notes for historical introduced/removed details. - -## 37. Low-Level TS3 Client Protocol Architecture - -The ReSpeak `tsdeclarations` and `tsclientlib` projects show a layered model for implementing a TS3-compatible client. - -### 37.1 Layer responsibilities - -```text -UDP packet layer - packet header parsing, packet ID, generation, flags - -Reliability layer - ack tracking, resend, ordering, fragmentation reassembly - -Crypto/compression layer - EAX-style encryption where applicable - compression and decompression - -Handshake layer - multi-step init handshake - identity proof - -Bookkeeping layer - transform protocol messages into server state - -Client API layer - connect, disconnect, subscribe, send message, audio, channel operations -``` - -### 37.2 Packet handling loop - -```pseudo -while socket open: - packet = udp_recv() - header = parse_header(packet) - if header.encrypted: - payload = decrypt(packet.payload) - if header.compressed: - payload = decompress(payload) - if header.fragmented: - fragment_buffer.add(header, payload) - if not complete: continue - payload = fragment_buffer.reassemble(header.fragment_group) - message = parse_message(payload) - reliability.ack(header.packet_id) - dispatch(message) -``` - -### 37.3 Sending loop - -```pseudo -function send_reliable(message): - payload = encode_message(message) - if should_compress(payload): payload = compress(payload) - fragments = fragment_if_needed(payload) - for fragment in fragments: - packet_id = next_packet_id() - packet = build_packet(packet_id, flags, fragment) - encrypted = encrypt_if_needed(packet) - send_udp(encrypted) - reliability.track(packet_id, encrypted) -``` - -### 37.4 State bookkeeping - -The client should not expose raw protocol messages directly to application code. Instead: - -```pseudo -on protocol_message(msg): - events = bookkeeping.apply(msg) - for event in events: - application_event_bus.emit(event) -``` - -Example application events: - -```text -ServerWelcome -ChannelCreated -ChannelDeleted -ChannelMoved -ClientEnteredView -ClientLeftView -ClientMoved -TextMessageReceived -PermissionChanged -AudioPacketReceived -ConnectionLost -``` - -## 38. Audio Client Implementation Notes - -A TS3-compatible voice client needs more than Query. - -### 38.1 Components - -```text -Audio input device -> Opus encoder -> packet scheduler -> UDP protocol -UDP protocol -> jitter buffer -> Opus decoder -> audio output device -``` - -### 38.2 Runtime tasks - -- Capture microphone frames at the codec-required sample rate. -- Encode frames. -- Send voice packets at the negotiated interval. -- Receive and reorder incoming audio packets. -- Decode and mix per-speaker streams. -- Apply mute/deafen/talk-power/channel-commander state. - -### 38.3 Bot mode - -A bot often does not need full playback. Minimal bot implementation: - -- Connect to server. -- Maintain state. -- Subscribe to text/private/server/channel events. -- Optionally capture or play audio. -- Avoid joining restricted channels unless configured. - -## 39. Statistics Pipeline - -Use `ts3stats` as the reference for offline log analytics. - -### 39.1 Input sources - -```text -TS3 server logs -TS3AudioBot logs optional -Settings.py-like configuration -``` - -### 39.2 Processing stages - -```text -Read logs -> parse timestamp and event -> normalize identities -> merge aliases -> construct sessions -> bin by slot length -> render graphs -> generate HTML report -``` - -### 39.3 Data model - -```text -LogEvent { - timestamp - source: server | bot - event_type - client_uid optional - client_nickname optional - channel optional - raw_line -} - -ClientSession { - client_uid - display_name - start_time - end_time - channel_path optional -} - -StatsReport { - date_range - users - total_online_time - peak_concurrency - daily_series - hourly_series - vip_breakdown - bot_breakdown optional -} -``` - -### 39.4 Alias merging - -Support explicit merges: - -```python -merges = { - "old_uid_or_name": "canonical_user", - "another_alias": "canonical_user" -} -``` - -Never merge solely on nickname unless the operator confirms because nicknames are mutable. - -## 40. Qint-Style Desktop Client Architecture - -Qint demonstrates a modern replacement client architecture. - -### 40.1 Architecture - -```text -Frontend web UI - React/Vue/Svelte-like SPA or similar - -Tauri shell - desktop integration, permissions, windowing - -Proxy/backend process - TS3 connection manager - audio integration - command interface - -Protocol library - tsclientlib / equivalent -``` - -### 40.2 IPC contract - -Define stable commands between UI and backend: - -```json -{ "cmd": "connect", "server": "voice.example.com", "nickname": "Bot" } -{ "cmd": "disconnect" } -{ "cmd": "send_text", "targetMode": "channel", "target": 12, "message": "hello" } -{ "cmd": "move_client", "clid": 5, "cid": 10 } -``` - -Backend-to-frontend events: - -```json -{ "event": "client_entered", "client": { "clid": 5, "uid": "...", "nickname": "Alice" } } -{ "event": "channel_updated", "channel": { "cid": 10, "name": "Lobby" } } -{ "event": "text_message", "from": "Alice", "message": "hello" } -``` - -### 40.3 Production cautions - -- Treat Qint as architectural reference; verify maintenance status before dependency use. -- Keep frontend state derived from backend authoritative state. -- Avoid exposing raw identities to frontend logs. -- Use platform-specific audio backends behind a common interface. - -## 41. Error Handling Specification - -### 41.1 Error object - -```text -Ts3Error { - id: integer - name: string optional - message: string - failed_permid optional - return_code optional - command optional - raw_line - category - retryable: boolean -} -``` - -### 41.2 Categories - -```text -0xxx normal/general -01xx command/network binding -02xx client -03xx channel -04xx server -05xx database -06xx parameter -07xx connection -08xx file -0Axx permissions -``` - -### 41.3 Retry policy - -| Category | Retry? | Notes | -|---|---|---| -| connection lost | yes | reconnect and resync | -| currently not possible | sometimes | retry with backoff | -| convert error | no | fix parameter or encoding | -| invalid parameter | no | fix command builder | -| client is flooding | yes later | apply anti-flood limiter | -| insufficient permissions | no | show missing permission | -| database empty result | no | treat as empty lookup | -| server is booting | yes | poll until ready | - -## 42. Test Fixtures and Quality Gates - -### 42.1 Protocol tests - -- Escaping round trip for spaces, slashes, pipes, tabs, newlines. -- Array response parsing with escaped pipes. -- Error line parsing with return codes. -- Command builder output equality. - -### 42.2 Permission tests - -- Multiple server groups with and without negation. -- Client server-level override. -- Skip flag suppressing channel/group rights. -- Channel-client final override. -- Power/needed power denial explanation. - -### 42.3 Snapshot tests - -- Verify hash. -- Parse and reserialize without changes. -- Modify one field and recalculate hash. -- Preserve unknown fields. -- Handle CESU-8 or invalid Unicode safely. - -### 42.4 Event tests - -- Client enters and leaves. -- Client moves with one clid. -- Client moves with multiple clids. -- Channel edited with incomplete fields. -- Duplicate event deduplication. -- Resubscription after reconnect. - -### 42.5 Identity tests - -- Security-level count for known digest examples. -- Vanity pattern matcher. -- Parallel search cancellation. -- Identity export redaction in logs. - -## 43. Implementation Roadmap - -### Phase 1: Query foundation - -Deliver: - -- Query escaping codec. -- TCP connection lifecycle. -- Login and `use -virtual`. -- Command builder and parser. -- Error dictionary. -- Basic `serverinfo`, `clientlist`, `channellist`. - -### Phase 2: State and events - -Deliver: - -- Notify subscriptions. -- Event parser. -- State reducer. -- Periodic reconciliation. -- Text message handling. - -### Phase 3: Administration - -Deliver: - -- Channel CRUD. -- Client move/kick/poke. -- Server edit. -- Token management. -- Ban/complaint management. - -### Phase 4: Permissions - -Deliver: - -- Permission constants. -- Assignment fetchers. -- Effective permission engine. -- Explanation UI. -- Safe permission editor with grant checks. - -### Phase 5: Backup and files - -Deliver: - -- Snapshot export/import/verify. -- File transfer upload/download. -- Icon/avatar helpers. -- Full migration runbook. - -### Phase 6: Identity and stats - -Deliver: - -- Identity import/export. -- Security-level calculation. -- Vanity search. -- Log parser. -- HTML report generator. - -### Phase 7: Full client or bot - -Deliver: - -- Client runtime. -- Optional audio. -- Desktop/web UI. -- Reconnect/resync. -- Packaging and operational monitoring. - -## 44. Security and Operational Hardening - -### 44.1 Secrets - -Never log: - -- ServerQuery passwords. -- Privilege keys before use. -- Private identity material. -- License keys. -- Raw blacklist license fields. - -### 44.2 Access control - -Administration systems should use their own role model on top of TS3 permissions. Do not expose raw ServerQuery credentials to every operator. - -### 44.3 Backups - -- Store snapshots encrypted. -- Store file backups with integrity hashes. -- Keep at least one offline backup. -- Test restore periodically. - -### 44.4 Rate limits - -Apply client-side rate limiting even for privileged users. - -### 44.5 Auditing - -Log: - -- Who issued each admin action. -- Command family, not necessarily raw command with secrets. -- Target IDs and names. -- Error ID and failed permission. -- Snapshot export/import events. - -## 45. Final Self-Contained Build Checklist - -A developer implementing from this manual should be able to check off: - -- [ ] Query codec supports escaping and array rows. -- [ ] Query connection handles login, `use -virtual`, return codes, reconnects, and graceful quit. -- [ ] Errors are mapped to categories and retry policies. -- [ ] Events are parsed and state is reconciled. -- [ ] Client/channel/server variable schemas are generated from declarations. -- [ ] Permission engine handles server groups, client rights, channel rights, channel groups, channel-client rights, skip, negated, and grant powers. -- [ ] Anti-flood limiter models point decay and action costs. -- [ ] Snapshot parser verifies and recalculates `hash=`. -- [ ] File transfer supports token setup and raw binary transfer. -- [ ] Icon/avatar/cache helpers are isolated and fixture-tested. -- [ ] DNS resolver supports explicit host:port, `_ts3._udp`, and explicit `_tsdns._tcp`. -- [ ] Identity tool computes UID/security level and supports safe export. -- [ ] Stats processor parses logs into sessions and reports. -- [ ] UI/backend IPC is typed and does not leak secrets. -- [ ] Migration runbooks distinguish snapshots from full instance backups. -- [ ] Tests cover protocol, permissions, snapshots, events, identity, and logs. - - ---- - -# Appendix D - Full YaTQA Resources Link Coverage Review - -This appendix was added after a page-by-page review of `https://yat.qa/ressourcen/`. Its purpose is to make the manual self-auditing: every direct content link from the YaTQA resources page is either covered in the main manual or explicitly listed as a non-core, external, download-only, unavailable, or historical artifact. This prevents hidden dependencies on the original page. - -## D.1 Coverage classification - -Coverage categories used below: - -- **Fully covered** - the manual includes the engineering meaning, implementation behavior, and development relevance. -- **Covered as external/download artifact** - the resource is not a technical specification page, but the manual records what it is for and how it fits into a development workflow. -- **Covered as operational/site reference** - the link is navigation, legal, site metadata, or a support page rather than a protocol/implementation document. -- **Not source-analyzable** - the page linked to a binary/download/external host that was not fully accessible in the review environment; the manual records its stated purpose and limitation rather than inventing source details. - -## D.2 Direct links from the German YaTQA resources page - -| Resource/link text | Resource type | Coverage status | What this manual now contains | -|---|---:|---:|---| -| **Server-Backup/Umzug ohne Snapshot-Rechte** | YaTQA article | Fully covered | Backup and migration without snapshot permission, including command pacing, channel tree export, group export/import, file backup with YaTQA Pro, icon restoration order, and limitations: no server settings and no avatars. | -| **Details** under Unicode note | Internal technical anchor | Fully covered | Unicode/CESU-8/BMP handling, legacy TS3 Unicode limitations, malformed Unicode risks, and why some Unicode channel-name tricks require TeaSpeak. | -| **Web-basierte Tools / Web-based tools** | Tool page | Fully covered | Avatar filename conversion, icon signed/unsigned 32-bit conversion, server UID to cache directory conversion, and implementation notes. | -| **UniChars** | External Unicode channel-name tool | Covered as external/download artifact | Purpose: formatting channel names and other text using Unicode variants; includes the operational warning that important use cases require TeaSpeak and that Windows clients may render taller rows. | -| **Windows Unicode example image** | Image example | Covered as external/download artifact | Recorded as a rendering-behavior example for Unicode-styled channel names on Windows. Not treated as a protocol source. | -| **Linux Unicode example image** | Image example | Covered as external/download artifact | Recorded as a rendering-behavior example for Unicode-styled channel names on Linux. Not treated as a protocol source. | -| **Blacklist2-Webclient** | JavaScript/AJAX tool page | Fully covered | Blacklist2 HTTPS/protobuf query purpose, webclient role, query fields such as IP/domain/UID/license, and implementation placement in blacklist diagnostics tooling. | -| **Identities decoder / tsidentity.teaspeak.de** | External identity utility | Covered as external/download artifact | Purpose: decode exported identity strings and extract Omega; limitation: external endpoint was not reliably source-analyzable. Manual links this concept to MahTsIdentity and security-level implementation. | -| **DSGVO-Kompatibilitäts-Fix** | SQL download | Covered as external/download artifact | Purpose: remove historical IP logging from the TS3 server database using `UPDATE clients SET client_lastip = "" WHERE 1;`; limitation: manual warns this is unsupported by TeamSpeak and should be treated as a database-maintenance artifact, not normal API behavior. | -| **Anydate** | Delphi utility/download | Covered as external/download artifact | Purpose: update/downdate TeamSpeak client to any version >= 3.0.3 for compatibility testing; warning to back up TS3Client AppData because old clients may corrupt it. | -| **Client versions / Version** | Reference page | Fully covered | Version-number history, use in compatibility testing, historical download workflow, and caveat that the list is archival. | -| **Hashdog** | Security-level utility | Covered as external/download artifact | Purpose: faster security-level improvement; manual connects it to Hashcash-style SHA-1 nonce search and MahTsIdentity implementation. | -| **Snapshot Toolkit** | Tool collection | Covered as external/download artifact | Purpose: snapshot conversion, rehashing, and Nitrado legacy snapshot handling; manual lists it under snapshot tooling rather than treating it as a normative TS3 format. | -| **Changelog download** under Snapshot Toolkit | Download/list | Covered as external/download artifact | Recorded as a snapshot-toolkit support artifact. Not a protocol specification. | -| **Nitrado-Snapshot-Dekoder** | CLI binary | Covered as external/download artifact | Purpose: convert old pre-mid-December-2016 Nitrado `.dat` snapshot files into normal TS3 snapshots; described pipeline: zlib unpack, Base64 clean/decode, Vigenere decode. | -| **Nitrado-Snapshot-Dekoder Delphi source** | Source download | Not source-analyzable | Listed as source link; unavailable for full source review in the generated manual. Purpose recorded from the resource page. | -| **Nitrado-Snapshot-Enkoder** | CLI binary | Covered as external/download artifact | Purpose: convert normal snapshots into old Nitrado-compatible upload files; described pipeline: Vigenere encode, paragraph removal, BOM removal, Base64 encode/chunk, zlib pack; marked educational/obsolete. | -| **Nitrado-Snapshot-Enkoder Delphi source** | Source download | Not source-analyzable | Listed as source link; unavailable for full source review. Purpose recorded from the resource page. | -| **Snapshot-SHA-1-Neuberechner / Rehash** | CLI binary | Covered as external/download artifact | Standalone snapshot rehashing tool: removes paragraphs/BOM, recomputes SHA-1, and outputs modified snapshot as a file. | -| **Snapshot-SHA-1-Neuberechner Delphi source** | Source download | Not source-analyzable | Listed as source link; unavailable for full source review. Purpose recorded from the resource page. | -| **Teamoji** | Delphi utility/download | Covered as external/download artifact | Purpose: generate `emoticons` folder assets for third-party iconpacks from Unicode emoji data; limitations around UCS-4/TeaSpeak and combined emoji prioritization. | -| **Unicode Emoji List** | External Unicode data | Covered as external reference | Used as input data for Teamoji; manual records it as upstream data source rather than TS3-specific protocol documentation. | -| **Diverse Teamoji packages** | Download package | Covered as external/download artifact | Purpose: ready-made emoji packages using frequently used Twitter emoji plus squirrel; compatibility note: TS3 server >= 3.2.0 or TeaSpeak. | -| **Apple Teamoji package** | Download package | Covered as external/download artifact | Purpose: ready-made Apple emoji package with 179 emojis, stated to work on all servers. | -| **Server-Fehlercodes / Server Error Codes** | Reference table | Fully covered | Error-code taxonomy, mapping strategy, common codes, and recommendation to generate a typed error enum/dictionary. | -| **Rechte-IDs / Permission IDs** | Reference table | Fully covered | Permission IDs, SID naming, grant-ID calculation, historical numbering scheme, permission engine, and upgrade implications. | -| **Client-Versionen / Client Versions** | Reference table | Fully covered | Client version/build history, compatibility-testing role, and how tools such as Anydate use it. | -| **Definitionen und Algorithmen** | Technical document | Fully covered | Codecs, permission evaluation, query connection behavior, image/icon/avatar naming, snapshot basics, client cache, BBCode, Unicode/encoding caveats. | -| **Server-Query-Kommentare** | Technical document | Fully covered | Query behavior corrections, command quirks, token aliases, return codes, `use -virtual`, file-transfer command edge cases, permission-command behavior, and undocumented commands. | -| **Server Query Notify** | Technical document | Fully covered | Subscription lifecycle, per-event field lists, duplicate event caveats, tokenused event, client enter/leave, server/channel edits, text-message events. | -| **Variablen-Parameter** | Technical matrix | Fully covered | Client/channel/server/traffic variable availability and editability; integrated into typed command/event schema recommendations. | -| **Voice-Client-Anti-Flood** | Technical document | Fully covered | Flood point accumulation/decay, tick behavior, block/IP block thresholds, ignore permissions, and action-cost implementation model. | -| **Sicherheitsstufe** | Technical document | Fully covered | Hashcash-like SHA-1 leading-zero model, public key/Omega/nonces, expected cost, faster generation approaches, and MahTsIdentity integration. | -| **Snapshots** | Technical document | Fully covered | Snapshot contents/exclusions, `hash=` header, pipe separator, Base64-SHA1, field families, deploy semantics, mapping behavior, and limitations. | -| **Strings** | Placeholder | Covered as absent/planned | The resources page states this was planned; no released document exists to cover. | -| **Andere Protokolle / Protokolle** | Technical document | Fully covered | See Section 16: Protobuf, update protocol, file-transfer protocol, TSDNS, server nicknames, DNS/SRV behavior, Blacklist/Blacklist2, Weblist, badges, and TLD/public-suffix appendix. | -| **Abzeichen-Liste / Badges list** | Technical/list document | Fully covered | Badge GUIDs, asset URL bases, client field behavior, history/removed badges, and implementation guidance for rendering/validation. | -| **Impressum** | Legal/site page | Covered as operational/site reference | Listed as site/legal metadata, not an implementation source. | -| **Header/footer navigation: Info & Download, Funktionen, Manual, Changelog, FAQ, Support & Contact, Resources, About, English** | Site navigation | Covered as operational/site reference | These are navigational pages. The manual uses relevant operational facts from YaTQA manual/download/FAQ where they affect implementation, but they are not direct resource-document specifications. | - -## D.3 Protocol page internal coverage checklist - -The link `https://yat.qa/ressourcen/protokolle/` is now specifically covered by Section 16. Its internal topics are mapped as follows: - -| Protokolle topic | Coverage in this manual | -|---|---| -| Protobuf | Field tags, wire types used by TS3, Varint/BigNum encoding/decoding, length-delimited data, repeated fields, and parser strategy. | -| Updates | Historical UDP update request styles, later Protobuf version files, version-type records, updater image/mirror concept, and compatibility guidance. | -| File transfers | `ftinitupload`/`ftinitdownload` token exchange, raw byte stream transfer, upload/download directionality, file-list edge cases, and channel file backup implications. | -| TSDNS | Query formats, old/new client behavior, explicit `_tsdns._tcp` requirement, compatibility pitfalls. | -| Server nicknames (3.1.6+) | Treated as resolver-level aliases and included in DNS/TSDNS decision logic. | -| DNS | `_ts3._udp` SRV behavior, priority/weight/port/target semantics, and input-normalization guidance. | -| Blacklist | Legacy blacklist behavior summarized as historical compatibility context. | -| Blacklist2 | HTTPS + Protobuf query shape, relevant fields, diagnostics use, and cautions against relying on unofficial blacklist behavior for security decisions. | -| Weblist (server) | Server publication/listing behavior and implementation role. | -| Weblist (client) | Client-side retrieval/consumption role and noncriticality for private administration tools. | -| Badges | Badges field behavior, GUID list source, asset URL bases, and rendering strategy. | -| Disabling badges on an instance | Included as administrative/compatibility behavior rather than core protocol. | -| Historical TLD appendices | Covered as a compatibility-only appendix: old embedded TLD/public suffix behavior should not be used as a modern public suffix list. | - -## D.4 Review outcome - -After this review, every direct link or listed artifact from the YaTQA German resources page is now represented in the manual in one of the following forms: - -1. as a full technical implementation section; -2. as a tool/download artifact with purpose, pipeline, and limitation; -3. as a site/navigation/legal reference; -4. as an explicitly absent/planned resource where no released document exists. - -The important correction from v3 is that several helper artifacts were previously only implicit or missing by name: UniChars, DSGVO SQL, Hashdog, Snapshot Toolkit, Nitrado decoder/encoder, Teamoji, Unicode emoji source data, and ready-made emoji packages. They are now explicitly listed and classified in this appendix. - diff --git a/docs/ui-ux/adaptive-layout-platform-guide.md b/docs/ui-ux/adaptive-layout-platform-guide.md new file mode 100644 index 0000000..a08785a --- /dev/null +++ b/docs/ui-ux/adaptive-layout-platform-guide.md @@ -0,0 +1,27 @@ +# Chanora Adaptive Layout and Platform Guide + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Layout Policy + +Chanora prioritizes compact/mobile layout for MVP while preserving a path to medium and expanded layouts. + +| Layout class | Current expectation | +|---|---| +| Compact | Primary supported layout for mobile and narrow windows | +| Medium | Must preserve connection and voice control visibility; side navigation rail remains a P1 gap where not implemented | +| Expanded | Persistent side panes are deferred beyond current candidate | + +## 2. Platform Policy + +| Platform concern | Required behavior | +|---|---| +| Safe areas/system bars | Critical controls remain reachable | +| Android back | Back intent service mediates app behavior | +| iOS gestures/haptics | Platform services handle iOS-specific behavior | +| Desktop PTT | UI displays actual PTT capability level and fallback state | + +## 3. DV Conclusion + +Adaptive layout policy is documented. Medium/expanded hardening remains a tracked post-MVP/P1 gap where not implemented. diff --git a/docs/ui-ux/material3-component-catalog.md b/docs/ui-ux/material3-component-catalog.md new file mode 100644 index 0000000..8efe3e8 --- /dev/null +++ b/docs/ui-ux/material3-component-catalog.md @@ -0,0 +1,38 @@ +# Chanora Material 3 Component Catalog + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Component Catalogue + +| Component area | Source files | Purpose | +|---|---|---| +| Connect and bookmarks | `connect_widgets.dart`, `input_dialogs.dart` | Server input, bookmarks, connect actions | +| Channel and client view | `snapshot_view.dart`, `client_info_sheet.dart`, `channel_spacer.dart` | Channel tree, clients, client details | +| Chat | `chat_views.dart`, `bbcode_text.dart` | Channel text and BBCode rendering | +| Voice controls | `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart` | Mute, deaf, PTT, levels, processing controls | +| Platform/permission indicators | `permission_state_banner.dart`, `ptt_capability_badge.dart`, `talk_power_warning.dart` | Platform readiness and safety warnings | +| Diagnostics | `audio_debug_stats_panel.dart` and diagnostics surfaces in app shell | Runtime/debug evidence and export support | + +## 2. Acceptance Criteria + +| Component area | DV acceptance criterion | +|---|---| +| Connect and bookmarks | User can enter server details, save/reuse bookmarks, and receive user-safe errors | +| Channel and client view | Channel tree and online clients remain visible after snapshot updates | +| Chat | Channel text renders without corrupting Unicode or unsafe BBCode display | +| Voice controls | Mute, deaf, PTT, metering, and processing controls expose current state clearly | +| Platform/permission indicators | Permission and capability limitations are visible and not color-only | +| Diagnostics | Diagnostic/debug surfaces do not expose unredacted secrets in release evidence | + +## 3. Evidence Required Before Release + +| Evidence | Purpose | +|---|---| +| Flutter widget test run | Confirms component behavior remains stable | +| Accessibility review | Confirms critical states are not color-only and controls are reachable | +| Candidate screenshots or demo notes | Supports DV review and release readiness | + +## 4. DV Conclusion + +The component catalog is sufficient for DV navigation. Final UI audit should attach screenshots or test evidence for each critical component. diff --git a/docs/ui-ux/material3-design-tokens.md b/docs/ui-ux/material3-design-tokens.md new file mode 100644 index 0000000..edc5787 --- /dev/null +++ b/docs/ui-ux/material3-design-tokens.md @@ -0,0 +1,21 @@ +# Chanora Material 3 Design Tokens + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 + +## 1. Source + +Implementation token source is `apps/chanora_flutter/lib/design/chanora_tokens.dart`. + +## 2. Token Areas + +| Token area | Purpose | +|---|---| +| Color roles | Connection, voice, permission, diagnostic, warning, and neutral states | +| Typography roles | App shell, status, channel/client lists, chat, settings, diagnostics | +| Spacing roles | Compact/mobile layout, control grouping, list density | +| Shape/elevation roles | Material 3 cards, sheets, banners, controls | + +## 3. Verification + +Token usage is verified through Flutter widget tests, visual review, and accessibility checks that critical state is not represented by color alone. diff --git a/docs/ui-ux/material3-guideline.md b/docs/ui-ux/material3-guideline.md new file mode 100644 index 0000000..ccd1f00 --- /dev/null +++ b/docs/ui-ux/material3-guideline.md @@ -0,0 +1,8 @@ +# Chanora Material 3 Guideline + +**Document status:** DV path record +**Canonical source:** `../material3-guideline.md` + +The current Material 3 guideline lives at `docs/material3-guideline.md`. This file preserves the README-advertised path `docs/ui-ux/material3-guideline.md` for DV navigation. + +Reviewers shall use `docs/material3-guideline.md` as the canonical UI/UX guideline until the migration moves it into `docs/ui-ux/`. diff --git a/docs/verification/swe4-unit-verification-plan.md b/docs/verification/swe4-unit-verification-plan.md new file mode 100644 index 0000000..bb23b65 --- /dev/null +++ b/docs/verification/swe4-unit-verification-plan.md @@ -0,0 +1,57 @@ +# SWE.4 Unit Verification Plan + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Scope:** Unit-level verification for Chanora software components + +## 1. Purpose + +This plan defines unit verification coverage for the software modules that implement Chanora behavior. The plan is based on the current repository state and the SRS verification hooks. It is acceptable for DV only when open unit gaps remain visible in the waiver register. + +## 2. Unit Verification Scope + +| Area | Components | Verification method | Current evidence | +|---|---|---|---| +| Rust core orchestration | `core/chanora_core` | Cargo unit tests, integration-oriented crate tests | `cargo test --workspace --locked --no-fail-fast` in CI | +| Protocol adapter | `crates/chanora_protocol` | Protocol DTO and error mapping tests | Workspace tests; protocol compatibility still needs SWE.5/SYS.4 evidence | +| State sync | `crates/chanora_state` | Snapshot, delta, reducer, malformed-event, reconnect, deterministic ordering, unknown-client, and channel-delete/client cleanup unit tests | Local `cargo test -p chanora_state --locked` passes with 27 tests | +| Audio subsystem | `crates/chanora_audio` | DSP, Opus, mixer, gate, PTT logic, baseline examples, benches | Workspace tests and benchmark harnesses; platform loopback evidence is integration/system scope | +| Storage | `crates/chanora_storage` | Bookmark repository, identity store, encryption behavior, keyring-disabled CI mode | Workspace tests with `CHANORA_DISABLE_KEYRING=1` in CI | +| Diagnostics | `crates/chanora_diagnostics` | Redaction, known-secret registry, diagnostic export JSON, log sink | Workspace tests and security audit plan | +| Resolver and prefetch | `crates/chanora_resolver`, `crates/chanora_server_prefetch`, Flutter prefetch debouncer | Resolver fallback, cache TTL, generation safety, debouncer tests | Workspace tests and `apps/chanora_flutter/test/services/prefetch_debouncer_test.dart` | +| Flutter services | `apps/chanora_flutter/lib/services` | Dart unit tests | Service tests under `apps/chanora_flutter/test/services/` | +| Flutter widgets | `apps/chanora_flutter/lib/widgets` and screen widgets | Widget tests | Widget tests under `apps/chanora_flutter/test/widgets/` | + +## 3. Required CI Commands + +| Command | Owner | Expected use | +|---|---|---| +| `cargo check --workspace --locked` | Software | Rust compile verification | +| `CHANORA_DISABLE_KEYRING=1 cargo test --workspace --locked --no-fail-fast` | Software / QA | Rust unit and crate tests without host keyring dependency | +| `cargo clippy --workspace --all-targets -- -D warnings` | Software | Advisory static analysis in current CI | +| `flutter analyze` in `apps/chanora_flutter` | Software | Dart static analysis | +| `flutter test --exclude-tags e2e` in `apps/chanora_flutter` | Software / QA | Flutter unit and widget tests | + +## 4. SRS Unit Coverage Focus + +| SRS group | Unit focus | Status for DV | +|---|---|---| +| SRS-036 through SRS-043 | Rust core and state behavior | Reducer unit evidence present; candidate CI/run ID still required for release record | +| SRS-054 through SRS-061 | State sync reducers and replay support | Reducer unit coverage present; standalone event replay support remains P1/not complete | +| SRS-062 through SRS-083 | Audio capture, processing, codec, playback, controls | Partial; unit and benchmark evidence exists, full platform loopback is SWE.5/SYS.4 | +| SRS-084 through SRS-095 | Storage, secure storage, diagnostics | Partial; CI avoids real keyring and must be supplemented by platform audits | +| SRS-165 through SRS-178 | Localization and Unicode handling | Partial; generated localization exists and targeted tests should be reviewed | +| SRS-184 and platform behavior items | App initialization and platform services | Partial; service tests exist for back intent, permissions, bootstrap, and lifecycle | + +## 5. Known SWE.4 Gaps + +| Gap | Impact | Required DV handling | +|---|---|---| +| Standalone event replay tool is not yet complete | Limits P1 replay-based state-sync verification claims | Keep event replay waiver; reducer unit evidence is available | +| Event replay tool not found | P1 verification hook is incomplete | Mark deferred/P1 in SWE.6 and waiver register if discussed | +| Platform keyring behavior not exercised in CI | Secure-storage unit evidence is incomplete for real OS services | Cover through platform audit/SYS.4, not CI-only claims | +| Audio device hot-plug recovery follow-up exists | Limits reliability evidence for SRS-082 | Mark as P1 gap | + +## 6. SWE.4 DV Decision Rule + +SWE.4 can pass for documentation readiness if all existing unit commands are identified and open unit gaps are listed as limitations. SWE.4 cannot be used to approve public release until required unit evidence is executed on the release candidate and attached to the release-readiness record. diff --git a/docs/verification/swe5-software-integration-verification-plan.md b/docs/verification/swe5-software-integration-verification-plan.md new file mode 100644 index 0000000..25abf8e --- /dev/null +++ b/docs/verification/swe5-software-integration-verification-plan.md @@ -0,0 +1,58 @@ +# SWE.5 Software Integration Verification Plan + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Scope:** Integration verification across Chanora software components + +## 1. Purpose + +This plan defines how Chanora verifies that independently tested software components work together. It covers the integration paths identified by SRS section 8 and SysDes verification handoff items. + +## 2. Integration Paths + +| Integration path | Covered components | Primary requirements | Evidence method | +|---|---|---|---| +| Flutter to bridge to Rust core | Flutter UI/state, generated FRB bindings, `chanora_bridge`, `chanora_core` | SRS-031 through SRS-035, SRS-103 | Flutter integration tests, Rust bridge tests, manual candidate smoke | +| Core to protocol adapter | `chanora_core`, `chanora_protocol`, `tsclientlib` | SRS-044 through SRS-053, SRS-131, SRS-134 | Protocol integration tests and compatible-server demo | +| Protocol to state | Protocol events, Rust core state handling, `chanora_state`, Flutter snapshot mapper | SRS-054 through SRS-061, SRS-124 | Reducer tests, snapshot mapper tests, reconnect/error tests | +| Audio to platform | `chanora_audio`, OS audio APIs, Flutter voice controls, platform permissions | SRS-062 through SRS-083, SRS-104 through SRS-115, SRS-135 | Audio loopback tests, platform smoke, mobile device tests | +| Storage to platform secure storage | `chanora_storage`, keyring adapters, Flutter bookmark/identity UI | SRS-084 through SRS-095, SRS-126, SRS-137 | Storage tests, platform secure-storage audit | +| Diagnostics export | Diagnostics crate, Flutter diagnostics UI, redaction, share/export surface | SRS-095 through SRS-102, SRS-126, SRS-128 | Redaction tests, diagnostic export review, manual export demo | +| Server resolution prefetch | Flutter host-field debouncer, `chanora_server_prefetch`, `chanora_resolver`, connect path | Resolver/prefetch design specs and connection requirements | Unit tests, Android connect smoke, manual connect demo | +| Packaging and release build | Flutter build, Rust cdylib/framework/DLL/SO packaging, CI workflows | SRS-116 through SRS-123, SRS-127 | CI build evidence, unsigned iOS build, platform-specific smoke | + +## 3. Required Integration Evidence + +| Evidence | Current state | DV conclusion | +|---|---|---| +| CI Rust workspace test run | Defined in `.github/workflows/ci.yml` | Accept as automated baseline when latest run is attached | +| CI Flutter analyze/test run | Defined in `.github/workflows/ci.yml` | Accept as automated baseline when latest run is attached | +| iOS unsigned release build | Defined in `.github/workflows/ci.yml` and `docs/release/ios-build.md` | Accept for unsigned build verification only | +| Audio benchmark advisory | Defined in `.github/workflows/bench-advisory.yml` | Accept as advisory performance evidence, not a release blocker | +| Windows smoke procedure | Defined in `tools/windows-smoke.md` | Accept only when an executed result is attached | +| Android connect and permission smoke | Referenced by implementation status and tests | Requires executed evidence for release decision | + +## 4. Integration Acceptance Criteria + +An integration path passes when: + +| Criterion | Requirement | +|---|---| +| Buildability | Integrated components compile in CI or on the declared target build host | +| Data contract stability | DTOs and bridge boundaries preserve required fields and error states | +| Error behavior | Integration failures produce user-safe errors, not crashes or secret leakage | +| Platform behavior | Platform-specific permission, audio, and lifecycle behavior matches the target platform policy | +| Evidence attachment | Test logs, CI run IDs, smoke records, or manual demo notes are referenced in the release-readiness record | + +## 5. Known SWE.5 Gaps + +| Gap | Impact | Required DV handling | +|---|---|---| +| Desktop release artifacts are source-buildable only for current candidate | Blocks binary distribution readiness for Windows/macOS/Linux | Release record must state source-build-only scope | +| iOS build is unsigned | Blocks App Store/TestFlight release approval | Release record must state unsigned verification only | +| Android Keystore-backed DEK deferred | Limits secure-storage integration claim on Android | Waiver required for internal testing; public release claim blocked | +| Event replay infrastructure not found | Limits protocol-state integration stress evidence | Mark P1 gap | + +## 6. SWE.5 DV Decision Rule + +SWE.5 can pass for DV documentation readiness if every integration path has an identified evidence method and every incomplete path has a waiver or follow-up. SWE.5 does not pass for public release until current candidate integration runs are attached to the release-readiness record. diff --git a/docs/verification/swe6-software-verification-plan.md b/docs/verification/swe6-software-verification-plan.md new file mode 100644 index 0000000..86d04de --- /dev/null +++ b/docs/verification/swe6-software-verification-plan.md @@ -0,0 +1,62 @@ +# SWE.6 Software Verification Plan + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Scope:** Integrated software verification against `docs/srs.md` + +## 1. Purpose + +This plan verifies the integrated Chanora software against the SRS. It focuses on SRS-128 and the MVP acceptance requirements inherited from SysRS-241 through SysRS-257. + +## 2. SRS Verification Strategy + +| SRS area | Verification approach | Evidence source | +|---|---|---| +| Software process and traceability | Review and inspection | `docs/srs.md`, `docs/governance/traceability-matrix.md` | +| UI and app shell | Flutter widget/service tests and demo | `apps/chanora_flutter/test/` | +| Bridge and Rust core | Workspace tests and integration smoke | CI Rust job, bridge/core tests | +| Protocol and connection | Protocol integration and compatible-server demo | Protocol tests, manual server demo | +| State sync | Reducer tests, snapshot mapper tests, reconnect/error scenarios, and runtime integration evidence | Rust state tests, Flutter mapper tests, compatible-server runtime evidence | +| Audio | Audio tests, processing tests, benchmark advisory, platform demo | `chanora_audio` tests/benches and manual platform evidence | +| Storage and secure storage | Storage tests and platform secure-storage audit | Rust storage tests, platform audit | +| Diagnostics and redaction | Redaction tests, diagnostic export demo, security review | Diagnostics tests, export review | +| Platform and release | Platform smoke and release build inspection | CI, platform smoke docs, release record | + +## 3. MVP Acceptance Matrix + +| SysRS | Acceptance requirement | SRS coverage | Required evidence | Current DV status | +|---|---|---|---|---| +| SysRS-241 | Connect to TeamSpeak 3-compatible server using `tsclientlib` | SRS-045, SRS-046, SRS-049, SRS-050, SRS-128, SRS-134 | Compatible-server connection demo and protocol integration evidence | Passed with limitation when demo log is attached; not sufficient for public release alone | +| SysRS-242 | Display server channel tree | SRS-019 through SRS-022, SRS-054 through SRS-058, SRS-128 | UI demo, snapshot/state tests | Reducer unit evidence present; candidate UI/demo and runtime reducer-integration evidence must still be attached | +| SysRS-243 | Display online clients | SRS-019 through SRS-022, SRS-054 through SRS-058, SRS-128 | UI demo, snapshot/state tests | Reducer unit evidence present; candidate UI/demo and runtime reducer-integration evidence must still be attached | +| SysRS-244 | Allow user to join a voice channel | SRS-023, SRS-024, SRS-045 through SRS-051, SRS-128 | Channel join demo, error mapper tests | Passed with limitation when candidate demo is attached | +| SysRS-245 | Send voice | SRS-062 through SRS-071, SRS-077, SRS-079, SRS-128 | Audio loopback/platform demo | Partial; platform evidence must be attached | +| SysRS-246 | Receive voice | SRS-071 through SRS-076, SRS-078, SRS-081, SRS-128 | Audio loopback/platform demo | Partial; platform evidence must be attached | +| SysRS-247 | Support microphone mute | SRS-025, SRS-077, SRS-079, SRS-128 | UI/audio gate demo or test | Passed with limitation when candidate demo is attached | +| SysRS-248 | Support output deaf | SRS-025, SRS-078, SRS-128 | UI/audio gate demo or test | Passed with limitation when candidate demo is attached | +| SysRS-249 | Support push-to-talk | SRS-077 through SRS-079, SRS-197 through SRS-200, SRS-128 | PTT backend tests, platform capability evidence | Partial; release record must state platform capability level | +| SysRS-250 | Support Echo Canceller | SRS-064 through SRS-067, SRS-125, SRS-128 | Audio processing test/demo | Partial; attach audio evidence | +| SysRS-251 | Support Automatic Gain Control | SRS-064 through SRS-067, SRS-125, SRS-128 | Audio processing test/demo | Partial; attach audio evidence | +| SysRS-252 | Support Noise Suppression | SRS-064 through SRS-067, SRS-125, SRS-128 | Audio processing test/demo | Partial; attach audio evidence | +| SysRS-253 | Support High-Pass Filter | SRS-064 through SRS-067, SRS-125, SRS-128 | Audio processing test/demo | Partial; attach audio evidence | +| SysRS-254 | Send and receive channel text messages | SRS-019, SRS-036 through SRS-038, SRS-170 through SRS-174, SRS-128 | Chat UI tests and compatible-server demo | Passed with limitation when candidate demo is attached | +| SysRS-255 | Save and reuse server bookmarks | SRS-084 through SRS-089, SRS-128 | Storage tests and UI demo | Passed with limitation when candidate demo is attached | +| SysRS-256 | Use secure storage for sensitive data | SRS-090 through SRS-095, SRS-126, SRS-137 | Security audit and platform storage evidence | Partial; Android Keystore-backed DEK deferred | +| SysRS-257 | Export redacted diagnostic logs | SRS-028, SRS-093 through SRS-102, SRS-126, SRS-128 | Redaction tests and export demo | Passed with limitation when redaction/export evidence is attached | + +## 4. Candidate Software Verification Runs + +For a release decision, the following run records must be attached or linked from `docs/release/release-readiness-go-nogo-record.md`: + +| Run record | Command or method | Required for | +|---|---|---| +| Rust workspace CI | `cargo check --workspace --locked` and `cargo test --workspace --locked --no-fail-fast` | All Rust-backed SRS behavior | +| Flutter CI | `flutter analyze` and `flutter test --exclude-tags e2e` | UI, service, and widget SRS behavior | +| Candidate compatible-server demo | Manual or scripted demo against a controlled TeamSpeak-compatible server | SysRS-241 through SysRS-246, SysRS-254 | +| Candidate audio demo | Manual or scripted loopback/platform run | SysRS-245 through SysRS-253 | +| Candidate diagnostics demo | Export and inspect redacted diagnostic bundle | SysRS-257, SRS-093 through SRS-102 | +| Candidate secure-storage audit | Platform inspection for Windows, macOS, Linux, iOS, Android | SysRS-256, SRS-090 through SRS-095 | + +## 5. SWE.6 DV Decision Rule + +SWE.6 can pass the DV meeting if the acceptance matrix is accepted as the controlling checklist and each partial item is carried into the release-readiness record. SWE.6 cannot approve public release while DEC-012, Android secure-storage limitation, platform signing, and incomplete candidate evidence remain open. diff --git a/docs/verification/sys4-system-integration-verification-plan.md b/docs/verification/sys4-system-integration-verification-plan.md new file mode 100644 index 0000000..cb86093 --- /dev/null +++ b/docs/verification/sys4-system-integration-verification-plan.md @@ -0,0 +1,57 @@ +# SYS.4 System Integration Verification Plan + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Scope:** System integration verification against `docs/sysdes.md` + +## 1. Purpose + +This plan verifies Chanora as an integrated application system, including software, operating-system services, hardware, network dependencies, external compatible servers, diagnostics, and release operations. + +## 2. System Elements Under Verification + +| Element | Integration concern | Verification method | +|---|---|---| +| External TeamSpeak 3-compatible server | Connection, authentication, channel tree, clients, voice, channel text, server permissions | Compatible-server demo and protocol compatibility matrix | +| OS audio services | Capture, playback, route changes, permissions, mobile audio session, foreground behavior | Platform audio tests and manual device smoke | +| Audio hardware | Microphone/headset/speaker availability and failure handling | Platform smoke and user-safe error checks | +| Platform secure storage | Keychain/Credential Manager/Secret Service/mobile key stores and fallback behavior | Platform security audit | +| Network | DNS/SRV/TSDNS resolution, unreachable server behavior, reconnect | Resolver tests and network failure smoke | +| Platform packaging | Android, iOS, Windows, macOS, Linux build and packaging behavior | CI build, unsigned iOS build, platform smoke, release inspection | +| Diagnostics export | User-initiated export and secret redaction | Export demo and security review | +| Public wording | Non-affiliation and compatible-server claims | Legal/trademark review | + +## 3. SysDes Verification Handoff Coverage + +| SysDes item | Handoff | SYS.4 handling | +|---|---|---| +| SysDes-102 | Protocol verification via protocol probe and compatibility matrix | Requires compatible-server evidence and protocol adapter review | +| SysDes-103 | State verification via reducer and event replay tests | Reducer evidence handled in SWE.4/SWE.5; event replay remains P1 gap | +| SysDes-104 | Audio loopback and processing tests | Requires platform audio evidence and processing tests | +| SysDes-105 | Security audit for storage, keys, passwords, validation, redaction | Requires security/privacy/legal gate review | +| SysDes-106 | Deployment package, signing, notarization, app-store build, metadata | Release-readiness record controls final decision | +| SysDes-107 | MVP acceptance verification for SysRS-241 through SysRS-257 | SWE.6 acceptance matrix controls software evidence; SYS.4 adds environment evidence | + +## 4. Platform Matrix + +| Platform | Current candidate status | Required SYS.4 evidence before public/store release | +|---|---|---| +| Android | Implemented features include permissions, foreground service, Oboe audio, MODE_IN_COMMUNICATION; Android Keystore-backed DEK deferred | Device smoke, permission flow, foreground voice, secure-storage limitation waiver, Play target SDK inspection | +| iOS | Source-buildable and unsigned; AVAudioSession work exists; public artifact not ready | Unsigned build evidence, device audio session smoke, signing/TestFlight evidence before release | +| Windows | Source-buildable; smoke procedure exists | Executed smoke result, PTT capability evidence, packaging/signing evidence before release | +| macOS | Source-buildable; not in current release artifacts | Build evidence, PTT capability evidence, signing/notarization evidence before release | +| Linux | Source-buildable; GlobalShortcuts portal behavior depends on environment | Build/smoke evidence, portal/focused fallback capability evidence, packaging evidence before release | + +## 5. SYS.4 Acceptance Criteria + +| Criterion | Required result | +|---|---| +| External server compatibility | Candidate connects to a controlled compatible server and exercises connection, channel, voice, and text flows | +| Platform behavior transparency | Release notes and UI do not overstate platform PTT, packaging, or secure-storage capabilities | +| Security/privacy/legal readiness | DEC-012 and privacy/security gates are signed off or explicitly block release | +| Release artifact integrity | Build number, commit SHA, tag, artifact hashes, and signing status are recorded | +| Environmental limitations | Source-build-only, unsigned, or platform-specific limitations are listed in release readiness | + +## 6. SYS.4 DV Decision Rule + +SYS.4 can pass the documentation baseline if system integration responsibilities, platform evidence needs, and release blockers are explicit. SYS.4 cannot pass for public/store release until platform-specific evidence and legal/signing gates are complete. diff --git a/docs/verification/verification-master-plan.md b/docs/verification/verification-master-plan.md new file mode 100644 index 0000000..dcbe3c6 --- /dev/null +++ b/docs/verification/verification-master-plan.md @@ -0,0 +1,90 @@ +# Chanora Verification Master Plan + +**Document status:** DV meeting baseline candidate +**Date:** 2026-05-29 +**Applies to:** Chanora `0.2.0-beta.1` workspace / `v1.0.0-rc.1` release-candidate evidence +**Primary upstream documents:** `docs/sysrs.md`, `docs/sysdes.md`, `docs/srs.md`, `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/implementation-status-2026-05-28.md` + +## 1. Purpose + +This plan defines the verification evidence Chanora must present at the DV meeting. It does not approve release by itself. It gives reviewers a single route from requirements and design baselines to available tests, demos, audits, waivers, and release gates. + +The DV meeting may approve the verification posture for continued internal candidate work if the open gates are accepted as explicit waivers. Public or store release remains blocked until the release-readiness record reaches `Go`. + +## 2. Verification Lifecycle Mapping + +| Lifecycle level | Plan | Primary input | Verification focus | +|---|---|---|---| +| SWE.4 | `docs/verification/swe4-unit-verification-plan.md` | `docs/architecture/sdd.md` module behavior plus current crate and Flutter tests | Unit behavior of reducers, protocol mappers, audio processors, storage, diagnostics, UI services, and utility logic | +| SWE.5 | `docs/verification/swe5-software-integration-verification-plan.md` | `docs/architecture/sad.md` component interfaces and `docs/architecture/sdd.md` module integration hooks | Integration across Flutter, bridge, Rust core, protocol, audio, storage, diagnostics, resolver, and packaging boundaries | +| SWE.6 | `docs/verification/swe6-software-verification-plan.md` | `docs/srs.md` | Integrated software verification against SRS, especially SRS-128 MVP acceptance | +| SYS.4 | `docs/verification/sys4-system-integration-verification-plan.md` | `docs/sysdes.md` and external environment assumptions | System integration with compatible servers, OS services, hardware, network, app stores, diagnostics, and release operations | + +## 3. Verification Policy + +Verification evidence shall be recorded as one of these states: + +| State | Meaning | DV handling | +|---|---|---| +| Passed | Evidence exists and satisfies the stated criterion | Accept as supporting evidence | +| Passed with limitation | Evidence exists, but scope is narrower than final release scope | Accept only with documented limitation | +| Not run | Criterion is defined but no run is recorded for the candidate | Requires waiver or follow-up action | +| Failed | Evidence exists and does not satisfy the criterion | Blocks the covered release scope unless waived by the release owner | +| Deferred | Requirement is intentionally outside current candidate scope | Requires requirement stage or waiver reference | +| Blocked | External decision, platform access, legal sign-off, or environment prevents completion | Requires owner and unblock condition | + +No document in this pack may convert an implementation gap into a pass. Gaps must be represented as `Not run`, `Deferred`, or `Blocked`. + +## 4. Current Evidence Sources + +| Evidence source | Current content | DV use | +|---|---|---| +| `.github/workflows/ci.yml` | Rust workspace check/test, advisory clippy, cargo-deny, cargo-about inventory check, Flutter analyze/test, unsigned iOS release build | Build, unit, static-analysis, supply-chain, Flutter test, and iOS unsigned build evidence | +| `.github/workflows/bench-advisory.yml` | Advisory-only realtime audio benchmark workflow for PR/push events | Performance trend evidence, not a hard quality gate | +| `docs/implementation-status-2026-05-28.md` | Current implementation status, blockers, partial areas, and P1/P2 gaps | Primary readiness and waiver input | +| `apps/chanora_flutter/test/` | Flutter widget/service/e2e-labeled tests | SWE.4/SWE.5/SWE.6 evidence depending on test type | +| Rust crate tests and benches | Workspace tests plus audio benchmark harnesses | SWE.4/SWE.5 performance and component evidence | +| `tools/windows-smoke.md` | Windows source-build smoke procedure | SYS.4/SWE.5 manual platform evidence when executed | +| `docs/release/ios-build.md` | Unsigned iOS verification build note | Release and platform build evidence | + +## 5. Entry Criteria for DV Review + +| Criterion | Status | Evidence | +|---|---|---| +| SysRS baseline available | Met | `docs/sysrs.md` | +| SysDes baseline available | Met | `docs/sysdes.md` | +| SRS baseline available | Met | `docs/srs.md` | +| SAD baseline available | Met | `docs/architecture/sad.md` | +| SDD baseline available | Met | `docs/architecture/sdd.md` | +| Verification plans available | Met by this pack | `docs/verification/` | +| Traceability summary available | Met by this pack | `docs/governance/traceability-matrix.md` | +| Release decision record available | Met by this pack | `docs/release/release-readiness-go-nogo-record.md` | +| Open gates represented as waivers or blockers | Met by this pack | `docs/release/dv-waiver-register.md` | + +## 6. Exit Criteria for DV Meeting + +The DV meeting can pass the documentation baseline if reviewers agree that: + +| Exit criterion | Required result | +|---|---| +| Requirements-to-verification route is reviewable | SysRS/SysDes/SRS items map to verification plans and acceptance evidence | +| MVP acceptance criteria are explicit | SysRS-241 through SysRS-257 appear in SWE.6 with evidence status | +| Verification scope is honest | Known gaps are not marked as passed | +| Waivers are explicit | Each release-affecting gap has owner, impact, mitigation, and unblock condition | +| Release recommendation is clear | Current candidate is not represented as public-release-ready while DEC-012 and other gates remain open | + +## 7. Open Gates Affecting Release + +| Gate | Status | Release impact | Owning document | +|---|---|---|---| +| DEC-012 legal/trademark/OSS sign-off | Blocked / open | Blocks public or store release | `docs/legal/trademark-and-attribution-review.md`, `docs/release/dv-waiver-register.md` | +| Android Keystore-backed DEK | Deferred to v1.1 | Blocks claim that Android secrets use hardware-backed DEK protection | `docs/release/dv-waiver-register.md` | +| Full state reducer test suite | Partial | Blocks full claim for SysRS-235/SysDes-103/SRS-059 through SRS-061 | `docs/release/dv-waiver-register.md` | +| Desktop and iOS release artifacts | Source-buildable / unsigned only | Blocks broad binary distribution claims | `docs/release/release-readiness-go-nogo-record.md` | +| Store signing and notarization | Not complete for candidate | Blocks production release | `docs/release/release-readiness-go-nogo-record.md` | + +## 8. DV Recommendation + +Recommended DV outcome: **Pass documentation baseline with release waivers recorded**. + +This means the project has enough document structure to conduct DV review and continue internal candidate validation, but it does not mean the candidate is approved for public release. The release decision remains **No-Go for public/store release** until legal, signing, platform, and evidence gates are closed or formally waived by the accountable owners. diff --git a/poc/README.md b/poc/README.md deleted file mode 100644 index 51b259b..0000000 --- a/poc/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Chanora — Proofs of Concept - -This directory holds **non-product** technical spikes. They exist to retire -risk before the Flutter/Rust product scaffold is created. - -Authority: [`docs/architecture/proof-of-concept-plan.md`](../docs/architecture/proof-of-concept-plan.md). - -> **Rule (PoC plan §4):** A PoC is not product code unless explicitly -> promoted. Nothing here should be imported by the future -> `apps/chanora_flutter` or `crates/chanora_*` trees. - -## Status - -| Spike | Required by PoC plan | Status | -|---|---|---| -| `tsclientlib-connect-spike` | yes | **PASS** — see `tsclientlib-connect-spike/VERIFICATION.md` | -| `flutter_rust_bridge_hello` | yes (`flutter-rust-bridge-hello` in plan) | **PASS** — see `flutter_rust_bridge_hello/VERIFICATION.md` | -| `secure-storage-spike` | yes | **PASS (Linux only)** — see `secure-storage-spike/VERIFICATION.md` | -| `sqlite-storage-spike` | yes | **PASS** — see `sqlite-storage-spike/VERIFICATION.md` | -| `diagnostics-redaction-spike` | yes | **PASS** — see `diagnostics-redaction-spike/VERIFICATION.md` | -| `audio-capture-playback-spike` (desktop half) | yes | **PASS** — see `audio-capture-playback-spike/VERIFICATION.md` | -| `audio-capture-playback-android-spike` (mobile half) | yes | **PASS** — see `audio-capture-playback-android-spike/VERIFICATION.md` | -| `linux_voice_test` | no (extra Linux backend spike) | **PASS** — see `linux_voice_test/VERIFICATION.md` | -| `ts3-manual-research` | no (extra TS3 protocol research spike) | **PASS** — see `ts3-manual-research/VERIFICATION.md` | -| `tsclientlib-query-spike` | no (extra TS3 protocol research spike) | **PASS** — see `tsclientlib-query-spike/VERIFICATION.md` | -| `tsclientlib-channel-query-spike` | no (extra TS3 protocol research spike) | **PASS** — see `tsclientlib-channel-query-spike/VERIFICATION.md` | -| `tsclientlib-filetransfer-spike` | no (extra TS3 protocol research spike) | **PASS** — see `tsclientlib-filetransfer-spike/VERIFICATION.md` | -| `tsidentity-import-spike` | no (extra TS3 protocol research spike) | **PASS** — see `tsidentity-import-spike/VERIFICATION.md` | - -## Naming note - -`flutter_rust_bridge_hello` is listed in the PoC plan as -`flutter-rust-bridge-hello`. The directory on disk uses underscores -because the Dart `pubspec.yaml` package-name grammar forbids hyphens. -This is a naming convention, not a scope change. - -## What "verified" means here - -Each verified spike contains: - -- a `README.md` describing scope, exit criterion, and reproduction; -- a `VERIFICATION.md` recording the exact toolchain versions, command, - observed output, and an honest list of things the spike does **not** - validate. - -Spikes are intentionally narrow. They do not approximate Chanora -architecture layering, they do not implement the documented DTO -catalogue, and they do not stand in for the product Rust crates or -Flutter modules. diff --git a/poc/audio-capture-playback-android-spike/Cargo.lock b/poc/audio-capture-playback-android-spike/Cargo.lock deleted file mode 100644 index cb648f1..0000000 --- a/poc/audio-capture-playback-android-spike/Cargo.lock +++ /dev/null @@ -1,929 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.11.1", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[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.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - -[[package]] -name = "audio_capture_playback_android_spike" -version = "0.1.0" -dependencies = [ - "android_logger", - "cpal", - "hound", - "jni", - "log", - "ndk-context", - "thiserror 2.0.18", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[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 = "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 = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "coreaudio-rs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" -dependencies = [ - "bitflags 1.3.2", - "libc", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "cpal" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" -dependencies = [ - "alsa", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.11.1", - "objc2", -] - -[[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 = "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 = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[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.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.11.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-audio-toolbox" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" -dependencies = [ - "bitflags 2.11.1", - "libc", - "objc2", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" -dependencies = [ - "bitflags 2.11.1", - "objc2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "objc2", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[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 = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[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 = "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 = "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 = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[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 = "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 = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[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 = "wasm-bindgen" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[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" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - -[[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.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[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.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 = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -dependencies = [ - "memchr", -] diff --git a/poc/audio-capture-playback-android-spike/Cargo.toml b/poc/audio-capture-playback-android-spike/Cargo.toml deleted file mode 100644 index 33642d6..0000000 --- a/poc/audio-capture-playback-android-spike/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "audio_capture_playback_android_spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: Android mobile half of the audio capture/playback spike. cdylib loaded by the Kotlin app via JNI; cpal targets the Oboe backend on Android." - -# Not product code. See docs/architecture/proof-of-concept-plan.md §4. -# Closes the mobile half of the PoC audio exit criterion left open by -# poc/audio-capture-playback-spike on Linux desktop. - -[lib] -name = "audio_capture_playback_android_spike" -crate-type = ["cdylib"] -path = "src/lib.rs" - -[dependencies] -# cpal automatically selects the Oboe backend on Android — platform-native -# per DEC-011. cpal links the Oboe native library through ndk-glue. -cpal = "0.16" -hound = "3" -jni = { version = "0.21", default-features = false } -thiserror = "2" - -# Android-specific logging: route `log` macros to logcat. -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.14" -log = "0.4" -ndk-context = "0.1" - -# Required by cpal at build time on Android. -[target.'cfg(target_os = "android")'.build-dependencies] diff --git a/poc/audio-capture-playback-android-spike/README.md b/poc/audio-capture-playback-android-spike/README.md deleted file mode 100644 index ad954bd..0000000 --- a/poc/audio-capture-playback-android-spike/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# Android Audio Capture / Playback Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `audio-capture-playback-android-spike` | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Close the mobile half of the audio capture/playback PoC exit criterion | -| Exit criterion (mobile half) | "Capture/playback works on at least one mobile target" | -| Authority | DEC-011 (platform-native first), DEC-011.1 (Android crate = `cpal`-on-Oboe) | - -## What it proves - -The desktop half of the audio PoC was closed by -[`poc/audio-capture-playback-spike`](../audio-capture-playback-spike/) on -Linux + PipeWire. This spike closes the **mobile-Android** half. - -End-to-end path verified on a physical device: - -``` -Kotlin (MainActivity) - └─ JNI → Rust cdylib (audio_capture_playback_android_spike) - └─ cpal 0.16 - └─ Oboe / AAudio - └─ Android audio HAL - └─ device speaker / microphone -``` - -Specifically: - -- **Playback**: a 440 Hz mono sine, 500 ms, driven by Rust through cpal's - Oboe backend, played out through the device's default output. 22,050 - frames at 44.1 kHz emitted (exactly as expected). -- **Capture**: 1 s of stereo F32 audio from the default input device, - down-mixed to mono i16 PCM and written to app-private storage as a - valid 85,292-byte WAV file (44.1 kHz, mono, 16-bit). The file was - pulled via `adb exec-out run-as ... cat` and confirmed with `file(1)` - to be RIFF/WAVE. - -## Layout - -```text -audio-capture-playback-android-spike/ - src/ - lib.rs # JNI entry points + JNI_OnLoad + initContext + - # play_sine_inner + record_inner + panic-catching - # boilerplate - android/ - settings.gradle.kts - build.gradle.kts - gradle.properties - gradlew - gradle/wrapper/ # gradle-wrapper.{jar,properties} - app/ - build.gradle.kts # AGP + cargoBuildRust task - src/main/ - AndroidManifest.xml - java/app/chanora/poc/audio/ - MainActivity.kt - NativeAudio.kt - Cargo.toml - README.md - VERIFICATION.md -``` - -## Why a separate spike - -The PoC plan lists a single -`audio-capture-playback-spike`; the mobile and desktop halves share -the cpal abstraction but have totally different build toolchains -(Cargo only vs. Gradle + cargo-ndk + JNI + Kotlin app shell). Splitting -keeps each spike's `VERIFICATION.md` independently provable, while -both close one half of the same PoC plan entry. - -## Reproduce - -Requires: - -- Rust stable (developed against 1.95) with the Android targets - installed: - `rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android` -- Android SDK with platform 34, build-tools 34.0.0. -- Android NDK r26.x (developed against 26.3.11579264) at - `$ANDROID_NDK_HOME`. -- `cargo-ndk` 4.x (`cargo install cargo-ndk`). -- An Android device on USB (API 24+) or an emulator AVD on API 26+. - -```bash -# 1. Build the APK. AGP's preBuild depends on the custom cargoBuildRust -# task that invokes `cargo ndk -P 26 -t ... build --release` -# for all four enabled ABIs and copies the .so into jniLibs/. -cd android -./gradlew :app:assembleDebug - -# 2. Install and run on the connected device. -adb install -r app/build/outputs/apk/debug/app-debug.apk -adb shell pm grant app.chanora.poc.audio android.permission.RECORD_AUDIO -adb shell am start -n app.chanora.poc.audio/.MainActivity - -# 3. Drive the buttons (or just tap them on-device). -adb shell input tap 540 520 # "PLAY 500 MS SINE" -adb shell input tap 540 640 # "RECORD 1 S TO FILE" -adb logcat -d -s ChanoraAudioPoC ChanoraPoCActivity -``` - -The captured WAV lives at -`/data/data/app.chanora.poc.audio/files/chanora_poc_capture.wav` and -can be pulled with `adb exec-out run-as app.chanora.poc.audio cat -files/chanora_poc_capture.wav > capture.wav`. - -## NDK platform note - -cpal links `libaaudio` which was introduced at API 26. The Gradle -task therefore passes `-P 26` to `cargo-ndk` so the NDK toolchain -targets API 26 at compile time. The Android module itself stays at -`minSdk = 24` (matches DEC-004). On API 24–25 devices the AAudio path -is dynamically unavailable; cpal/Oboe falls back to OpenSL ES at -runtime. Not exercised in this spike. - -## Scope boundaries - -- **No DSP.** HPF / NS / AEC / AGC, Opus, jitter buffer, mixer all - belong to `chanora_audio` and are out of scope. -- **No bit-exact loopback assertion.** The spike proves the streams - open and frames flow; it does not verify signal integrity from - capture to playback. -- **No latency measurement.** -- **No device-rotation / lifecycle correctness.** The cpal streams - are owned by short-lived `Thread { … }` blocks and dropped when - the call returns. Production code in `chanora_audio` needs proper - lifecycle handling. -- **No iOS verification.** Deferred per - `docs/governance/product-decision-register.md` DEC-011.1. - -## Verification log - -See `VERIFICATION.md` in this directory. diff --git a/poc/audio-capture-playback-android-spike/VERIFICATION.md b/poc/audio-capture-playback-android-spike/VERIFICATION.md deleted file mode 100644 index 0fe2048..0000000 --- a/poc/audio-capture-playback-android-spike/VERIFICATION.md +++ /dev/null @@ -1,154 +0,0 @@ -# Verification record — `audio-capture-playback-android-spike` - -## Result - -PASS. The mobile half of the PoC plan §2 audio exit criterion -("at least one mobile target") is met on a physical Android device. - -This combined with the desktop half (closed by -`poc/audio-capture-playback-spike` on Linux + PipeWire on 2026-05-13) -fully retires the audio PoC. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Build host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | -| Rust toolchain | stable 1.95.0 (59807616e 2026-04-14) | -| Android SDK | platform 34, build-tools 34.0.0, platform-tools | -| Android NDK | r26.3.11579264 | -| `cargo-ndk` | 4.1.2 | -| AGP | 8.5.2 | -| Gradle | 8.7 | -| Kotlin | 1.9.24 | -| `cpal` | 0.16 | -| `hound` | 3.5 | -| `jni` | 0.21 | -| `ndk-context` | 0.1.1 | -| `android_logger` | 0.14 | -| Test device | Motorola Moto G Stylus 5G (2023), Android 14 (SDK 34), arm64-v8a (transport_id ZD222DQHFY) | - -The Android x86_64 emulator AVD `chanora-poc-api34` was created and -the system image installed, but emulator verification was **skipped -intentionally** once the physical-device run succeeded; the physical -device is stronger evidence than emulator audio HAL. - -## Reproduction - -```bash -cd poc/audio-capture-playback-android-spike/android -./gradlew :app:assembleDebug -adb install -r app/build/outputs/apk/debug/app-debug.apk -adb shell pm grant app.chanora.poc.audio android.permission.RECORD_AUDIO -adb shell am start -n app.chanora.poc.audio/.MainActivity -adb shell input tap 540 520 # Play -adb shell input tap 540 640 # Record -``` - -## Observed result - -### Playback (PLAY 500 MS SINE) - -logcat: - -``` -ChanoraPoCActivity: playing… -ChanoraAudioPoC: audio_capture_playback_android_spike: playback device='default' - sample_rate=44100 channels=2 fmt=F32 -ChanoraAudioPoC: audio_capture_playback_android_spike: playSine440 ok n=22050 -ChanoraPoCActivity: PLAY OK frames=22050 -``` - -UI status text after tap: `"PLAY OK frames=22050"`. - -22,050 frames at 44.1 kHz mono equals exactly 500 ms — the requested -duration. The full Rust → cpal → Oboe → AAudio → speaker path -completed without error. - -### Capture (RECORD 1 S TO FILE) - -logcat: - -``` -ChanoraPoCActivity: recording 1 s… -ChanoraAudioPoC: audio_capture_playback_android_spike: capture device='default' - sample_rate=44100 channels=2 fmt=F32 -ChanoraAudioPoC: audio_capture_playback_android_spike: record1sToFile ok n=42624 -ChanoraPoCActivity: RECORD OK frames=42624 bytes=85292 - path=/data/user/0/app.chanora.poc.audio/files/chanora_poc_capture.wav -``` - -UI status text after tap: `"RECORD OK frames=42624 bytes=85292 -path=/data/user/0/app.chanora.poc.audio/files/chanora_poc_capture.wav"`. - -42,624 frames at 44.1 kHz is approximately 966 ms — slightly under -the requested 1 s because `std::thread::sleep` and the cpal callback -boundary do not align perfectly. The full Rust → cpal → Oboe → AAudio -→ microphone path completed without error. - -### File integrity - -The WAV file was pulled with: - -```bash -adb exec-out run-as app.chanora.poc.audio cat files/chanora_poc_capture.wav \ - > /tmp/chanora_capture.wav -``` - -`file(1)` output: - -``` -/tmp/chanora_capture.wav: RIFF (little-endian) data, WAVE audio, - Microsoft PCM, 16 bit, mono 44100 Hz -``` - -Header bytes confirm: - -``` -00000000: 5249 4646 244d 0100 5741 5645 666d 7420 RIFF$M..WAVEfmt -00000010: 1000 0000 0100 0100 44ac 0000 8858 0100 ........D....X.. -00000020: 0200 1000 6461 7461 004d 0100 0000 0000 ....data.M...... -``` - -- `RIFF` magic, size 85,284 + 8. -- `WAVE` format, fmt chunk: PCM (0x0001), mono (0x0001), 44100 Hz - (0x0000AC44), 88200 bytes/s, block-align 2, 16 bits/sample. -- `data` chunk size 0x00014D00 = 85,248 bytes of PCM ≈ 42,624 i16 - samples — matches the reported frame count. - -## Coverage matrix - -| PoC plan exit criterion | Status | Evidence | -|---|---|---| -| Capture/playback works on at least one **desktop** target | PASS | `poc/audio-capture-playback-spike/VERIFICATION.md` | -| Capture/playback works on at least one **mobile** target | **PASS** | This document. | - -## What this spike does NOT validate - -- iOS (deferred per DEC-011.1; requires macOS + Xcode hardware). -- Android emulator audio path (skipped — physical device is - stronger evidence). -- API 24–25 devices (cpal's OpenSL ES fallback path is not exercised - here; only AAudio via API 26+). -- Bit-exact loopback signal integrity from capture to playback. -- Audio latency under load. -- Streaming reconfiguration on device change or audio focus loss. -- Background-service / foreground-service audio (required for a real - voice client; out of scope here). -- DSP, Opus encode/decode, jitter buffer, mixer — owned by - `chanora_audio`. -- Privacy-sensitive UX (microphone consent flows beyond a single - runtime grant). - -## Decisions closed by this spike - -- **DEC-011.1** sub-decision is promoted from - `Accepted (desktop) / Deferred (mobile)` to - `Accepted (desktop: cpal; Android: cpal-on-Oboe) / Deferred (iOS)`. - See `docs/governance/product-decision-register.md` v0.9.4. -- The audio row in - `docs/architecture/proof-of-concept-plan.md` §2 is promoted from - PARTIAL PASS to PASS. -- RISK-PoC-001 in `docs/governance/poc-results-summary.md` is closed - for Android; iOS portion remains open. diff --git a/poc/audio-capture-playback-android-spike/android/app/build.gradle.kts b/poc/audio-capture-playback-android-spike/android/app/build.gradle.kts deleted file mode 100644 index bd0cf97..0000000 --- a/poc/audio-capture-playback-android-spike/android/app/build.gradle.kts +++ /dev/null @@ -1,124 +0,0 @@ -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") -} - -android { - namespace = "app.chanora.poc.audio" - compileSdk = 34 - - defaultConfig { - applicationId = "app.chanora.poc.audio" - minSdk = 24 // matches DEC-004 - targetSdk = 34 - versionCode = 1 - versionName = "0.1.0-poc" - - ndk { - // Keep all four common ABIs so the same APK works on both - // the x86_64 emulator and a physical arm64 device. - abiFilters += listOf("x86_64", "arm64-v8a", "armeabi-v7a", "x86") - } - } - - sourceSets { - getByName("main") { - // cargo-ndk drops .so files here. - jniLibs.srcDirs("src/main/jniLibs") - } - } - - buildTypes { - getByName("debug") { - isMinifyEnabled = false - } - getByName("release") { - isMinifyEnabled = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = "17" - } - packaging { - // Avoid duplicate-resource conflicts from native libs if any - // future deps bring them in. - resources.excludes += setOf("META-INF/AL2.0", "META-INF/LGPL2.1") - } -} - -dependencies { - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") -} - -// --------------------------------------------------------------------- -// Rust cdylib build via cargo-ndk. -// -// Each ABI maps to a Rust target triple; cargo-ndk handles linker -// selection. The task copies the produced .so into the per-ABI -// jniLibs directory expected by Android Gradle Plugin. -// --------------------------------------------------------------------- - -val rustCrateDir = rootProject.projectDir.parentFile // ../ -> the spike root -val rustTargetDir = file("$rustCrateDir/target") -val jniLibsDir = file("$projectDir/src/main/jniLibs") - -val cargoBuildRust = tasks.register("cargoBuildRust") { - group = "build" - description = "Builds the Rust cdylib for all enabled Android ABIs via cargo ndk." - - val abis = listOf( - Triple("x86_64", "x86_64-linux-android", "x86_64"), - Triple("arm64-v8a", "aarch64-linux-android", "arm64-v8a"), - Triple("armeabi-v7a", "armv7-linux-androideabi", "armeabi-v7a"), - Triple("x86", "i686-linux-android", "x86") - ) - - doLast { - // Resolve cargo: try CARGO_HOME/bin first, then PATH, then the - // rustup shim location. - val cargoBinDir = System.getenv("CARGO_HOME")?.let { "$it/bin" } - ?: "${System.getProperty("user.home")}/.cargo/bin" - val cargo = listOf("$cargoBinDir/cargo", "/usr/local/bin/cargo", "/usr/bin/cargo") - .firstOrNull { file(it).exists() } - ?: error("cargo not found; install via rustup") - val cargoNdkBin = listOf("$cargoBinDir/cargo-ndk", "/usr/local/bin/cargo-ndk") - .firstOrNull { file(it).exists() } - check(cargoNdkBin != null) { "cargo-ndk not installed (cargo install cargo-ndk)" } - - val ndkHome = System.getenv("ANDROID_NDK_HOME") - ?: System.getenv("NDK_HOME") - ?: "/opt/android-sdk/ndk/26.3.11579264" - check(file(ndkHome).exists()) { "NDK not found at $ndkHome" } - - // cargo-ndk is a cargo subcommand; invoke via `cargo ndk ...`. - // We must ensure cargo's PATH lookup can find cargo-ndk too. - // `-p 26` selects API 26 for the NDK toolchain so cpal can - // link against libaaudio (introduced in API 26). minSdk in - // the Android module stays at 24 — the device just won't - // execute the AAudio code path below API 26; cpal/Oboe falls - // back to OpenSL ES there. - val abiArgs = abis.flatMap { listOf("-t", it.first) } - val cargoCmd = listOf(cargo, "ndk", "-P", "26") + abiArgs + - listOf("-o", jniLibsDir.absolutePath, "build", "--release", - "--manifest-path", "$rustCrateDir/Cargo.toml") - - println("[cargoBuildRust] running: ${cargoCmd.joinToString(" ")}") - - exec { - commandLine(cargoCmd) - environment("ANDROID_NDK_HOME", ndkHome) - environment("PATH", "$cargoBinDir:${System.getenv("PATH")}") - } - } -} - -// Make Android's preBuild depend on the Rust build so AGP picks up the .so files. -tasks.matching { it.name == "preBuild" }.configureEach { - dependsOn(cargoBuildRust) -} diff --git a/poc/audio-capture-playback-android-spike/android/app/src/main/AndroidManifest.xml b/poc/audio-capture-playback-android-spike/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 9201bd9..0000000 --- a/poc/audio-capture-playback-android-spike/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/MainActivity.kt b/poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/MainActivity.kt deleted file mode 100644 index 33501e5..0000000 --- a/poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/MainActivity.kt +++ /dev/null @@ -1,134 +0,0 @@ -package app.chanora.poc.audio - -import android.Manifest -import android.app.Activity -import android.content.pm.PackageManager -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.Button -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import java.io.File -import kotlin.concurrent.thread - -/** - * Chanora PoC — Android mobile audio spike. - * - * Two buttons: - * 1) Play 500 ms 440 Hz sine wave through the default output device. - * 2) Record 1 s from the default input device to app-private storage. - * - * Both call into the Rust cdylib via JNI. Status is logged to logcat - * under the tag "ChanoraAudioPoC" (Rust) and "ChanoraPoCActivity" - * (Kotlin). The UI also displays the result string. - */ -class MainActivity : Activity() { - - private companion object { - const val TAG = "ChanoraPoCActivity" - const val REQ_RECORD = 1001 - } - - private lateinit var status: TextView - private lateinit var recordBtn: Button - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Hand the Application Context to the Rust side BEFORE any - // audio call, so cpal's Oboe backend can find Android audio - // services via ndk_context. - NativeAudio.initContext(applicationContext) - - val root = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(48, 48, 48, 48) - } - - val title = TextView(this).apply { - text = "Chanora Audio PoC (Android)" - textSize = 20f - } - - status = TextView(this).apply { - text = "ready" - textSize = 14f - setPadding(0, 24, 0, 24) - } - - val playBtn = Button(this).apply { - text = "Play 500 ms sine" - setOnClickListener { onPlay() } - } - - recordBtn = Button(this).apply { - text = "Record 1 s to file" - setOnClickListener { onRecord() } - } - - root.addView(title) - root.addView(status) - root.addView(playBtn) - root.addView(recordBtn) - setContentView(root) - } - - private fun setStatus(s: String) { - Log.i(TAG, s) - runOnUiThread { status.text = s } - } - - private fun onPlay() { - setStatus("playing…") - thread(name = "chanora-poc-play") { - val frames = NativeAudio.playSine440(500L) - if (frames < 0) { - setStatus("PLAY ERROR (see logcat)") - } else { - setStatus("PLAY OK frames=$frames") - } - } - } - - private fun onRecord() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) - != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), REQ_RECORD) - setStatus("requesting RECORD_AUDIO…") - return - } - doRecord() - } - - private fun doRecord() { - setStatus("recording 1 s…") - thread(name = "chanora-poc-record") { - val path = File(filesDir, "chanora_poc_capture.wav").absolutePath - val frames = NativeAudio.record1sToFile(path, 1000L) - if (frames < 0) { - setStatus("RECORD ERROR (see logcat)") - } else { - val sz = File(path).length() - setStatus("RECORD OK frames=$frames bytes=$sz path=$path") - } - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == REQ_RECORD) { - if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) { - doRecord() - } else { - setStatus("RECORD_AUDIO permission denied") - } - } - } -} diff --git a/poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/NativeAudio.kt b/poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/NativeAudio.kt deleted file mode 100644 index 3cb8dd0..0000000 --- a/poc/audio-capture-playback-android-spike/android/app/src/main/java/app/chanora/poc/audio/NativeAudio.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.chanora.poc.audio - -import android.content.Context - -object NativeAudio { - init { - System.loadLibrary("audio_capture_playback_android_spike") - } - - /** - * Must be called once after `System.loadLibrary` with the - * application Context. Initialises the Rust-side `ndk_context` - * that cpal/Oboe relies on to locate Android audio services. - */ - external fun initContext(context: Context) - - /** Returns the number of frames emitted, or -1 on error. */ - external fun playSine440(durationMs: Long): Long - - /** Returns the number of frames captured, or -1 on error. */ - external fun record1sToFile(path: String, durationMs: Long): Long -} diff --git a/poc/audio-capture-playback-android-spike/android/build.gradle.kts b/poc/audio-capture-playback-android-spike/android/build.gradle.kts deleted file mode 100644 index c6f03e0..0000000 --- a/poc/audio-capture-playback-android-spike/android/build.gradle.kts +++ /dev/null @@ -1,5 +0,0 @@ -// Root build file — versions only; module config lives in app/build.gradle.kts. -plugins { - id("com.android.application") version "8.5.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.24" apply false -} diff --git a/poc/audio-capture-playback-android-spike/android/gradle.properties b/poc/audio-capture-playback-android-spike/android/gradle.properties deleted file mode 100644 index 2318707..0000000 --- a/poc/audio-capture-playback-android-spike/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 -android.useAndroidX=true -kotlin.code.style=official -android.nonTransitiveRClass=true diff --git a/poc/audio-capture-playback-android-spike/android/gradle/wrapper/gradle-wrapper.jar b/poc/audio-capture-playback-android-spike/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e6441136f3d4ba8a0da8d277868979cfbc8ad796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/poc/audio-capture-playback-android-spike/android/settings.gradle.kts b/poc/audio-capture-playback-android-spike/android/settings.gradle.kts deleted file mode 100644 index e1db0d1..0000000 --- a/poc/audio-capture-playback-android-spike/android/settings.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -// Settings: declare the single :app module. -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "chanora-audio-poc" -include(":app") diff --git a/poc/audio-capture-playback-android-spike/src/lib.rs b/poc/audio-capture-playback-android-spike/src/lib.rs deleted file mode 100644 index 2db1d29..0000000 --- a/poc/audio-capture-playback-android-spike/src/lib.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! Chanora PoC — Android mobile audio spike. -//! -//! Closes the mobile half of the PoC plan §2 audio exit criterion: -//! "Capture/playback works on at least one desktop and one mobile target." -//! -//! Desktop half is closed by `poc/audio-capture-playback-spike` on Linux/PipeWire. -//! This crate is the mobile-Android half. -//! -//! Approach: -//! * `cpal` 0.16 with no extra features — on Android it automatically -//! uses the Oboe backend (AAudio + OpenSL ES fallback). Matches -//! DEC-011 "platform-native first". -//! * The Kotlin app calls two JNI entry points: -//! - Java_app_chanora_poc_audio_NativeAudio_playSine440() -//! - Java_app_chanora_poc_audio_NativeAudio_record1sToFile(path) -//! * `tracing` is not used; on Android we route `log` macros to logcat -//! via `android_logger` so all output is visible via `adb logcat`. - -#![cfg(target_os = "android")] - -use std::panic::{self, AssertUnwindSafe}; -use std::sync::Once; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{SampleFormat, SizedSample}; -use hound::{SampleFormat as HoundSampleFormat, WavSpec, WavWriter}; -use jni::objects::{JClass, JString}; -use jni::sys::{jint, jlong, JNI_VERSION_1_6}; -use jni::JNIEnv; -use log::{error, info, warn, LevelFilter}; - -const TAG: &str = "ChanoraAudioPoC"; - -static LOG_INIT: Once = Once::new(); - -// Stash the JavaVM* captured at library-load time. We need it to -// resolve a JNIEnv on the audio thread later if we ever want to make -// JNI calls there. cpal's Oboe backend uses `ndk_context` instead, -// which we initialise from the Kotlin side once we have a Context. -static mut JAVA_VM: *mut std::ffi::c_void = std::ptr::null_mut(); - -/// JNI entry point invoked by the Android runtime when our cdylib is -/// loaded via `System.loadLibrary`. Captures the JavaVM* but does NOT -/// yet initialise the ndk_context — that needs a `Context` reference, -/// which only the Activity has. -#[no_mangle] -pub extern "system" fn JNI_OnLoad(vm: *mut std::ffi::c_void, _reserved: *mut std::ffi::c_void) -> jint { - ensure_logging(); - unsafe { - JAVA_VM = vm; - } - info!("JNI_OnLoad: captured JavaVM"); - JNI_VERSION_1_6 -} - -/// Called by `MainActivity.onCreate` with the activity reference. -/// Initialises the `ndk_context` so cpal's Oboe backend can locate -/// the Android `Context` it needs. -#[no_mangle] -pub extern "system" fn Java_app_chanora_poc_audio_NativeAudio_initContext<'local>( - env: JNIEnv<'local>, - _class: JClass<'local>, - context: jni::objects::JObject<'local>, -) { - ensure_logging(); - // Promote the local-ref Context to a global ref so it survives - // beyond this JNI call. ndk_context borrows the pointer. - let global = match env.new_global_ref(&context) { - Ok(g) => g, - Err(e) => { - error!("new_global_ref(context) failed: {e}"); - return; - } - }; - let raw_ctx = global.as_obj().as_raw() as *mut std::ffi::c_void; - // Leak the global ref intentionally — it must live for the - // lifetime of the library (which is the lifetime of the process). - std::mem::forget(global); - unsafe { - ndk_context::initialize_android_context(JAVA_VM, raw_ctx); - } - info!("initContext: ndk_context initialised (vm={:p}, ctx={:p})", unsafe { JAVA_VM }, raw_ctx); -} - -fn ensure_logging() { - LOG_INIT.call_once(|| { - android_logger::init_once( - android_logger::Config::default() - .with_max_level(LevelFilter::Trace) - .with_tag(TAG), - ); - // Route panics into logcat so they don't disappear into the void. - panic::set_hook(Box::new(|info| { - let payload = if let Some(s) = info.payload().downcast_ref::<&str>() { - (*s).to_string() - } else if let Some(s) = info.payload().downcast_ref::() { - s.clone() - } else { - "".to_string() - }; - let location = info - .location() - .map(|l| format!("{}:{}", l.file(), l.line())) - .unwrap_or_else(|| "".to_string()); - error!("RUST PANIC at {location}: {payload}"); - })); - info!("logging initialised"); - }); -} - -/// Run a closure that produces a result, catching any panic and -/// converting it to an error string. Required because our JNI -/// functions are `extern "system"` and panicking across that -/// boundary is undefined behaviour (and aborts the process). -fn catch Result>(label: &str, f: F) -> jlong { - let r = panic::catch_unwind(AssertUnwindSafe(f)); - match r { - Ok(Ok(n)) => { - info!("{label} ok n={n}"); - n as jlong - } - Ok(Err(e)) => { - error!("{label} returned err: {e}"); - -1 - } - Err(_) => { - // The panic hook already logged the details. - error!("{label} panicked (caught)"); - -2 - } - } -} - -/// Synth a 440 Hz mono sine for `duration_ms` directly through the -/// default output device. Returns a status code (0 = OK, non-zero = -/// error code). Errors are also logged to logcat. -#[no_mangle] -pub extern "system" fn Java_app_chanora_poc_audio_NativeAudio_playSine440( - _env: JNIEnv, - _class: JClass, - duration_ms: jlong, -) -> jlong { - ensure_logging(); - let dur = Duration::from_millis(duration_ms.max(0) as u64); - catch("playSine440", || play_sine_inner(440.0, dur)) -} - -fn play_sine_inner(freq_hz: f32, duration: Duration) -> Result { - let host = cpal::default_host(); - let device = host - .default_output_device() - .ok_or_else(|| "no default output device".to_string())?; - let device_name = device.name().unwrap_or_else(|_| "".to_string()); - let cfg = device - .default_output_config() - .map_err(|e| format!("default_output_config: {e}"))?; - info!( - "playback device='{}' sample_rate={} channels={} fmt={:?}", - device_name, - cfg.sample_rate().0, - cfg.channels(), - cfg.sample_format() - ); - - let sample_rate = cfg.sample_rate().0 as f32; - let channels = cfg.channels() as usize; - let total_frames = (duration.as_secs_f32() * sample_rate) as u64; - let phase = Arc::new(Mutex::new(0.0f32)); - let frames_emitted = Arc::new(Mutex::new(0u64)); - let done = Arc::new(Mutex::new(false)); - - let stream_config: cpal::StreamConfig = cfg.clone().into(); - - fn err_fn(e: cpal::StreamError) { - error!("playback stream error: {e}"); - } - - let stream = match cfg.sample_format() { - SampleFormat::F32 => build_play_stream::( - &device, - &stream_config, - sample_rate, - freq_hz, - channels, - total_frames, - phase.clone(), - frames_emitted.clone(), - done.clone(), - err_fn, - )?, - SampleFormat::I16 => build_play_stream::( - &device, - &stream_config, - sample_rate, - freq_hz, - channels, - total_frames, - phase.clone(), - frames_emitted.clone(), - done.clone(), - err_fn, - )?, - SampleFormat::U16 => build_play_stream::( - &device, - &stream_config, - sample_rate, - freq_hz, - channels, - total_frames, - phase.clone(), - frames_emitted.clone(), - done.clone(), - err_fn, - )?, - other => return Err(format!("unsupported sample format: {other:?}")), - }; - - stream - .play() - .map_err(|e| format!("play(): {e}"))?; - - // Wait until either the producer reaches total_frames or a generous timeout. - let timeout = duration + Duration::from_millis(500); - let start = std::time::Instant::now(); - loop { - if *done.lock().unwrap() { - break; - } - if start.elapsed() > timeout { - warn!("playback timeout reached"); - break; - } - std::thread::sleep(Duration::from_millis(20)); - } - drop(stream); - let final_frames = *frames_emitted.lock().unwrap(); - Ok(final_frames) -} - -fn build_play_stream( - device: &cpal::Device, - config: &cpal::StreamConfig, - sample_rate: f32, - freq_hz: f32, - channels: usize, - total_frames: u64, - phase: Arc>, - frames_emitted: Arc>, - done: Arc>, - err_fn: fn(cpal::StreamError), -) -> Result -where - T: SizedSample + FromSineSample + Send + 'static, -{ - let phase_inc = 2.0 * std::f32::consts::PI * freq_hz / sample_rate; - let stream = device - .build_output_stream( - config, - move |out: &mut [T], _| { - let mut ph = phase.lock().unwrap(); - let mut emitted = frames_emitted.lock().unwrap(); - for frame in out.chunks_mut(channels) { - if *emitted >= total_frames { - for s in frame.iter_mut() { - *s = T::from_f32(0.0); - } - *done.lock().unwrap() = true; - } else { - let v = (*ph).sin() * 0.5; - *ph = (*ph + phase_inc) % (2.0 * std::f32::consts::PI); - for s in frame.iter_mut() { - *s = T::from_f32(v); - } - *emitted += 1; - } - } - }, - err_fn, - None, - ) - .map_err(|e| format!("build_output_stream: {e}"))?; - Ok(stream) -} - -pub trait FromSineSample { - fn from_f32(v: f32) -> Self; -} -impl FromSineSample for f32 { - fn from_f32(v: f32) -> Self { - v - } -} -impl FromSineSample for i16 { - fn from_f32(v: f32) -> Self { - (v.clamp(-1.0, 1.0) * f32::from(i16::MAX)) as i16 - } -} -impl FromSineSample for u16 { - fn from_f32(v: f32) -> Self { - let s = (v.clamp(-1.0, 1.0) * f32::from(i16::MAX)) as i32; - (s + i32::from(i16::MAX) + 1) as u16 - } -} - -/// Record `duration_ms` of mono 16-bit PCM to `path` from the default -/// input device. Returns frames captured (>= 0) or -1 on error. -#[no_mangle] -pub extern "system" fn Java_app_chanora_poc_audio_NativeAudio_record1sToFile( - mut env: JNIEnv, - _class: JClass, - path: JString, - duration_ms: jlong, -) -> jlong { - ensure_logging(); - let path_str: String = match env.get_string(&path) { - Ok(s) => s.into(), - Err(e) => { - error!("get_string(path) failed: {e}"); - return -1; - } - }; - let dur = Duration::from_millis(duration_ms.max(0) as u64); - catch("record1sToFile", move || record_inner(&path_str, dur)) -} - -fn record_inner(path: &str, duration: Duration) -> Result { - let host = cpal::default_host(); - let device = host - .default_input_device() - .ok_or_else(|| "no default input device".to_string())?; - let device_name = device.name().unwrap_or_else(|_| "".to_string()); - let cfg = device - .default_input_config() - .map_err(|e| format!("default_input_config: {e}"))?; - info!( - "capture device='{}' sample_rate={} channels={} fmt={:?}", - device_name, - cfg.sample_rate().0, - cfg.channels(), - cfg.sample_format() - ); - let spec = WavSpec { - channels: 1, - sample_rate: cfg.sample_rate().0, - bits_per_sample: 16, - sample_format: HoundSampleFormat::Int, - }; - let writer = Arc::new(Mutex::new(Some( - WavWriter::create(path, spec).map_err(|e| format!("wav create: {e}"))?, - ))); - let frames = Arc::new(Mutex::new(0u64)); - let channels = cfg.channels() as usize; - - let stream_config: cpal::StreamConfig = cfg.clone().into(); - - fn err_fn(e: cpal::StreamError) { - error!("capture stream error: {e}"); - } - - let stream = match cfg.sample_format() { - SampleFormat::F32 => build_capture_stream::( - &device, &stream_config, writer.clone(), frames.clone(), channels, err_fn, - )?, - SampleFormat::I16 => build_capture_stream::( - &device, &stream_config, writer.clone(), frames.clone(), channels, err_fn, - )?, - SampleFormat::U16 => build_capture_stream::( - &device, &stream_config, writer.clone(), frames.clone(), channels, err_fn, - )?, - other => return Err(format!("unsupported sample format: {other:?}")), - }; - - stream - .play() - .map_err(|e| format!("capture play(): {e}"))?; - std::thread::sleep(duration); - drop(stream); - - if let Some(w) = writer.lock().unwrap().take() { - w.finalize().map_err(|e| format!("wav finalize: {e}"))?; - } - let n = *frames.lock().unwrap(); - Ok(n) -} - -fn build_capture_stream( - device: &cpal::Device, - config: &cpal::StreamConfig, - writer: Arc>>>>, - frames: Arc>, - channels: usize, - err_fn: fn(cpal::StreamError), -) -> Result -where - T: SizedSample + ToI16Sample + Send + 'static, -{ - let stream = device - .build_input_stream( - config, - move |data: &[T], _| { - let mut w_guard = writer.lock().unwrap(); - if let Some(w) = w_guard.as_mut() { - let mut n = 0u64; - for frame in data.chunks(channels) { - let mut acc: i32 = 0; - for s in frame { - acc += s.to_i16_sample() as i32; - } - let mono = (acc / frame.len() as i32) as i16; - let _ = w.write_sample(mono); - n += 1; - } - *frames.lock().unwrap() += n; - } - }, - err_fn, - None, - ) - .map_err(|e| format!("build_input_stream: {e}"))?; - Ok(stream) -} - -pub trait ToI16Sample { - fn to_i16_sample(&self) -> i16; -} -impl ToI16Sample for i16 { - fn to_i16_sample(&self) -> i16 { - *self - } -} -impl ToI16Sample for u16 { - fn to_i16_sample(&self) -> i16 { - (i32::from(*self) - i32::from(i16::MAX) - 1) as i16 - } -} -impl ToI16Sample for f32 { - fn to_i16_sample(&self) -> i16 { - let v = (*self * f32::from(i16::MAX)).clamp(f32::from(i16::MIN), f32::from(i16::MAX)); - v as i16 - } -} diff --git a/poc/audio-capture-playback-spike/Cargo.lock b/poc/audio-capture-playback-spike/Cargo.lock deleted file mode 100644 index 12b728e..0000000 --- a/poc/audio-capture-playback-spike/Cargo.lock +++ /dev/null @@ -1,1315 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.11.1", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "audio-capture-playback-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "cpal", - "hound", - "tempfile", - "thiserror 2.0.18", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[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 = "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 = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "coreaudio-rs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" -dependencies = [ - "bitflags 1.3.2", - "libc", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "cpal" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" -dependencies = [ - "alsa", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.11.1", - "objc2", -] - -[[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 = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[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.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" -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 = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[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 = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.11.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[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-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-audio-toolbox" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" -dependencies = [ - "bitflags 2.11.1", - "libc", - "objc2", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" -dependencies = [ - "bitflags 2.11.1", - "objc2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "objc2", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[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-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[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 = "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 = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[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 = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[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 = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - -[[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 = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[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 = "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" - -[[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 = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[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 = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[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 = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[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" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - -[[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.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[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.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 = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/poc/audio-capture-playback-spike/Cargo.toml b/poc/audio-capture-playback-spike/Cargo.toml deleted file mode 100644 index 79db97a..0000000 --- a/poc/audio-capture-playback-spike/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "audio-capture-playback-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: cross-platform audio capture + playback round-trip via cpal. Linux desktop verified; mobile path documented but unverified." - -# Not product code. See docs/architecture/proof-of-concept-plan.md §4. -# Authority: PoC plan §2, DEC-011 (platform-native first audio), SysDes-013 -# (audio subsystem), SRS audio family. - -[lib] -name = "audio_capture_playback_spike" -path = "src/lib.rs" - -[[bin]] -name = "audio-capture-playback-cli" -path = "src/main.rs" - -[dependencies] -# `cpal` is the de facto Rust cross-platform audio crate. It speaks -# WASAPI/CoreAudio/ALSA/PipeWire/Oboe and matches the "platform-native -# first" stance of DEC-011. -cpal = "0.16" -hound = "3" -thiserror = "2" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -anyhow = "1" - -[dev-dependencies] -tempfile = "3" diff --git a/poc/audio-capture-playback-spike/README.md b/poc/audio-capture-playback-spike/README.md deleted file mode 100644 index 92f2b69..0000000 --- a/poc/audio-capture-playback-spike/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Audio Capture / Playback Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `audio-capture-playback-spike` | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Prove platform audio capture + playback behaviour | -| Exit criterion (plan) | "Capture/playback works on at least one desktop and one mobile target" | -| Authority | DEC-011 (platform-native first), audio family in SysDes / SRS | - -## Honest status - -**Partial pass.** - -- **Linux desktop**: verified. cpal opens both the default input and - the default output device, capture streams build (or report a typed - error when no hardware-backed input is available), and playback - drives a full PCM stream end-to-end through the platform audio - stack (ALSA → PipeWire on this host). -- **Mobile (Android / iOS)**: **not verified**. cpal supports Oboe - (Android) and AVAudioEngine (iOS) but this PoC has not been built - or run on either. The exit criterion's mobile half remains an open - item for a follow-up spike — see "Gaps" below. - -## What it proves - -- `AudioCapture::record_to_wav(path, duration)` — opens the default - input device, reads frames in any supported sample format, down-mixes - to mono, writes 16-bit-PCM WAV. -- `AudioPlayback::play_wav(path)` — opens the default output device, - picks a stream config matching the file's sample rate, blocks until - the file drains. -- `synth_sine_wav` — produces a deterministic 440 Hz sine WAV so the - playback path is exercisable on hosts without a usable input device. -- A typed `AudioError` covering the practical failure modes (no - device, config mismatch, stream-build failure, WAV I/O, unsupported - format). Production code (`chanora_audio`) will widen this when DSP - + Opus + jitter buffer surfaces land. -- Headless-friendly behaviour: if capture cannot build a stream, the - CLI falls back to synth and the playback path is still exercised. - This is the same shape production code would use for a - "device test" UI. - -## Layout - -```text -audio-capture-playback-spike/ - src/ - lib.rs # crate root + re-exports - capture.rs # AudioCapture, AudioError, sample-format adapters - playback.rs # AudioPlayback - synth.rs # synth_sine_wav (deterministic test signal) - main.rs # audio-capture-playback-cli - tests/ - audio.rs # 2 tests - Cargo.toml -``` - -## Reproduce (Linux desktop) - -Requires Rust stable, ALSA dev headers (cpal builds against ALSA on -Linux), and a working audio stack (PipeWire / Pulse / pure ALSA). - -```bash -cargo test -cargo run --bin audio-capture-playback-cli -- roundtrip -``` - -CLI modes: - -| Mode | What it does | -|---|---| -| `capture` | Records 1 s of audio from the default input device to `${TMPDIR}/chanora_audio_spike_capture.wav`. | -| `synth` | Writes a 440 Hz / 500 ms sine wave to `${TMPDIR}/chanora_audio_spike_synth.wav`. | -| `playback` | Plays the synth WAV through the default output device. | -| `roundtrip` (default) | Tries `capture`; on any failure, falls back to `synth`; then `playback`. | - -### PipeWire-only Linux hosts - -On hosts where the only audio server is PipeWire (no `pulseaudio` -package), `cpal` still uses ALSA underneath and needs a -`pcm.!default` alias to the PipeWire PCM plugin. The package -`pipewire-alsa` (or your distro equivalent) plus a one-line -`~/.asoundrc` is the standard fix: - -```text -pcm.!default { type pipewire } -ctl.!default { type pipewire } -``` - -## Gaps (the mobile half of the exit criterion) - -| Target | What's missing | Notes | -|---|---|---| -| Android | Build & run cpal-oboe path on an emulator or device | Needs NDK + Gradle wiring + emulator. cpal supports it; this spike has not built it. | -| iOS | Build & run cpal-AVAudioEngine path on a device | Needs macOS + Xcode + a developer account. | - -These should be closed by a follow-up spike (or by promotion into the -product `chanora_audio` crate scaffold) before the audio half of the -PoC matrix is considered complete. - -## Scope boundaries - -- **No DSP.** HPF / NS / AEC / AGC / mixing / Opus encode-decode / - jitter buffer all belong to `chanora_audio` and are out of scope. -- **No bit-exact loopback assertion.** This PoC verifies *that* a - capture stream can be opened and *that* a playback stream can drive - a file to completion. It does not verify capture-to-playback signal - integrity. -- **No latency measurement.** -- **No device-permission flows.** Mobile microphone consent UI is - product-level. -- **No echo-cancellation evidence.** Different PoC. - -## Verification log - -See `VERIFICATION.md` in this directory. diff --git a/poc/audio-capture-playback-spike/VERIFICATION.md b/poc/audio-capture-playback-spike/VERIFICATION.md deleted file mode 100644 index dfe8cb8..0000000 --- a/poc/audio-capture-playback-spike/VERIFICATION.md +++ /dev/null @@ -1,123 +0,0 @@ -# Verification record — `audio-capture-playback-spike` - -## Result - -**PASS (desktop half).** The Linux/PipeWire desktop half of the PoC -plan §2 audio exit criterion is fully met by this spike. - -The mobile half ("at least one mobile target") is closed by -[`poc/audio-capture-playback-android-spike`](../audio-capture-playback-android-spike/) -on a physical Android device — see that directory's `VERIFICATION.md`. - -Combined: the audio PoC is fully retired. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | -| Rust toolchain | stable 1.95.0 | -| `cpal` | 0.16 | -| `hound` (WAV I/O) | 3.5 | -| Audio server | PipeWire 1.6.4 (with the `pcm_pipewire` ALSA plugin module) | -| Default input | `auto_null` (system has no physical microphone bound to this session) | -| Default output | `auto_null` (PipeWire null sink) | -| Local config | `~/.asoundrc` aliases `pcm.!default` and `ctl.!default` to `pipewire` | - -## Reproduction - -```bash -cargo test -cargo run --bin audio-capture-playback-cli -- roundtrip -``` - -## Test run - -``` -running 2 tests -test synth_produces_valid_wav_with_expected_frame_count ... ok -test synth_then_playback_does_not_error_when_output_device_present ... ok - -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; - 0 filtered out; finished in 0.06s -``` - -## CLI round-trip - -``` -INFO spike: opening capture stream device="default" sample_rate=44100 - channels=2 sample_format=F32 -WARN spike: capture unavailable; falling back to synth - error=stream build error: A backend-specific error has - occurred: ALSA function 'snd_pcm_hw_params' failed with - error 'No such file or directory (2)' -INFO spike: synthesised fallback frames=24000 sample_rate=48000 -INFO spike: preparing playback device="default" file_sr=48000 - file_channels=1 samples=24000 -INFO spike: playback finished consumed=24000 total=24000 -round-trip complete (played from /tmp/chanora_audio_spike_synth.wav) -``` - -### What this evidences - -1. cpal successfully **opened the default input device** (negotiated - 44.1 kHz / 2-channel / F32 against PipeWire's `auto_null`). -2. The capture **stream build** then failed because the null source - does not expose `snd_pcm_hw_params` at that configuration. This is - a *real* platform-audio failure mode; the typed - `AudioError::BuildStream` was returned and surfaced cleanly. This - is the same shape production code will see on misconfigured user - hosts. -3. The fallback synth wrote a valid 24,000-frame, 48 kHz, mono, - 16-bit WAV. -4. cpal **opened the default output device**, negotiated the - matching config, and the playback callback drained the WAV - completely: `consumed=24000 total=24000`. Process exited 0. - -The full path **Rust → cpal → ALSA → `pcm_pipewire` → PipeWire → -`auto_null`** ran end-to-end, with PCM frames produced by Rust -consumed by the platform audio stack. - -## Coverage matrix - -| PoC plan requirement | Status | -|---|---| -| Desktop capture path | PASS (stream opens against the system audio stack; typed error returned when device cannot satisfy `hw_params`). | -| Desktop playback path | PASS (24,000 frames driven to completion). | -| Mobile capture path | NOT VERIFIED. | -| Mobile playback path | NOT VERIFIED. | -| Round-trip flow | PASS — capture-attempt → fallback → playback. | -| Typed error model | PASS — `AudioError` arms returned cleanly. | -| Cross-platform crate choice (DEC-011 alignment) | PARTIAL — `cpal` is a platform-native abstraction, but only the Linux back-end exercised here. | - -## What this spike does NOT validate - -- Real microphone capture (this host has none). -- Bit-exact capture-to-playback signal integrity. -- Latency. -- Hot-plug / device-change events. -- Permission flows (mobile microphone consent, Linux pulse permission - prompts). -- Behaviour under sample-rate or channel-count mismatch between file - and device (the PoC chose the file's rate; production code will - need a real resampler). -- DSP (HPF / NS / AEC / AGC). -- Opus encode / decode. -- Jitter buffer / mixer. -- Mobile (Android Oboe / iOS AVAudioEngine) — **explicit gap**. - -## Recommended follow-up - -1. Promote `cpal` selection to a recorded product decision (DEC-011 - sub-decision) — or pin a different crate after the mobile spike. - Done on 2026-05-13 as DEC-011.1 in - `docs/governance/product-decision-register.md`. -2. Spike Android Oboe path on an emulator or real device. - Done on 2026-05-13 — see - `poc/audio-capture-playback-android-spike/VERIFICATION.md`. -3. Spike iOS AVAudioEngine path on a real device. - Still deferred; requires macOS + Xcode hardware. -4. Only after the iOS half is closed (or explicitly waived): - promote the typed `AudioError` + capture/playback shapes into - `chanora_audio`. diff --git a/poc/audio-capture-playback-spike/src/capture.rs b/poc/audio-capture-playback-spike/src/capture.rs deleted file mode 100644 index 070c78d..0000000 --- a/poc/audio-capture-playback-spike/src/capture.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! Audio capture: open the default input device, record N seconds of -//! 16-bit mono PCM, write to a WAV file. - -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{Sample, SampleFormat}; -use hound::{SampleFormat as HoundSampleFormat, WavSpec, WavWriter}; -use thiserror::Error; -use tracing::{info, warn}; - -#[derive(Debug, Error)] -pub enum AudioError { - #[error("no input device available")] - NoInputDevice, - #[error("no output device available")] - NoOutputDevice, - #[error("device default config error: {0}")] - DefaultConfig(#[from] cpal::DefaultStreamConfigError), - #[error("device supported config error: {0}")] - SupportedConfig(#[from] cpal::SupportedStreamConfigsError), - #[error("stream build error: {0}")] - BuildStream(#[from] cpal::BuildStreamError), - #[error("stream play error: {0}")] - PlayStream(#[from] cpal::PlayStreamError), - #[error("wav io: {0}")] - Wav(#[from] hound::Error), - #[error("io: {0}")] - Io(#[from] std::io::Error), - #[error("unsupported sample format: {0:?}")] - UnsupportedFormat(SampleFormat), -} - -pub struct AudioCapture; - -impl AudioCapture { - /// Capture `duration` from the default input device into `path` as - /// 16-bit mono PCM WAV. Returns the number of frames written. - pub fn record_to_wav(path: &Path, duration: Duration) -> Result { - let host = cpal::default_host(); - let device = host.default_input_device().ok_or(AudioError::NoInputDevice)?; - let device_name = device.name().unwrap_or_else(|_| "".to_string()); - let config = device.default_input_config()?; - let sample_rate = config.sample_rate().0; - let channels = config.channels(); - let sample_format = config.sample_format(); - info!( - target: "spike", - device = %device_name, - sample_rate, channels, ?sample_format, - "opening capture stream" - ); - - let spec = WavSpec { - channels: 1, // we down-mix to mono - sample_rate, - bits_per_sample: 16, - sample_format: HoundSampleFormat::Int, - }; - let writer = Arc::new(Mutex::new(Some(WavWriter::create(path, spec)?))); - let frames = Arc::new(Mutex::new(0u64)); - - let err_fn = |e| warn!(target: "spike", error = %e, "capture stream error"); - - let stream = match sample_format { - SampleFormat::F32 => build_input_stream::( - &device, - &config.into(), - writer.clone(), - frames.clone(), - channels, - err_fn, - )?, - SampleFormat::I16 => build_input_stream::( - &device, - &config.into(), - writer.clone(), - frames.clone(), - channels, - err_fn, - )?, - SampleFormat::U16 => build_input_stream::( - &device, - &config.into(), - writer.clone(), - frames.clone(), - channels, - err_fn, - )?, - other => return Err(AudioError::UnsupportedFormat(other)), - }; - - stream.play()?; - std::thread::sleep(duration); - drop(stream); // stops capture - - // Finalize the WAV. - if let Some(w) = writer.lock().unwrap().take() { - w.finalize()?; - } - let n = *frames.lock().unwrap(); - info!(target: "spike", frames = n, "capture finished"); - Ok(n) - } -} - -fn build_input_stream( - device: &cpal::Device, - config: &cpal::StreamConfig, - writer: Arc>>>>, - frames: Arc>, - channels: u16, - err_fn: fn(cpal::StreamError), -) -> Result -where - T: SizedSample + ToI16, -{ - let stream = device.build_input_stream( - config, - move |data: &[T], _| { - let mut w_guard = writer.lock().unwrap(); - if let Some(w) = w_guard.as_mut() { - let mut n = 0u64; - for frame in data.chunks(channels as usize) { - // Down-mix to mono by averaging channels. - let mut acc: i32 = 0; - for s in frame { - acc += s.to_i16() as i32; - } - let mono = (acc / frame.len() as i32) as i16; - let _ = w.write_sample(mono); - n += 1; - } - *frames.lock().unwrap() += n; - } - }, - err_fn, - None, - )?; - Ok(stream) -} - -/// `cpal::SizedSample` is the public marker. We expose it so the -/// generic stream builder can constrain `T` without leaking cpal types -/// out of this module. -pub trait SizedSample: cpal::SizedSample + Send + 'static {} -impl SizedSample for T {} - -/// Adapter so we can convert any supported sample format down to i16 -/// for WAV writing. -pub trait ToI16 { - fn to_i16(&self) -> i16; -} - -impl ToI16 for i16 { - fn to_i16(&self) -> i16 { - *self - } -} - -impl ToI16 for u16 { - fn to_i16(&self) -> i16 { - (i32::from(*self) - i32::from(i16::MAX) - 1) as i16 - } -} - -impl ToI16 for f32 { - fn to_i16(&self) -> i16 { - let v = (*self * f32::from(i16::MAX)).clamp(f32::from(i16::MIN), f32::from(i16::MAX)); - v as i16 - } -} - -// `Sample` trait import is required for cpal's older method shapes; we -// keep an explicit no-op use so this stays consistent across versions. -#[allow(dead_code)] -fn _assert_sample() {} diff --git a/poc/audio-capture-playback-spike/src/lib.rs b/poc/audio-capture-playback-spike/src/lib.rs deleted file mode 100644 index ef07d7b..0000000 --- a/poc/audio-capture-playback-spike/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Chanora PoC — audio capture / playback spike. -//! -//! Authority: -//! * `docs/architecture/proof-of-concept-plan.md` §2 — Audio -//! capture/playback spike. Exit criterion: "Capture/playback works -//! on at least one desktop and one mobile target." -//! * DEC-011 — platform-native first; Rust/WebRTC-style as fallback. -//! * SysDes audio subsystem. -//! -//! Honest scope: -//! * Linux desktop is verified here via `cpal` against the system -//! audio stack (PipeWire / Pulse / ALSA, whichever is active). -//! * **Mobile targets are NOT verified by this PoC.** That is a -//! documented gap, not a passed criterion. -//! -//! What the PoC shows: -//! * `AudioCapture` — opens the default input device and writes -//! 16-bit PCM samples to a WAV file via `hound`. -//! * `AudioPlayback` — opens the default output device and plays -//! back a WAV file end-to-end. -//! * `synth_sine_wav` — when no input device is available -//! (genuinely headless box), the PoC can still exercise the -//! playback path against a synthesised waveform. This is what -//! keeps the spike useful in CI. -//! -//! Production code (`chanora_audio`) will own the real DSP chain -//! (HPF, NS, AEC, AGC, Opus encode/decode, jitter buffer, mixer). -//! None of that lives here. - -pub mod capture; -pub mod playback; -pub mod synth; - -pub use capture::{AudioCapture, AudioError}; -pub use playback::AudioPlayback; -pub use synth::synth_sine_wav; diff --git a/poc/audio-capture-playback-spike/src/main.rs b/poc/audio-capture-playback-spike/src/main.rs deleted file mode 100644 index e12836e..0000000 --- a/poc/audio-capture-playback-spike/src/main.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! CLI driver — capture or playback or both. - -use std::path::PathBuf; -use std::process::ExitCode; -use std::time::Duration; - -use audio_capture_playback_spike::{synth_sine_wav, AudioCapture, AudioPlayback}; -use tracing::{info, warn}; - -fn main() -> ExitCode { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .init(); - - let mode = std::env::args().nth(1).unwrap_or_else(|| "roundtrip".into()); - let tmp = std::env::temp_dir(); - let cap_path = tmp.join("chanora_audio_spike_capture.wav"); - let play_path = tmp.join("chanora_audio_spike_synth.wav"); - - let result: anyhow::Result<()> = match mode.as_str() { - "capture" => do_capture(&cap_path), - "synth" => do_synth(&play_path), - "playback" => do_playback(&play_path), - "roundtrip" => do_roundtrip(&cap_path, &play_path), - other => { - eprintln!("unknown mode: {other}. Use one of: capture, synth, playback, roundtrip."); - return ExitCode::from(2); - } - }; - - if let Err(e) = result { - eprintln!("error: {e:#}"); - return ExitCode::from(1); - } - ExitCode::SUCCESS -} - -fn do_capture(path: &PathBuf) -> anyhow::Result<()> { - info!(target: "spike", "capture → {}", path.display()); - let frames = AudioCapture::record_to_wav(path, Duration::from_secs(1))?; - println!("captured {} frames to {}", frames, path.display()); - Ok(()) -} - -fn do_synth(path: &PathBuf) -> anyhow::Result<()> { - info!(target: "spike", "synth → {}", path.display()); - let (frames, sr) = synth_sine_wav(path, 440.0, Duration::from_millis(500), 48_000)?; - println!("synthesised {} frames @ {} Hz to {}", frames, sr, path.display()); - Ok(()) -} - -fn do_playback(path: &PathBuf) -> anyhow::Result<()> { - info!(target: "spike", "playback ← {}", path.display()); - AudioPlayback::play_wav(path)?; - println!("playback finished"); - Ok(()) -} - -fn do_roundtrip(cap_path: &PathBuf, play_path: &PathBuf) -> anyhow::Result<()> { - // Try real capture first; if no input device or the capture failed, - // fall back to synthesised audio so the playback path is still - // exercised. This mirrors what production code would do during - // a "device test" UI on headless or permission-denied hosts. - let to_play = match AudioCapture::record_to_wav(cap_path, Duration::from_millis(750)) { - Ok(n) if n > 0 => { - info!(target: "spike", "captured {} frames; playing back", n); - cap_path.clone() - } - Ok(_) => { - warn!(target: "spike", "capture wrote 0 frames; falling back to synth"); - let (n, sr) = synth_sine_wav(play_path, 440.0, Duration::from_millis(500), 48_000)?; - info!(target: "spike", frames = n, sample_rate = sr, "synthesised fallback"); - play_path.clone() - } - Err(e) => { - warn!(target: "spike", error = %e, "capture unavailable; falling back to synth"); - let (n, sr) = synth_sine_wav(play_path, 440.0, Duration::from_millis(500), 48_000)?; - info!(target: "spike", frames = n, sample_rate = sr, "synthesised fallback"); - play_path.clone() - } - }; - - AudioPlayback::play_wav(&to_play)?; - println!("round-trip complete (played from {})", to_play.display()); - Ok(()) -} diff --git a/poc/audio-capture-playback-spike/src/playback.rs b/poc/audio-capture-playback-spike/src/playback.rs deleted file mode 100644 index ffab429..0000000 --- a/poc/audio-capture-playback-spike/src/playback.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Audio playback: open the default output device, stream a WAV file -//! to it until the file is consumed. - -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{SampleFormat, SizedSample}; -use hound::WavReader; -use tracing::{info, warn}; - -use crate::capture::AudioError; - -pub struct AudioPlayback; - -impl AudioPlayback { - /// Play a 16-bit-PCM WAV file through the default output device. - /// Blocks until the file has been fully streamed. - pub fn play_wav(path: &Path) -> Result<(), AudioError> { - let host = cpal::default_host(); - let device = host.default_output_device().ok_or(AudioError::NoOutputDevice)?; - let device_name = device.name().unwrap_or_else(|_| "".to_string()); - - // Read all WAV samples up-front (PoC; production code would - // stream in chunks). Resample is not done here — we pick a - // device config that matches the file's sample rate when - // possible. - let mut reader = WavReader::open(path)?; - let wav_spec = reader.spec(); - let samples: Vec = reader.samples::().collect::>()?; - info!( - target: "spike", - device = %device_name, - file_sr = wav_spec.sample_rate, - file_channels = wav_spec.channels, - samples = samples.len(), - "preparing playback" - ); - - // Pick a supported config that matches the file's channel count - // and sample rate where possible. - let default_cfg = device.default_output_config()?; - let config = cpal::StreamConfig { - channels: default_cfg.channels(), - sample_rate: cpal::SampleRate(wav_spec.sample_rate), - buffer_size: cpal::BufferSize::Default, - }; - - let cursor = Arc::new(Mutex::new(0usize)); - let total = samples.len(); - let samples_arc = Arc::new(samples); - let out_channels = config.channels as usize; - let done = Arc::new(Mutex::new(false)); - - let err_fn = |e| warn!(target: "spike", error = %e, "playback stream error"); - - let stream = match default_cfg.sample_format() { - SampleFormat::F32 => build_output_stream::( - &device, &config, samples_arc.clone(), cursor.clone(), out_channels, done.clone(), err_fn, - )?, - SampleFormat::I16 => build_output_stream::( - &device, &config, samples_arc.clone(), cursor.clone(), out_channels, done.clone(), err_fn, - )?, - SampleFormat::U16 => build_output_stream::( - &device, &config, samples_arc.clone(), cursor.clone(), out_channels, done.clone(), err_fn, - )?, - other => return Err(AudioError::UnsupportedFormat(other)), - }; - stream.play()?; - - // Wait for the callback to drain the buffer. - let timeout = Duration::from_secs_f64( - (total as f64 / wav_spec.sample_rate as f64 / wav_spec.channels as f64) + 2.0, - ); - let start = std::time::Instant::now(); - loop { - if *done.lock().unwrap() { - break; - } - if start.elapsed() > timeout { - warn!(target: "spike", "playback timeout reached"); - break; - } - std::thread::sleep(Duration::from_millis(50)); - } - - drop(stream); - let consumed = *cursor.lock().unwrap(); - info!(target: "spike", consumed, total, "playback finished"); - Ok(()) - } -} - -fn build_output_stream( - device: &cpal::Device, - config: &cpal::StreamConfig, - samples: Arc>, - cursor: Arc>, - out_channels: usize, - done: Arc>, - err_fn: fn(cpal::StreamError), -) -> Result -where - T: SizedSample + FromI16 + Send + 'static, -{ - let stream = device.build_output_stream( - config, - move |out: &mut [T], _| { - let mut idx = cursor.lock().unwrap(); - for frame in out.chunks_mut(out_channels) { - if *idx >= samples.len() { - // End of file — write silence; signal done. - for s in frame.iter_mut() { - *s = T::from_i16(0); - } - *done.lock().unwrap() = true; - } else { - let sample = samples[*idx]; - *idx += 1; - for s in frame.iter_mut() { - *s = T::from_i16(sample); - } - } - } - }, - err_fn, - None, - )?; - Ok(stream) -} - -pub trait FromI16 { - fn from_i16(v: i16) -> Self; -} - -impl FromI16 for i16 { - fn from_i16(v: i16) -> Self { - v - } -} - -impl FromI16 for u16 { - fn from_i16(v: i16) -> Self { - (i32::from(v) + i32::from(i16::MAX) + 1) as u16 - } -} - -impl FromI16 for f32 { - fn from_i16(v: i16) -> Self { - f32::from(v) / f32::from(i16::MAX) - } -} diff --git a/poc/audio-capture-playback-spike/src/synth.rs b/poc/audio-capture-playback-spike/src/synth.rs deleted file mode 100644 index 0c393f9..0000000 --- a/poc/audio-capture-playback-spike/src/synth.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Synthesised waveform for environments with no real input device. -//! -//! Generates a 16-bit-PCM mono WAV containing a sine wave at the -//! requested frequency and duration. This lets the playback path be -//! exercised against the platform output stack even when capture is -//! unavailable (typical headless CI host). - -use std::path::Path; -use std::time::Duration; - -use hound::{SampleFormat, WavSpec, WavWriter}; - -use crate::capture::AudioError; - -/// Write a sine-wave WAV file. Returns (frames_written, sample_rate). -pub fn synth_sine_wav( - path: &Path, - freq_hz: f32, - duration: Duration, - sample_rate: u32, -) -> Result<(u64, u32), AudioError> { - let spec = WavSpec { - channels: 1, - sample_rate, - bits_per_sample: 16, - sample_format: SampleFormat::Int, - }; - let mut w = WavWriter::create(path, spec)?; - let total_frames = ((duration.as_secs_f64() * sample_rate as f64) as u64).max(1); - let amp = f32::from(i16::MAX) * 0.5; - for n in 0..total_frames { - let t = n as f32 / sample_rate as f32; - let s = (2.0 * std::f32::consts::PI * freq_hz * t).sin() * amp; - w.write_sample(s as i16)?; - } - w.finalize()?; - Ok((total_frames, sample_rate)) -} diff --git a/poc/audio-capture-playback-spike/tests/audio.rs b/poc/audio-capture-playback-spike/tests/audio.rs deleted file mode 100644 index d02856d..0000000 --- a/poc/audio-capture-playback-spike/tests/audio.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Verification tests. These run against the host audio stack and -//! therefore live behind the `audio-hardware` opt-in feature for CI -//! environments without a usable audio device. Without the feature -//! they fall back to a pure-software synth+WAV round-trip that does -//! not touch cpal. - -use std::time::Duration; - -use audio_capture_playback_spike::synth_sine_wav; - -#[test] -fn synth_produces_valid_wav_with_expected_frame_count() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("sine.wav"); - let (frames, sr) = - synth_sine_wav(&path, 440.0, Duration::from_millis(500), 48_000).unwrap(); - assert_eq!(sr, 48_000); - // 0.5 s × 48 kHz = 24 000 frames (mono). - assert!(frames >= 23_900 && frames <= 24_100, "got {frames}"); - - let reader = hound::WavReader::open(&path).unwrap(); - let spec = reader.spec(); - assert_eq!(spec.channels, 1); - assert_eq!(spec.sample_rate, 48_000); - assert_eq!(spec.bits_per_sample, 16); - assert_eq!(reader.len(), frames as u32); -} - -#[test] -fn synth_then_playback_does_not_error_when_output_device_present() { - use audio_capture_playback_spike::AudioPlayback; - use cpal::traits::HostTrait; - - // Skip on hosts with no output device. The PoC's exit criterion - // is satisfied by the verification CLI run captured in - // VERIFICATION.md; this test is a CI sanity check. - if cpal::default_host().default_output_device().is_none() { - eprintln!("SKIP: no default output device"); - return; - } - - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("sine.wav"); - synth_sine_wav(&path, 440.0, Duration::from_millis(120), 48_000).unwrap(); - AudioPlayback::play_wav(&path).expect("playback round-trip should not error"); -} diff --git a/poc/diagnostics-redaction-spike/Cargo.lock b/poc/diagnostics-redaction-spike/Cargo.lock deleted file mode 100644 index 689a635..0000000 --- a/poc/diagnostics-redaction-spike/Cargo.lock +++ /dev/null @@ -1,190 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "diagnostics-redaction-spike" -version = "0.1.0" -dependencies = [ - "indoc", - "once_cell", - "regex", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[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 = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[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 = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[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 = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[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 = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[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 = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/poc/diagnostics-redaction-spike/Cargo.toml b/poc/diagnostics-redaction-spike/Cargo.toml deleted file mode 100644 index b5dbaf9..0000000 --- a/poc/diagnostics-redaction-spike/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "diagnostics-redaction-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: diagnostic redaction policy + redactor covering REDACT-TC-001..010 from the diagnostic redaction audit report." - -# Not product code. See docs/architecture/proof-of-concept-plan.md §4. -# Authority: docs/security/diagnostic-redaction-audit-report.md, -# SRS-093 / SysRS-152/154/155, SDD §5 redaction notes. - -[lib] -name = "diagnostics_redaction_spike" -path = "src/lib.rs" - -[[bin]] -name = "diagnostics-redaction-cli" -path = "src/main.rs" - -[dependencies] -regex = "1" -once_cell = "1" -thiserror = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -[dev-dependencies] -indoc = "2" diff --git a/poc/diagnostics-redaction-spike/README.md b/poc/diagnostics-redaction-spike/README.md deleted file mode 100644 index b046e1d..0000000 --- a/poc/diagnostics-redaction-spike/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Diagnostics Redaction Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `diagnostics-redaction-spike` | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Prove redaction of secrets before logs or diagnostic export | -| Exit criterion | "Password and identity-secret samples are redacted" | -| Authority | `docs/security/diagnostic-redaction-audit-report.md` §2 + §4 + §5; SRS-093, SysRS-152/154/155, SDD §5 | - -## What it proves - -- A typed `RedactionPolicy` carrying: - - regex-driven rules (with optional capture-group narrowing so the - rule can scrub a value while keeping the surrounding context); - - structured-field redaction keyed on case-insensitive name - substrings (`password`, `secret`, `token`, …); - - bundle-level switches (chat / channel tree default to *excluded*). -- A `Redactor` that applies the policy to free text and to a typed - `DiagnosticBundle`. -- A `KnownSecretRegistry` for *literal* scrubbing — the strongest - defence when the host has already loaded the actual secret value - into memory. This is what closes the gap that regexes alone cannot - fully cover (REDACT-TC-002). -- A length cap (`MAX_PROTOCOL_STRING_LEN`) so hostile / oversized - protocol strings cannot grow the diagnostic surface - (REDACT-TC-009). -- UTF-8 preservation for benign multilingual text (REDACT-TC-010 / - ADR-008). - -## Coverage of the audit-report test matrix - -All ten REDACT-TC-001..010 entries are covered (see `VERIFICATION.md`). - -## Layout - -```text -diagnostics-redaction-spike/ - src/ - lib.rs # crate root, re-exports, REDACTION_MARKER - policy.rs # RedactionRule, RedactionPolicy, default policy - redactor.rs # Redactor, KnownSecretRegistry - bundle.rs # DiagnosticBundle DTO + bundle-level redaction - main.rs # diagnostics-redaction-cli driver - tests/ - redaction.rs # 12 tests; REDACT-TC-001..010 + sanity - Cargo.toml -``` - -## Reproduce - -Requires Rust stable (developed against 1.95). - -```bash -cargo test -echo 'INFO password=hunter2 path=/home/alice/x' | cargo run --bin diagnostics-redaction-cli -``` - -## Scope boundaries - -- **Not a `tracing` layer.** Production code in `chanora_diagnostics` - will wire the redactor as a `tracing-subscriber` layer to enforce - redaction at write-time, not via post-processing. The mechanism - here is the same; the integration surface is not. -- **No diagnostic bundle file format.** The spike only redacts the - in-memory struct; the actual export format (zip / json-lines / etc.) - is owned by `chanora_diagnostics`. -- **No threat-model coverage of memory dumps, core dumps, or kernel - logs.** That is `docs/security/threat-model.md` territory. -- **No language-aware redaction.** The Unicode policy is "preserve - multilingual text in non-sensitive fields; do not introspect it." -- **No PII discovery.** This is a deny-list redactor; it does not - attempt to detect previously-unknown secrets by entropy heuristics. - -## Verification log - -See `VERIFICATION.md` in this directory. diff --git a/poc/diagnostics-redaction-spike/VERIFICATION.md b/poc/diagnostics-redaction-spike/VERIFICATION.md deleted file mode 100644 index 9395acd..0000000 --- a/poc/diagnostics-redaction-spike/VERIFICATION.md +++ /dev/null @@ -1,104 +0,0 @@ -# Verification record — `diagnostics-redaction-spike` - -## Result - -PASS. The PoC exit criterion (from -`docs/architecture/proof-of-concept-plan.md` §2: "Password and -identity-secret samples are redacted") is met, with full coverage of -REDACT-TC-001..010 from -`docs/security/diagnostic-redaction-audit-report.md` §4. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | -| Rust toolchain | stable 1.95.0 | -| `regex` | 1 | -| `serde` / `serde_json` | 1 | -| `once_cell` | 1 | - -## Reproduction - -```bash -cargo test -printf 'INFO password=hunter2 path=/home/milkice/x\n' \ - | cargo run --bin diagnostics-redaction-cli -``` - -## Test run - -``` -running 12 tests -test known_secret_is_scrubbed_even_when_no_regex_matches ... ok -test redact_tc_005_channel_name_with_unicode_excluded_by_default ... ok -test redact_tc_006_nickname_with_unicode_preserved_in_safe_field ... ok -test redact_tc_002_identity_secret_in_storage_error ... ok -test redact_tc_010_multilingual_safe_text_preserved ... ok -test redact_tc_009_long_hostile_protocol_string_truncated ... ok -test redact_tc_007_local_file_paths_user_segment_minimized ... ok -test empty_registered_secret_is_ignored ... ok -test redact_tc_004_chat_text_excluded_by_default ... ok -test redact_tc_008_diagnostic_bundle_with_mixed_sensitive_fields ... ok -test redact_tc_001_server_password_in_connection_data ... ok -test redact_tc_003_server_url_with_password_field ... ok - -test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; - 0 filtered out; finished in 0.01s -``` - -## CLI run - -Input: - -```text -INFO connect host=cn.teamspeak.app password=hunter2 nickname=Chanora -DEBUG identity=MG0DAgeAAgEgAiAIXJBlj1hQbaH0Eq0DuLlCmH8bl+veTAO2k9EQjEYSgIgNnImcmKo7ls5mExb6skfK2Twu54aeDr0OP1ITsC50CIA8M5nmDBn -ERROR open /home/milkice/.local/share/chanora/db failed -NOTICE CHANORA_LITERAL_DEMO_SECRET seen on bus -``` - -Output: - -```text -INFO connect host=cn.teamspeak.app password=[REDACTED] nickname=Chanora -DEBUG identity=[REDACTED] -ERROR open /home/[REDACTED]/.local/share/chanora/db failed -NOTICE [REDACTED] seen on bus -``` - -- Password: redacted (rule `password-kv`). -- Base64-ish identity blob: redacted (rule `identity-base64-blob`). -- Linux home-username segment: redacted (rule `linux-home-path`); the - rest of the path is preserved. -- Pre-registered literal secret: redacted via - `KnownSecretRegistry`, demonstrating the SS-AUD-003 defence-in-depth. -- Non-secret tokens (`host=`, `nickname=`, `INFO`, `ERROR`, …) all - flow through unchanged. - -## Audit-matrix coverage - -| Audit ID | Test | Result | -|---|---|---| -| REDACT-TC-001 (server password in connection data) | `redact_tc_001_server_password_in_connection_data` | PASS | -| REDACT-TC-002 (identity secret in storage error) | `redact_tc_002_identity_secret_in_storage_error` | PASS — uses `KnownSecretRegistry`. | -| REDACT-TC-003 (URL with password field) | `redact_tc_003_server_url_with_password_field` | PASS — only the password value is redacted. | -| REDACT-TC-004 (chat text in export) | `redact_tc_004_chat_text_excluded_by_default` | PASS — default policy excludes. | -| REDACT-TC-005 (Unicode channel name) | `redact_tc_005_channel_name_with_unicode_excluded_by_default` | PASS — default policy excludes. | -| REDACT-TC-006 (Unicode nickname) | `redact_tc_006_nickname_with_unicode_preserved_in_safe_field` | PASS — preserved in non-sensitive extras. | -| REDACT-TC-007 (local file path) | `redact_tc_007_local_file_paths_user_segment_minimized` | PASS — username segment redacted on Linux/Windows/macOS path shapes. | -| REDACT-TC-008 (mixed sensitive bundle) | `redact_tc_008_diagnostic_bundle_with_mixed_sensitive_fields` | PASS — whole-bundle JSON scanned for plaintext. | -| REDACT-TC-009 (long hostile protocol string) | `redact_tc_009_long_hostile_protocol_string_truncated` | PASS — `MAX_PROTOCOL_STRING_LEN = 256` cap with truncation marker. | -| REDACT-TC-010 (multilingual safe text) | `redact_tc_010_multilingual_safe_text_preserved` | PASS — Chinese / Japanese / Korean / Latin-diacritic text preserved verbatim. | - -## What this spike does NOT validate - -- Integration as a `tracing` layer (production code will own this). -- Diagnostic export *file format* (zip / json-lines / archive layout). -- Mobile-platform path shapes (`/data/data//` on Android, - iOS app-group containers). -- Memory / core dumps. -- Performance under sustained high-volume logging. -- Adversarial regex evasion beyond the trivial cases exercised. -- Process-level review of the audit report (SS-AUD-007 style sign-off). diff --git a/poc/diagnostics-redaction-spike/src/bundle.rs b/poc/diagnostics-redaction-spike/src/bundle.rs deleted file mode 100644 index 6dab39d..0000000 --- a/poc/diagnostics-redaction-spike/src/bundle.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Structured diagnostic bundle and the redaction pass over it. -//! -//! Mirrors `docs/security/diagnostic-redaction-audit-report.md` §5 -//! "Export Bundle Contents". Fields default to safe values; sensitive -//! categories are excluded by default. - -use serde::{Deserialize, Serialize}; - -use crate::redactor::Redactor; -use crate::REDACTION_MARKER; - -/// Raw bundle assembled by the application before redaction. The -/// caller MUST pass this through `Redactor::redact_bundle` before any -/// disk write or user-visible export. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct DiagnosticBundle { - pub app_version: String, - pub build_number: String, - pub platform_info: String, - pub connection_state: String, - pub server_address: Option, - pub channel_tree: Vec, - pub chat_history: Vec, - pub log_lines: Vec, - pub audio_device_names: Vec, - /// Free-form extras keyed by field name. Sensitive field names - /// (matched against `RedactionPolicy::redact_field_substrings`) - /// are redacted wholesale; everything else flows through - /// `redact_text` for value-level scrubbing. - pub extras: std::collections::BTreeMap, -} - -/// Final, redacted bundle suitable for export. Produced by -/// `Redactor::redact_bundle`. The shape is intentionally the same -/// type so callers can re-serialize as JSON without conversion. -pub type RedactedBundle = DiagnosticBundle; - -impl Redactor { - /// Apply the redaction policy to `bundle`, returning a copy safe - /// for export. - pub fn redact_bundle(&self, bundle: &DiagnosticBundle) -> RedactedBundle { - let policy = self.policy(); - - let chat_history = if policy.include_chat { - bundle - .chat_history - .iter() - .map(|m| self.redact_text(m)) - .collect() - } else { - // Chat is excluded by default (audit report §5 / REDACT-TC-004). - Vec::new() - }; - - let channel_tree = if policy.include_channel_tree { - bundle - .channel_tree - .iter() - .map(|c| self.redact_text(c)) - .collect() - } else { - Vec::new() - }; - - let mut redacted_extras = std::collections::BTreeMap::new(); - for (k, v) in &bundle.extras { - if self.is_sensitive_field(k) { - redacted_extras.insert(k.clone(), REDACTION_MARKER.to_string()); - } else { - redacted_extras.insert(k.clone(), self.redact_text(v)); - } - } - - RedactedBundle { - app_version: bundle.app_version.clone(), - build_number: bundle.build_number.clone(), - // Platform info gets a soft scrub (paths, usernames). - platform_info: self.redact_text(&bundle.platform_info), - connection_state: self.redact_text(&bundle.connection_state), - server_address: bundle.server_address.as_ref().map(|s| self.redact_text(s)), - channel_tree, - chat_history, - log_lines: bundle - .log_lines - .iter() - .map(|l| self.redact_text(l)) - .collect(), - audio_device_names: bundle - .audio_device_names - .iter() - .map(|d| self.redact_text(d)) - .collect(), - extras: redacted_extras, - } - } -} diff --git a/poc/diagnostics-redaction-spike/src/lib.rs b/poc/diagnostics-redaction-spike/src/lib.rs deleted file mode 100644 index 0bf60a3..0000000 --- a/poc/diagnostics-redaction-spike/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Chanora PoC — diagnostics redaction spike. -//! -//! Authority: -//! * `docs/architecture/proof-of-concept-plan.md` §2 — Diagnostics -//! redaction spike. Exit criterion: "Password and identity-secret -//! samples are redacted." -//! * `docs/security/diagnostic-redaction-audit-report.md` — policy -//! in §2, surfaces in §3, test matrix REDACT-TC-001..010 in §4, -//! bundle policy in §5. -//! * SRS-093 / SysRS-152 / SysRS-154 / SysRS-155 — software-level -//! redaction obligations for logs and diagnostic exports. -//! -//! Production code will own this redactor inside the `chanora_diagnostics` -//! crate and wire it as a `tracing` layer. The PoC is the same shape in -//! miniature: a typed policy, a redactor that applies it to free text -//! and to structured diagnostic bundles, plus an explicit list of -//! known-literal secrets the host application can register at runtime. - -pub mod bundle; -pub mod policy; -pub mod redactor; - -pub use bundle::{DiagnosticBundle, RedactedBundle}; -pub use policy::{RedactionPolicy, RedactionRule}; -pub use redactor::{KnownSecretRegistry, Redactor}; - -/// The replacement string used for redacted segments. -/// -/// Chosen distinctly so audit tests can grep for either presence -/// (correct redaction occurred) or absence (no leak). -pub const REDACTION_MARKER: &str = "[REDACTED]"; diff --git a/poc/diagnostics-redaction-spike/src/main.rs b/poc/diagnostics-redaction-spike/src/main.rs deleted file mode 100644 index 9079d4a..0000000 --- a/poc/diagnostics-redaction-spike/src/main.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! CLI driver — accepts mixed sensitive input on stdin, prints the -//! redacted version on stdout, and prints a summary on stderr. - -use std::io::Read; -use std::process::ExitCode; - -use diagnostics_redaction_spike::Redactor; - -fn main() -> ExitCode { - let mut input = String::new(); - if std::io::stdin().read_to_string(&mut input).is_err() { - eprintln!("failed to read stdin"); - return ExitCode::from(1); - } - - let redactor = Redactor::with_default_policy(); - - // The host application would normally register live secrets here - // (e.g. as the secure-storage layer materialises them). - // For the CLI demo, register a fixed plaintext so the audience - // can see literal scrubbing work. - redactor - .known_secrets() - .register("CHANORA_LITERAL_DEMO_SECRET"); - - let out = redactor.redact_text(&input); - println!("{out}"); - eprintln!("--- redaction applied; rules in policy: {} ---", redactor.policy().rules.len()); - ExitCode::SUCCESS -} diff --git a/poc/diagnostics-redaction-spike/src/policy.rs b/poc/diagnostics-redaction-spike/src/policy.rs deleted file mode 100644 index 5d3157d..0000000 --- a/poc/diagnostics-redaction-spike/src/policy.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Redaction policy: the catalogue of patterns and structural rules -//! the redactor applies. - -use once_cell::sync::Lazy; -use regex::Regex; - -/// A single regex-driven rule. -#[derive(Debug, Clone)] -pub struct RedactionRule { - pub id: &'static str, - pub description: &'static str, - pub pattern: Regex, - /// If `Some(n)`, the rule replaces capture-group `n` rather than - /// the whole match. Useful for "ts3server://host?password=XXXX" - /// where the host should be preserved but `XXXX` redacted. - pub redact_group: Option, -} - -impl RedactionRule { - pub fn new( - id: &'static str, - description: &'static str, - pattern: &str, - ) -> Self { - Self { - id, - description, - pattern: Regex::new(pattern).expect("static rule regex must compile"), - redact_group: None, - } - } - - pub fn new_group( - id: &'static str, - description: &'static str, - pattern: &str, - group: usize, - ) -> Self { - Self { - id, - description, - pattern: Regex::new(pattern).expect("static rule regex must compile"), - redact_group: Some(group), - } - } -} - -/// The full redaction policy. -#[derive(Debug, Clone)] -pub struct RedactionPolicy { - pub rules: Vec, - - /// Structured-field redaction: any key whose lower-cased name - /// contains one of these substrings is fully redacted in - /// diagnostic bundles. Mirrors the bundle policy in - /// `diagnostic-redaction-audit-report.md` §5. - pub redact_field_substrings: Vec<&'static str>, - - /// Bundle-level: include chat messages? Audit-report default = No. - pub include_chat: bool, - - /// Bundle-level: include channel tree details? - pub include_channel_tree: bool, -} - -/// Hard upper bound on protocol-string lengths kept in diagnostics. -/// -/// REDACT-TC-009 ("long hostile protocol string"): truncate or safely -/// escape. The PoC truncates with a clear marker. -pub const MAX_PROTOCOL_STRING_LEN: usize = 256; - -impl RedactionPolicy { - /// The default policy used by the PoC. Mirrors the audit-report - /// catalogue. Production code (`chanora_diagnostics`) will own the - /// canonical version of this list. - pub fn default_policy() -> Self { - Self { - rules: DEFAULT_RULES.clone(), - redact_field_substrings: vec![ - "password", - "secret", - "private_key", - "private-key", - "identity_key", - "identity-key", - "token", - "credential", - "passphrase", - ], - include_chat: false, - include_channel_tree: false, - } - } -} - -static DEFAULT_RULES: Lazy> = Lazy::new(|| { - vec![ - // Identity secret as it appears in `tsclientlib` style: - // long base64-ish run preceded by a known marker. - // We err on the side of catching base64 blobs >= 64 chars. - RedactionRule::new( - "identity-base64-blob", - "Long base64-like run; matches identity secrets and tokens.", - r"\b[A-Za-z0-9+/]{64,}={0,2}\b", - ), - // `password = "..."` / `password: "..."` / `password=...` - // Captures the value in group 1 so the *key* name is kept. - // The value excludes separators commonly found in URL query - // strings and config lines (`& , ; " whitespace`). - RedactionRule::new_group( - "password-kv", - "key=value style password assignment.", - r#"(?i)\b(?:password|passwd|pass|pwd)\s*[:=]\s*"?([^"\s,;&]+)"?"#, - 1, - ), - // ts3server://host?password=XXXX&... - RedactionRule::new_group( - "ts3server-url-password", - "Password embedded in a ts3server:// URL query string.", - r"(?i)(?:[?&])password=([^&\s]+)", - 1, - ), - // Generic Authorization: Bearer ... - RedactionRule::new_group( - "authorization-bearer", - "Authorization header bearer token.", - r"(?i)Authorization:\s*Bearer\s+(\S+)", - 1, - ), - // Linux user home: /home//... → minimize the username - // segment. Captures group 1 = username. - RedactionRule::new_group( - "linux-home-path", - "Linux home-directory path; minimizes the username segment.", - r"(/home/)([^/\s]+)", - 2, - ), - // Windows user: C:\Users\\... - RedactionRule::new_group( - "windows-user-path", - "Windows user-profile path; minimizes the username segment.", - r"(?i)([A-Z]:\\Users\\)([^\\\s]+)", - 2, - ), - // macOS user: /Users//... - RedactionRule::new_group( - "macos-user-path", - "macOS user-profile path; minimizes the username segment.", - r"(/Users/)([^/\s]+)", - 2, - ), - ] -}); diff --git a/poc/diagnostics-redaction-spike/src/redactor.rs b/poc/diagnostics-redaction-spike/src/redactor.rs deleted file mode 100644 index e0d0a00..0000000 --- a/poc/diagnostics-redaction-spike/src/redactor.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! The redactor — applies the policy to free text and to structured -//! diagnostic data. - -use std::collections::HashSet; -use std::sync::{Arc, RwLock}; - -use crate::policy::{RedactionPolicy, MAX_PROTOCOL_STRING_LEN}; -use crate::REDACTION_MARKER; - -/// Runtime registry of *literal* known secrets. The host application -/// (e.g. the secure-storage layer) registers a secret here when it -/// loads one into memory, so even if it slips into a log line verbatim -/// it gets scrubbed. -/// -/// This is the strongest defense for SS-AUD-003 and REDACT-TC-002 — -/// regexes can miss; literal substring matches cannot. -#[derive(Debug, Clone, Default)] -pub struct KnownSecretRegistry { - inner: Arc>>, -} - -impl KnownSecretRegistry { - pub fn new() -> Self { - Self::default() - } - - /// Register a literal secret string. Empty strings are ignored - /// (to avoid degenerate `s.replace("", _)` behaviour). - pub fn register(&self, secret: &str) { - if secret.is_empty() { - return; - } - self.inner.write().unwrap().insert(secret.to_string()); - } - - pub fn forget(&self, secret: &str) { - self.inner.write().unwrap().remove(secret); - } - - pub fn snapshot(&self) -> Vec { - self.inner.read().unwrap().iter().cloned().collect() - } -} - -pub struct Redactor { - policy: RedactionPolicy, - known: KnownSecretRegistry, -} - -impl Redactor { - pub fn new(policy: RedactionPolicy, known: KnownSecretRegistry) -> Self { - Self { policy, known } - } - - pub fn with_default_policy() -> Self { - Self::new(RedactionPolicy::default_policy(), KnownSecretRegistry::new()) - } - - pub fn known_secrets(&self) -> &KnownSecretRegistry { - &self.known - } - - pub fn policy(&self) -> &RedactionPolicy { - &self.policy - } - - /// Redact `input` according to the policy + known secret registry. - /// - /// Order: - /// 1. Known literal secrets (defence in depth — never miss a value). - /// 2. Regex rules (with optional capture-group narrowing). - /// 3. Length cap for hostile / oversized strings (REDACT-TC-009). - pub fn redact_text(&self, input: &str) -> String { - // 1. Literal known secrets. - let mut text = input.to_string(); - for known in self.known.snapshot() { - if !known.is_empty() { - text = text.replace(&known, REDACTION_MARKER); - } - } - - // 2. Regex rules. - for rule in &self.policy.rules { - text = match rule.redact_group { - None => rule - .pattern - .replace_all(&text, REDACTION_MARKER) - .into_owned(), - Some(group) => rule - .pattern - .replace_all(&text, |caps: ®ex::Captures<'_>| { - let full = caps.get(0).map(|m| m.as_str()).unwrap_or(""); - match caps.get(group) { - Some(target) => full.replace(target.as_str(), REDACTION_MARKER), - None => full.to_string(), - } - }) - .into_owned(), - }; - } - - // 3. Length cap. - if text.len() > MAX_PROTOCOL_STRING_LEN { - // Find the last char boundary inside the budget so we - // never split a UTF-8 codepoint. - let mut cut = MAX_PROTOCOL_STRING_LEN; - while cut > 0 && !text.is_char_boundary(cut) { - cut -= 1; - } - let mut truncated = text[..cut].to_string(); - truncated.push_str("…[truncated]"); - text = truncated; - } - - text - } - - /// True if the policy classifies `field_name` as a sensitive - /// structured field that must be redacted wholesale. - pub fn is_sensitive_field(&self, field_name: &str) -> bool { - let lower = field_name.to_lowercase(); - self.policy - .redact_field_substrings - .iter() - .any(|needle| lower.contains(needle)) - } -} diff --git a/poc/diagnostics-redaction-spike/tests/redaction.rs b/poc/diagnostics-redaction-spike/tests/redaction.rs deleted file mode 100644 index 2519930..0000000 --- a/poc/diagnostics-redaction-spike/tests/redaction.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! Verification tests mapped to -//! `docs/security/diagnostic-redaction-audit-report.md` §4 -//! (REDACT-TC-001..010). -//! -//! Each test name carries its REDACT-TC- identifier for traceability. - -use diagnostics_redaction_spike::{ - bundle::DiagnosticBundle, Redactor, REDACTION_MARKER, -}; -use indoc::indoc; - -// Unique markers so a leak is unambiguous in test output. -const SERVER_PASSWORD: &str = "REDACT_POC_PW_a9c41f"; -const IDENTITY_SECRET: &str = - "MG0DAgeAAgEgAiAIXJBlj1hQbaH0Eq0DuLlCmH8bl_veTAO2_k9EQjEYSgIgNnImcmKo7ls5mExb6skfK2Twu54aeDr0OP1ITsC50CIA"; -const AUTH_TOKEN: &str = "tok_REDACT_POC_TOKEN_8d11"; - -fn fresh_redactor() -> Redactor { - Redactor::with_default_policy() -} - -// ---------- REDACT-TC-001 ---------- - -#[test] -fn redact_tc_001_server_password_in_connection_data() { - let r = fresh_redactor(); - let input = format!("connect host=cn.teamspeak.app password={SERVER_PASSWORD} nickname=Chanora"); - let out = r.redact_text(&input); - assert!(!out.contains(SERVER_PASSWORD), "password leaked: {out}"); - assert!(out.contains(REDACTION_MARKER), "no marker in {out}"); - // The non-secret context must be preserved. - assert!(out.contains("host=cn.teamspeak.app")); - assert!(out.contains("nickname=Chanora")); -} - -// ---------- REDACT-TC-002 ---------- - -#[test] -fn redact_tc_002_identity_secret_in_storage_error() { - let r = fresh_redactor(); - // Register the identity as a known literal — strongest defense. - r.known_secrets().register(IDENTITY_SECRET); - - let input = format!( - "ERROR: failed to load identity (raw={IDENTITY_SECRET}); falling back to default" - ); - let out = r.redact_text(&input); - assert!(!out.contains(IDENTITY_SECRET), "identity leaked: {out}"); - assert!(out.contains(REDACTION_MARKER)); -} - -// ---------- REDACT-TC-003 ---------- - -#[test] -fn redact_tc_003_server_url_with_password_field() { - let r = fresh_redactor(); - let url = format!("ts3server://cn.teamspeak.app?password={SERVER_PASSWORD}&nickname=Chanora"); - let out = r.redact_text(&url); - assert!(!out.contains(SERVER_PASSWORD)); - // Only the password value should be redacted; host + nickname keep flowing. - assert!(out.contains("ts3server://cn.teamspeak.app")); - assert!(out.contains("nickname=Chanora")); - assert!(out.contains("password=[REDACTED]")); -} - -// ---------- REDACT-TC-004 ---------- - -#[test] -fn redact_tc_004_chat_text_excluded_by_default() { - let r = fresh_redactor(); - let bundle = DiagnosticBundle { - chat_history: vec!["hello!".into(), "private message".into()], - ..Default::default() - }; - let red = r.redact_bundle(&bundle); - assert!(red.chat_history.is_empty(), "chat should be excluded by default"); -} - -// ---------- REDACT-TC-005 ---------- - -#[test] -fn redact_tc_005_channel_name_with_unicode_excluded_by_default() { - let r = fresh_redactor(); - let bundle = DiagnosticBundle { - channel_tree: vec!["樱花庄".into(), "Default Channel".into()], - ..Default::default() - }; - let red = r.redact_bundle(&bundle); - // Default policy: channel tree excluded (audit-report §5 "TBD / Redact/minimize"). - assert!(red.channel_tree.is_empty()); -} - -// ---------- REDACT-TC-006 ---------- - -#[test] -fn redact_tc_006_nickname_with_unicode_preserved_in_safe_field() { - // Nickname is conveyed via a non-sensitive extras key; the redactor - // does NOT mangle Unicode in safe fields (preserves UTF-8 by - // ADR-008, see audit-report row "Multilingual safe diagnostic text"). - let r = fresh_redactor(); - let mut extras = std::collections::BTreeMap::new(); - extras.insert("local_nickname".into(), "クマー".into()); - let bundle = DiagnosticBundle { extras, ..Default::default() }; - let red = r.redact_bundle(&bundle); - assert_eq!(red.extras.get("local_nickname").map(|s| s.as_str()), Some("クマー")); -} - -// ---------- REDACT-TC-007 ---------- - -#[test] -fn redact_tc_007_local_file_paths_user_segment_minimized() { - let r = fresh_redactor(); - let lines = [ - "/home/milkice/chanora/app.log", - "C:\\Users\\Alice\\AppData\\Roaming\\Chanora\\log.txt", - "/Users/bob/Library/Application Support/Chanora/x.db", - ]; - for l in lines { - let out = r.redact_text(l); - assert!(!out.contains("milkice"), "leaked milkice: {out}"); - assert!(!out.contains("Alice"), "leaked Alice: {out}"); - assert!(!out.contains("bob"), "leaked bob: {out}"); - assert!(out.contains(REDACTION_MARKER), "no marker in {out}"); - } -} - -// ---------- REDACT-TC-008 ---------- - -#[test] -fn redact_tc_008_diagnostic_bundle_with_mixed_sensitive_fields() { - let r = fresh_redactor(); - r.known_secrets().register(IDENTITY_SECRET); - - let mut extras = std::collections::BTreeMap::new(); - extras.insert("server_password".into(), SERVER_PASSWORD.into()); - extras.insert("auth_token".into(), AUTH_TOKEN.into()); - extras.insert("app_locale".into(), "en-US".into()); - extras.insert("user_home".into(), "/home/milkice".into()); - - let bundle = DiagnosticBundle { - app_version: "0.0.0-poc".into(), - build_number: "1".into(), - platform_info: "Linux x86_64 path=/home/milkice/.local".into(), - connection_state: format!("disconnected; last_error=password={SERVER_PASSWORD}"), - server_address: Some(format!( - "ts3server://cn.teamspeak.app?password={SERVER_PASSWORD}" - )), - channel_tree: vec!["whatever".into()], - chat_history: vec!["should not appear".into()], - log_lines: vec![ - format!("INFO loaded identity={IDENTITY_SECRET}"), - format!("WARN auth failed Authorization: Bearer {AUTH_TOKEN}"), - ], - audio_device_names: vec!["Built-in Microphone".into()], - extras, - }; - - let red = r.redact_bundle(&bundle); - - // Whole-bundle leak check: no plaintext anywhere. - let blob = serde_json::to_string(&red).unwrap(); - assert!(!blob.contains(SERVER_PASSWORD), "server password leaked: {blob}"); - assert!(!blob.contains(IDENTITY_SECRET), "identity leaked: {blob}"); - assert!(!blob.contains(AUTH_TOKEN), "auth token leaked: {blob}"); - assert!(!blob.contains("milkice"), "username leaked: {blob}"); - assert!(!blob.contains("should not appear"), "chat leaked: {blob}"); - - // Positive checks: structural fields keep flowing. - assert_eq!(red.app_version, "0.0.0-poc"); - assert_eq!(red.build_number, "1"); - assert!(red.audio_device_names.contains(&"Built-in Microphone".to_string())); - assert_eq!(red.extras.get("app_locale").map(|s| s.as_str()), Some("en-US")); - assert_eq!(red.extras.get("server_password").map(|s| s.as_str()), Some(REDACTION_MARKER)); - assert_eq!(red.extras.get("auth_token").map(|s| s.as_str()), Some(REDACTION_MARKER)); -} - -// ---------- REDACT-TC-009 ---------- - -#[test] -fn redact_tc_009_long_hostile_protocol_string_truncated() { - let r = fresh_redactor(); - // A long benign string that does NOT match any redaction rule - // (no base64-style run, no path, no key=value secret) — so the - // length cap is the only thing that can act on it. - let huge: String = "hello world! ".repeat(400); - let out = r.redact_text(&huge); - assert!(out.len() < huge.len(), "expected truncation; got len={} input_len={}", out.len(), huge.len()); - assert!(out.ends_with("…[truncated]"), "missing truncation marker; tail={:?}", &out[out.len().saturating_sub(40)..]); -} - -// ---------- REDACT-TC-010 ---------- - -#[test] -fn redact_tc_010_multilingual_safe_text_preserved() { - let r = fresh_redactor(); - let lines = indoc! {" - Welcome to Vigorous Pro! - 欢迎来到 Vigorous Pro - 日本語チャンネル - 한국어 채널 - Café résumé naïve - "}; - let out = r.redact_text(lines); - for l in [ - "Vigorous Pro", - "欢迎来到", - "日本語チャンネル", - "한국어 채널", - "Café résumé naïve", - ] { - assert!(out.contains(l), "missing line {l:?} in {out}"); - } - // No redaction markers should appear for benign multilingual text. - assert!(!out.contains(REDACTION_MARKER)); -} - -// ---------- Additional sanity ---------- - -#[test] -fn known_secret_is_scrubbed_even_when_no_regex_matches() { - let r = fresh_redactor(); - let weird = "BANANA_PHONE_42"; - r.known_secrets().register(weird); - let out = r.redact_text(&format!("note: secret value is {weird} here")); - assert!(!out.contains(weird)); - assert!(out.contains(REDACTION_MARKER)); -} - -#[test] -fn empty_registered_secret_is_ignored() { - let r = fresh_redactor(); - r.known_secrets().register(""); - let out = r.redact_text("nothing to redact"); - assert_eq!(out, "nothing to redact"); -} diff --git a/poc/flutter_rust_bridge_hello/.gitignore b/poc/flutter_rust_bridge_hello/.gitignore deleted file mode 100644 index 3820a95..0000000 --- a/poc/flutter_rust_bridge_hello/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/poc/flutter_rust_bridge_hello/.metadata b/poc/flutter_rust_bridge_hello/.metadata deleted file mode 100644 index 79dc3aa..0000000 --- a/poc/flutter_rust_bridge_hello/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - platform: android - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - platform: ios - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - platform: linux - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - platform: macos - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - platform: web - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - platform: windows - create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/poc/flutter_rust_bridge_hello/README.md b/poc/flutter_rust_bridge_hello/README.md deleted file mode 100644 index 8f7f7cb..0000000 --- a/poc/flutter_rust_bridge_hello/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# flutter_rust_bridge Hello Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `flutter_rust_bridge_hello` (dir kept underscored for Dart-package naming) | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Prove the command / result / event DTO boundary between Flutter and Rust | -| Exit criterion | "Flutter can call Rust and receive event stream data" | - -> **Naming note.** The PoC plan lists this spike as -> `flutter-rust-bridge-hello`. Dart pubspec package names require -> underscores, so the on-disk directory is `flutter_rust_bridge_hello`. -> This is a naming convention, not a scope change. - -## What it does - -| API | Direction | Demonstrates | -|---|---|---| -| `greet(name)` | Dart → Rust → Dart (sync) | Typed `GreetResult` DTO + typed `GreetError` (Result-style fallible command) | -| `counterStream(count, intervalMs)` | Rust → Dart (async stream) | Typed `CounterTick` event stream with ordered delivery and clean close | - -The `lib/main.dart` demo wires both into a Material 3 surface (button + -text field + live tick list). This UI is illustrative only — the -Chanora Design System belongs to the product Flutter app, not this PoC. - -## Layout - -```text -flutter_rust_bridge_hello/ - rust/ # Rust crate (cdylib + staticlib) - src/ - lib.rs # crate root; declares api + frb_generated - api/ - mod.rs - simple.rs # public API (greet, counter_stream, DTOs) - frb_generated.rs # autogenerated; do not edit - Cargo.toml - lib/ - main.dart # demo UI - src/rust/ # autogenerated Dart bindings; do not edit - test/ - poc_verification_test.dart # headless verification (flutter test) - integration_test/ - simple_test.dart # same checks, runs under integration_test driver - flutter_rust_bridge.yaml - pubspec.yaml - linux/ android/ ios/ macos/ windows/ web/ - rust_builder/ # cargokit build hook (generated) -``` - -## Reproduce - -Requires Flutter stable (developed against 3.41.9 / Dart 3.11.5), -`flutter_rust_bridge_codegen` 2.12.0, Rust stable (developed against 1.95). -Linux desktop toolchain (clang, cmake, ninja, GTK3) for the build path -proven here. Network access required for first build (cargo + pub). - -```bash -# Generate bindings whenever rust/src/api/*.rs changes -flutter_rust_bridge_codegen generate - -# Build for Linux desktop (also builds the cdylib via cargokit) -flutter build linux --debug - -# Headless verification — exercises the same API the demo UI uses. -LD_LIBRARY_PATH="$PWD/build/linux/x64/debug/bundle/lib:$LD_LIBRARY_PATH" \ - flutter test test/poc_verification_test.dart - -# Interactive run (requires a display) -flutter run -d linux -``` - -## Scope boundaries - -- No TeamSpeak / `tsclientlib` integration (covered by - `tsclientlib-connect-spike`). -- No platform secure storage, SQLite, or audio (separate PoCs). -- No real DTO catalogue, no error taxonomy, no event-key conventions — - `chanora_bridge` will own those per SAD §7.2. -- Only the Linux desktop target has been built and verified. Android, - iOS, macOS, Windows are scaffolded by `flutter create` but unverified - here. - -## Verification log - -See `VERIFICATION.md` in this directory. diff --git a/poc/flutter_rust_bridge_hello/VERIFICATION.md b/poc/flutter_rust_bridge_hello/VERIFICATION.md deleted file mode 100644 index c203b0c..0000000 --- a/poc/flutter_rust_bridge_hello/VERIFICATION.md +++ /dev/null @@ -1,68 +0,0 @@ -# Verification record — `flutter_rust_bridge_hello` - -## Result - -PASS. The PoC exit criterion (from -`docs/architecture/proof-of-concept-plan.md` §2: "Flutter can call Rust -and receive event stream data") is met. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | -| Flutter | 3.41.9 stable (Dart 3.11.5) | -| Rust toolchain | stable 1.95.0 | -| `flutter_rust_bridge` (Rust + Dart) | 2.12.0 | -| `flutter_rust_bridge_codegen` | 2.12.0 | -| Target | linux desktop (x86_64-unknown-linux-gnu) | - -## Reproduction - -```bash -# 1. Generate Dart bindings + Rust glue from rust/src/api. -flutter_rust_bridge_codegen generate - -# 2. Build the Linux desktop bundle (this builds the cdylib too, via cargokit). -flutter build linux --debug - -# 3. Run the headless verification test. The cdylib lives in the build bundle, -# so we point dart:ffi at it via LD_LIBRARY_PATH. -LD_LIBRARY_PATH="$PWD/build/linux/x64/debug/bundle/lib:$LD_LIBRARY_PATH" \ - flutter test test/poc_verification_test.dart -``` - -## Observed result - -``` -00:00 +0: loading /home/.../test/poc_verification_test.dart -00:00 +0: (setUpAll) -00:00 +0: Chanora FRB hello PoC greet() returns typed result for valid input -00:00 +1: Chanora FRB hello PoC greet() surfaces typed error for empty input -00:00 +2: Chanora FRB hello PoC counterStream() delivers the expected event sequence -00:00 +3: (tearDownAll) -00:00 +3: All tests passed! -``` - -## What the verification proves - -| Exit-criterion sub-claim | Evidence | -|---|---| -| Dart can call Rust | `greet(name: 'Chanora')` returns a `GreetResult` whose `message` field originated in Rust. | -| Typed result DTO crosses the boundary | `GreetResult.elapsedMicros` is a typed `BigInt` (Rust `u64`) deserialised correctly on the Dart side. | -| Typed error DTO crosses the boundary | `greet(name: ' ')` throws a `GreetError` instance on the Dart side (`isA()`); the Rust `Err(...)` arm is reached and reified as a Dart exception. | -| Dart can receive a Rust event stream | `counterStream(count: 4, intervalMs: 50)` yields exactly 4 ordered `CounterTick` events with `seq = 0..3` and matching `note` payloads. | -| Streams close cleanly | The `await for` loop terminates on its own without timeout. | - -## Scope honestly NOT validated by this spike - -- Performance characteristics of the bridge under load. -- Android, iOS, macOS, Windows targets. -- Lifecycle behaviour across Flutter hot-restart or Dart isolate boundaries. -- Cancellation semantics (Dart listener disconnect while Rust still produces). -- Behaviour with large/complex DTO graphs. -- Backpressure on the event stream. -- The full Chanora bridge DTO catalogue (owned by `chanora_bridge`). -- The "schema-controlled" half of DEC-014 — this PoC uses macro-driven - generation only. diff --git a/poc/flutter_rust_bridge_hello/analysis_options.yaml b/poc/flutter_rust_bridge_hello/analysis_options.yaml deleted file mode 100644 index 0d29021..0000000 --- a/poc/flutter_rust_bridge_hello/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/android/.gitignore b/poc/flutter_rust_bridge_hello/android/.gitignore deleted file mode 100644 index be3943c..0000000 --- a/poc/flutter_rust_bridge_hello/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/poc/flutter_rust_bridge_hello/android/app/build.gradle.kts b/poc/flutter_rust_bridge_hello/android/app/build.gradle.kts deleted file mode 100644 index fd3789e..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -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_rust_bridge_hello" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.flutter_rust_bridge_hello" - // 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/poc/flutter_rust_bridge_hello/android/app/src/debug/AndroidManifest.xml b/poc/flutter_rust_bridge_hello/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/AndroidManifest.xml b/poc/flutter_rust_bridge_hello/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index e1ab0c6..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/kotlin/com/example/flutter_rust_bridge_hello/MainActivity.kt b/poc/flutter_rust_bridge_hello/android/app/src/main/kotlin/com/example/flutter_rust_bridge_hello/MainActivity.kt deleted file mode 100644 index a6b3695..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/main/kotlin/com/example/flutter_rust_bridge_hello/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.flutter_rust_bridge_hello - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable-v21/launch_background.xml b/poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable/launch_background.xml b/poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/poc/flutter_rust_bridge_hello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/values-night/styles.xml b/poc/flutter_rust_bridge_hello/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/android/app/src/main/res/values/styles.xml b/poc/flutter_rust_bridge_hello/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef88..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/android/app/src/profile/AndroidManifest.xml b/poc/flutter_rust_bridge_hello/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/poc/flutter_rust_bridge_hello/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/poc/flutter_rust_bridge_hello/android/build.gradle.kts b/poc/flutter_rust_bridge_hello/android/build.gradle.kts deleted file mode 100644 index dbee657..0000000 --- a/poc/flutter_rust_bridge_hello/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -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/poc/flutter_rust_bridge_hello/android/gradle.properties b/poc/flutter_rust_bridge_hello/android/gradle.properties deleted file mode 100644 index fbee1d8..0000000 --- a/poc/flutter_rust_bridge_hello/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true diff --git a/poc/flutter_rust_bridge_hello/android/gradle/wrapper/gradle-wrapper.properties b/poc/flutter_rust_bridge_hello/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e4ef43f..0000000 --- a/poc/flutter_rust_bridge_hello/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/poc/flutter_rust_bridge_hello/android/settings.gradle.kts b/poc/flutter_rust_bridge_hello/android/settings.gradle.kts deleted file mode 100644 index ca7fe06..0000000 --- a/poc/flutter_rust_bridge_hello/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -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.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false -} - -include(":app") diff --git a/poc/flutter_rust_bridge_hello/flutter_rust_bridge.yaml b/poc/flutter_rust_bridge_hello/flutter_rust_bridge.yaml deleted file mode 100644 index e15ed91..0000000 --- a/poc/flutter_rust_bridge_hello/flutter_rust_bridge.yaml +++ /dev/null @@ -1,3 +0,0 @@ -rust_input: crate::api -rust_root: rust/ -dart_output: lib/src/rust \ No newline at end of file diff --git a/poc/flutter_rust_bridge_hello/integration_test/simple_test.dart b/poc/flutter_rust_bridge_hello/integration_test/simple_test.dart deleted file mode 100644 index dfee973..0000000 --- a/poc/flutter_rust_bridge_hello/integration_test/simple_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -// Chanora PoC verification — flutter_rust_bridge_hello. -// -// Verifies the PoC exit criterion from -// `docs/architecture/proof-of-concept-plan.md` §2: -// "Flutter can call Rust and receive event stream data." -// -// This is a Flutter integration test (runs on a real Flutter engine on the -// Linux desktop target). It exercises the FRB-generated Dart API directly -// rather than driving the demo UI — that is sufficient evidence for the -// PoC exit criterion and is robust against future UI changes. - -import 'package:flutter_rust_bridge_hello/src/rust/api/simple.dart'; -import 'package:flutter_rust_bridge_hello/src/rust/frb_generated.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - setUpAll(() async => await RustLib.init()); - - group('Chanora FRB hello PoC', () { - test('greet() returns typed result for valid input', () { - final r = greet(name: 'Chanora'); - expect(r.message, contains('Hello, Chanora!')); - // Sanity-check the typed numeric field round-trips. - expect(r.elapsedMicros, isA()); - expect(r.elapsedMicros.toInt(), greaterThanOrEqualTo(0)); - }); - - test('greet() surfaces typed error for empty input', () { - // FRB maps the Rust `Err(GreetError { .. })` to a Dart exception. - expect(() => greet(name: ' '), throwsA(isA())); - }); - - test('counterStream() delivers the expected event sequence', () async { - const count = 4; - final events = []; - await for (final tick in counterStream(count: count, intervalMs: 50)) { - events.add(tick); - } - expect(events, hasLength(count)); - for (var i = 0; i < count; i++) { - expect(events[i].seq.toInt(), i); - expect(events[i].note, 'tick $i/$count'); - } - }); - }); -} diff --git a/poc/flutter_rust_bridge_hello/ios/.gitignore b/poc/flutter_rust_bridge_hello/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/poc/flutter_rust_bridge_hello/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/poc/flutter_rust_bridge_hello/ios/Flutter/AppFrameworkInfo.plist b/poc/flutter_rust_bridge_hello/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 391a902..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - - diff --git a/poc/flutter_rust_bridge_hello/ios/Flutter/Debug.xcconfig b/poc/flutter_rust_bridge_hello/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/poc/flutter_rust_bridge_hello/ios/Flutter/Release.xcconfig b/poc/flutter_rust_bridge_hello/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.pbxproj b/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index cc73afb..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,620 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 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 */; }; - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; - 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 */ - 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 = ""; }; - 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 = ""; }; - 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 = ""; }; - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - 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 */, - ); - 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 */, - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - 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 = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - 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"; - }; - 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"; - }; -/* 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 */, - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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 = 13.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)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello; - 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; - 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.flutterRustBridgeHello.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; - 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.flutterRustBridgeHello.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; - 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.flutterRustBridgeHello.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 = 13.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 = 13.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)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello; - 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)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello; - 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/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e3773d4..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/contents.xcworkspacedata b/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/AppDelegate.swift b/poc/flutter_rust_bridge_hello/ios/Runner/AppDelegate.swift deleted file mode 100644 index c30b367..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { - GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) - } -} diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e458972bab9d994556c8305db4c827017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933e1120817fe9182483a228007b18ab6ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b0099ca80c806f8fe495613e8d6c69460d76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945a01f64a61e2235dbe3f45b08f7729182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a9bc882b461c96aadf492d1729e49e725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec303439225b78712f49115768196d8d76f6790..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea27c705180eb716271f41b582e76dcbd90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 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 diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12aa4d28f374bb26596605a46dcbb3e7c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/LaunchScreen.storyboard b/poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/Main.storyboard b/poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Info.plist b/poc/flutter_rust_bridge_hello/ios/Runner/Info.plist deleted file mode 100644 index e09a5e9..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Info.plist +++ /dev/null @@ -1,70 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Flutter Rust Bridge Hello - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - flutter_rust_bridge_hello - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - flutter - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/Runner-Bridging-Header.h b/poc/flutter_rust_bridge_hello/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/poc/flutter_rust_bridge_hello/ios/Runner/SceneDelegate.swift b/poc/flutter_rust_bridge_hello/ios/Runner/SceneDelegate.swift deleted file mode 100644 index b9ce8ea..0000000 --- a/poc/flutter_rust_bridge_hello/ios/Runner/SceneDelegate.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Flutter -import UIKit - -class SceneDelegate: FlutterSceneDelegate { - -} diff --git a/poc/flutter_rust_bridge_hello/ios/RunnerTests/RunnerTests.swift b/poc/flutter_rust_bridge_hello/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/poc/flutter_rust_bridge_hello/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -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/poc/flutter_rust_bridge_hello/lib/main.dart b/poc/flutter_rust_bridge_hello/lib/main.dart deleted file mode 100644 index 071e081..0000000 --- a/poc/flutter_rust_bridge_hello/lib/main.dart +++ /dev/null @@ -1,175 +0,0 @@ -// Chanora PoC — Flutter side of the flutter_rust_bridge hello spike. -// -// Demonstrates both halves of the PoC exit criterion -// (docs/architecture/proof-of-concept-plan.md §2): -// 1) Flutter calls a Rust command and renders the typed result. -// 2) Flutter subscribes to a Rust event stream and renders ticks live. -// -// This is PoC code, not product UI. The Chanora Design System (Material 3) -// lives in docs/ui-ux/ and is owned by the product Flutter app, not here. - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge_hello/src/rust/api/simple.dart'; -import 'package:flutter_rust_bridge_hello/src/rust/frb_generated.dart'; - -Future main() async { - await RustLib.init(); - runApp(const PocApp()); -} - -class PocApp extends StatelessWidget { - const PocApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Chanora PoC — FRB hello', - theme: ThemeData( - useMaterial3: true, - colorSchemeSeed: const Color(0xFF3F51B5), - ), - home: const PocHome(), - ); - } -} - -class PocHome extends StatefulWidget { - const PocHome({super.key}); - - @override - State createState() => _PocHomeState(); -} - -class _PocHomeState extends State { - final _nameCtl = TextEditingController(text: 'Chanora'); - String _greetOutput = '(no call yet)'; - bool _greetOk = true; - - StreamSubscription? _streamSub; - final List _ticks = []; - bool _streamRunning = false; - - @override - void dispose() { - _streamSub?.cancel(); - _nameCtl.dispose(); - super.dispose(); - } - - void _callGreet() { - try { - final r = greet(name: _nameCtl.text); - setState(() { - _greetOk = true; - _greetOutput = '${r.message}\n(elapsed: ${r.elapsedMicros} µs)'; - }); - } catch (e) { - setState(() { - _greetOk = false; - _greetOutput = 'error: $e'; - }); - } - } - - void _startStream() { - _streamSub?.cancel(); - setState(() { - _ticks.clear(); - _streamRunning = true; - }); - final stream = counterStream(count: 5, intervalMs: 400); - _streamSub = stream.listen( - (tick) => setState(() => _ticks.add(tick)), - onDone: () => setState(() => _streamRunning = false), - onError: (e) => setState(() { - _streamRunning = false; - _ticks.add(CounterTick(seq: BigInt.from(-1), note: 'error: $e')); - }), - ); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Scaffold( - appBar: AppBar( - title: const Text('Chanora PoC — flutter_rust_bridge hello'), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: ListView( - children: [ - Text('1. Command (Dart → Rust → typed Result)', style: theme.textTheme.titleMedium), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: TextField( - controller: _nameCtl, - decoration: const InputDecoration( - labelText: 'name', - border: OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 12), - FilledButton(onPressed: _callGreet, child: const Text('greet')), - ], - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: _greetOk - ? theme.colorScheme.surfaceContainerHighest - : theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(_greetOutput, style: const TextStyle(fontFamily: 'monospace')), - ), - const Divider(height: 32), - Text('2. Event stream (Rust → Dart Stream)', style: theme.textTheme.titleMedium), - const SizedBox(height: 8), - Row( - children: [ - FilledButton( - onPressed: _streamRunning ? null : _startStream, - child: Text(_streamRunning ? 'streaming…' : 'start counter_stream'), - ), - const SizedBox(width: 12), - Text('${_ticks.length} ticks received'), - ], - ), - const SizedBox(height: 8), - Container( - constraints: const BoxConstraints(minHeight: 120), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - ), - child: _ticks.isEmpty - ? const Text('(no ticks yet)') - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (final t in _ticks) - Text('seq=${t.seq} ${t.note}', - style: const TextStyle(fontFamily: 'monospace')), - ], - ), - ), - const SizedBox(height: 24), - Text( - 'PoC exit criterion (docs/architecture/proof-of-concept-plan.md §2):\n' - '"Flutter can call Rust and receive event stream data."', - style: theme.textTheme.bodySmall, - ), - ], - ), - ), - ); - } -} diff --git a/poc/flutter_rust_bridge_hello/lib/src/rust/api/simple.dart b/poc/flutter_rust_bridge_hello/lib/src/rust/api/simple.dart deleted file mode 100644 index 577ae1e..0000000 --- a/poc/flutter_rust_bridge_hello/lib/src/rust/api/simple.dart +++ /dev/null @@ -1,82 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. - -// 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'; - -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt` - -/// Synchronous fallible command. Proves Dart → Rust call with a typed -/// `Result` DTO crossing the boundary. -GreetResult greet({required String name}) => - RustLib.instance.api.crateApiSimpleGreet(name: name); - -/// Rust → Dart event stream. -/// `StreamSink` is provided in scope by `flutter_rust_bridge` glue; -/// the codegen wires it to a Dart `Stream`. -Stream counterStream({ - required int count, - required int intervalMs, -}) => RustLib.instance.api.crateApiSimpleCounterStream( - count: count, - intervalMs: intervalMs, -); - -class CounterTick { - final BigInt seq; - final String note; - - const CounterTick({required this.seq, required this.note}); - - @override - int get hashCode => seq.hashCode ^ note.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CounterTick && - runtimeType == other.runtimeType && - seq == other.seq && - note == other.note; -} - -/// Typed error DTO surfaced as a Dart exception by FRB. -class GreetError implements FrbException { - final GreetErrorKind kind; - final String detail; - - const GreetError({required this.kind, required this.detail}); - - @override - int get hashCode => kind.hashCode ^ detail.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is GreetError && - runtimeType == other.runtimeType && - kind == other.kind && - detail == other.detail; -} - -enum GreetErrorKind { emptyName } - -class GreetResult { - final String message; - final BigInt elapsedMicros; - - const GreetResult({required this.message, required this.elapsedMicros}); - - @override - int get hashCode => message.hashCode ^ elapsedMicros.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is GreetResult && - runtimeType == other.runtimeType && - message == other.message && - elapsedMicros == other.elapsedMicros; -} diff --git a/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.dart b/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.dart deleted file mode 100644 index 129865e..0000000 --- a/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.dart +++ /dev/null @@ -1,488 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. - -// 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/simple.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'frb_generated.io.dart' - if (dart.library.js_interop) 'frb_generated.web.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.crateApiSimpleInitApp(); - } - - @override - ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => - kDefaultExternalLibraryLoaderConfig; - - @override - String get codegenVersion => '2.12.0'; - - @override - int get rustContentHash => -1685941124; - - static const kDefaultExternalLibraryLoaderConfig = - ExternalLibraryLoaderConfig( - stem: 'rust_lib_flutter_rust_bridge_hello', - ioDirectory: 'rust/target/release/', - webPrefix: 'pkg/', - wasmBindgenName: 'wasm_bindgen', - ); -} - -abstract class RustLibApi extends BaseApi { - Stream crateApiSimpleCounterStream({ - required int count, - required int intervalMs, - }); - - GreetResult crateApiSimpleGreet({required String name}); - - Future crateApiSimpleInitApp(); -} - -class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { - RustLibApiImpl({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @override - Stream crateApiSimpleCounterStream({ - required int count, - required int intervalMs, - }) { - final sink = RustStreamSink(); - unawaited( - handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_StreamSink_counter_tick_Sse(sink, serializer); - sse_encode_u_32(count, serializer); - sse_encode_u_32(intervalMs, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 1, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiSimpleCounterStreamConstMeta, - argValues: [sink, count, intervalMs], - apiImpl: this, - ), - ), - ); - return sink.stream; - } - - TaskConstMeta get kCrateApiSimpleCounterStreamConstMeta => - const TaskConstMeta( - debugName: "counter_stream", - argNames: ["sink", "count", "intervalMs"], - ); - - @override - GreetResult crateApiSimpleGreet({required String name}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(name, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_greet_result, - decodeErrorData: sse_decode_greet_error, - ), - constMeta: kCrateApiSimpleGreetConstMeta, - argValues: [name], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiSimpleGreetConstMeta => - const TaskConstMeta(debugName: "greet", argNames: ["name"]); - - @override - Future crateApiSimpleInitApp() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 3, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiSimpleInitAppConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiSimpleInitAppConstMeta => - const TaskConstMeta(debugName: "init_app", argNames: []); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return AnyhowException(raw as String); - } - - @protected - RustStreamSink dco_decode_StreamSink_counter_tick_Sse( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - throw UnimplementedError(); - } - - @protected - String dco_decode_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as String; - } - - @protected - CounterTick dco_decode_counter_tick(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 CounterTick( - seq: dco_decode_u_64(arr[0]), - note: dco_decode_String(arr[1]), - ); - } - - @protected - GreetError dco_decode_greet_error(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 GreetError( - kind: dco_decode_greet_error_kind(arr[0]), - detail: dco_decode_String(arr[1]), - ); - } - - @protected - GreetErrorKind dco_decode_greet_error_kind(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return GreetErrorKind.values[raw as int]; - } - - @protected - GreetResult dco_decode_greet_result(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 GreetResult( - message: dco_decode_String(arr[0]), - elapsedMicros: dco_decode_u_64(arr[1]), - ); - } - - @protected - int dco_decode_i_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @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 - 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 - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_String(deserializer); - return AnyhowException(inner); - } - - @protected - RustStreamSink sse_decode_StreamSink_counter_tick_Sse( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - throw UnimplementedError('Unreachable ()'); - } - - @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 - CounterTick sse_decode_counter_tick(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_seq = sse_decode_u_64(deserializer); - var var_note = sse_decode_String(deserializer); - return CounterTick(seq: var_seq, note: var_note); - } - - @protected - GreetError sse_decode_greet_error(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_kind = sse_decode_greet_error_kind(deserializer); - var var_detail = sse_decode_String(deserializer); - return GreetError(kind: var_kind, detail: var_detail); - } - - @protected - GreetErrorKind sse_decode_greet_error_kind(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_i_32(deserializer); - return GreetErrorKind.values[inner]; - } - - @protected - GreetResult sse_decode_greet_result(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_message = sse_decode_String(deserializer); - var var_elapsedMicros = sse_decode_u_64(deserializer); - return GreetResult(message: var_message, elapsedMicros: var_elapsedMicros); - } - - @protected - int sse_decode_i_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getInt32(); - } - - @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 - 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 - 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_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.message, serializer); - } - - @protected - void sse_encode_StreamSink_counter_tick_Sse( - RustStreamSink self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String( - self.setupAndSerialize( - codec: SseCodec( - decodeSuccessData: sse_decode_counter_tick, - decodeErrorData: sse_decode_AnyhowException, - ), - ), - 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_counter_tick(CounterTick self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_64(self.seq, serializer); - sse_encode_String(self.note, serializer); - } - - @protected - void sse_encode_greet_error(GreetError self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_greet_error_kind(self.kind, serializer); - sse_encode_String(self.detail, serializer); - } - - @protected - void sse_encode_greet_error_kind( - GreetErrorKind self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.index, serializer); - } - - @protected - void sse_encode_greet_result(GreetResult self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.message, serializer); - sse_encode_u_64(self.elapsedMicros, serializer); - } - - @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_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_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_bool(bool self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self ? 1 : 0); - } -} diff --git a/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.io.dart b/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.io.dart deleted file mode 100644 index f68dab9..0000000 --- a/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.io.dart +++ /dev/null @@ -1,174 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. - -// 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/simple.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi' as ffi; -import 'frb_generated.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, - }); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - - @protected - RustStreamSink dco_decode_StreamSink_counter_tick_Sse( - dynamic raw, - ); - - @protected - String dco_decode_String(dynamic raw); - - @protected - CounterTick dco_decode_counter_tick(dynamic raw); - - @protected - GreetError dco_decode_greet_error(dynamic raw); - - @protected - GreetErrorKind dco_decode_greet_error_kind(dynamic raw); - - @protected - GreetResult dco_decode_greet_result(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(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 - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - - @protected - RustStreamSink sse_decode_StreamSink_counter_tick_Sse( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - CounterTick sse_decode_counter_tick(SseDeserializer deserializer); - - @protected - GreetError sse_decode_greet_error(SseDeserializer deserializer); - - @protected - GreetErrorKind sse_decode_greet_error_kind(SseDeserializer deserializer); - - @protected - GreetResult sse_decode_greet_result(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(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 - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ); - - @protected - void sse_encode_StreamSink_counter_tick_Sse( - RustStreamSink self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_counter_tick(CounterTick self, SseSerializer serializer); - - @protected - void sse_encode_greet_error(GreetError self, SseSerializer serializer); - - @protected - void sse_encode_greet_error_kind( - GreetErrorKind self, - SseSerializer serializer, - ); - - @protected - void sse_encode_greet_result(GreetResult self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List 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_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; -} diff --git a/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.web.dart b/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.web.dart deleted file mode 100644 index b6158c8..0000000 --- a/poc/flutter_rust_bridge_hello/lib/src/rust/frb_generated.web.dart +++ /dev/null @@ -1,174 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. - -// 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/simple.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.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, - }); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - - @protected - RustStreamSink dco_decode_StreamSink_counter_tick_Sse( - dynamic raw, - ); - - @protected - String dco_decode_String(dynamic raw); - - @protected - CounterTick dco_decode_counter_tick(dynamic raw); - - @protected - GreetError dco_decode_greet_error(dynamic raw); - - @protected - GreetErrorKind dco_decode_greet_error_kind(dynamic raw); - - @protected - GreetResult dco_decode_greet_result(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(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 - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - - @protected - RustStreamSink sse_decode_StreamSink_counter_tick_Sse( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - CounterTick sse_decode_counter_tick(SseDeserializer deserializer); - - @protected - GreetError sse_decode_greet_error(SseDeserializer deserializer); - - @protected - GreetErrorKind sse_decode_greet_error_kind(SseDeserializer deserializer); - - @protected - GreetResult sse_decode_greet_result(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(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 - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ); - - @protected - void sse_encode_StreamSink_counter_tick_Sse( - RustStreamSink self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_counter_tick(CounterTick self, SseSerializer serializer); - - @protected - void sse_encode_greet_error(GreetError self, SseSerializer serializer); - - @protected - void sse_encode_greet_error_kind( - GreetErrorKind self, - SseSerializer serializer, - ); - - @protected - void sse_encode_greet_result(GreetResult self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List 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_bool(bool self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - RustLibWire.fromExternalLibrary(ExternalLibrary lib); -} - -@JS('wasm_bindgen') -external RustLibWasmModule get wasmModule; - -@JS() -@anonymous -extension type RustLibWasmModule._(JSObject _) implements JSObject {} diff --git a/poc/flutter_rust_bridge_hello/linux/.gitignore b/poc/flutter_rust_bridge_hello/linux/.gitignore deleted file mode 100644 index d3896c9..0000000 --- a/poc/flutter_rust_bridge_hello/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/poc/flutter_rust_bridge_hello/linux/CMakeLists.txt b/poc/flutter_rust_bridge_hello/linux/CMakeLists.txt deleted file mode 100644 index 931a794..0000000 --- a/poc/flutter_rust_bridge_hello/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# 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_rust_bridge_hello") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.flutter_rust_bridge_hello") - -# 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/poc/flutter_rust_bridge_hello/linux/flutter/CMakeLists.txt b/poc/flutter_rust_bridge_hello/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd016..0000000 --- a/poc/flutter_rust_bridge_hello/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/linux/flutter/generated_plugins.cmake b/poc/flutter_rust_bridge_hello/linux/flutter/generated_plugins.cmake deleted file mode 100644 index f1c1d3e..0000000 --- a/poc/flutter_rust_bridge_hello/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - rust_lib_flutter_rust_bridge_hello -) - -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/poc/flutter_rust_bridge_hello/linux/runner/CMakeLists.txt b/poc/flutter_rust_bridge_hello/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc..0000000 --- a/poc/flutter_rust_bridge_hello/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -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/poc/flutter_rust_bridge_hello/linux/runner/main.cc b/poc/flutter_rust_bridge_hello/linux/runner/main.cc deleted file mode 100644 index e7c5c54..0000000 --- a/poc/flutter_rust_bridge_hello/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/linux/runner/my_application.cc b/poc/flutter_rust_bridge_hello/linux/runner/my_application.cc deleted file mode 100644 index f17662f..0000000 --- a/poc/flutter_rust_bridge_hello/linux/runner/my_application.cc +++ /dev/null @@ -1,148 +0,0 @@ -#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) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// 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_rust_bridge_hello"); - 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_rust_bridge_hello"); - } - - gtk_window_set_default_size(window, 1280, 720); - - 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); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(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/poc/flutter_rust_bridge_hello/linux/runner/my_application.h b/poc/flutter_rust_bridge_hello/linux/runner/my_application.h deleted file mode 100644 index db16367..0000000 --- a/poc/flutter_rust_bridge_hello/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/macos/.gitignore b/poc/flutter_rust_bridge_hello/macos/.gitignore deleted file mode 100644 index 746adbb..0000000 --- a/poc/flutter_rust_bridge_hello/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Debug.xcconfig b/poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Release.xcconfig b/poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.pbxproj b/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 4333626..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*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 */ - 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 */ - 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_rust_bridge_hello.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_rust_bridge_hello.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 = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - 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 */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* flutter_rust_bridge_hello.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 = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 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 = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* flutter_rust_bridge_hello.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"; - }; -/* 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; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_rust_bridge_hello.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_rust_bridge_hello"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_rust_bridge_hello.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_rust_bridge_hello"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_rust_bridge_hello.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_rust_bridge_hello"; - }; - 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.15; - 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.15; - 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.15; - 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/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 9e9cc42..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/contents.xcworkspacedata b/poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/AppDelegate.swift b/poc/flutter_rust_bridge_hello/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -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/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "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/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a33e198f5747104729e1fcef999772a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55c6dabc3aac36f33d859266c18fa0d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5680 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 diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^;j87-Auq zoUlN^K{r-Q+XN;zI ze|?*NFmgt#V#GwrSWaz^2G&@SBmck6ZcIFMww~vE<1E?M2#KUn1CzsB6D2+0SuRV@ zV2kK5HvIGB{HX-hQzs0*AB%5$9RJ@a;)Ahq#p$GSP91^&hi#6sg*;a~dt}4AclK>h z_3MoPRQ{i;==;*1S-mY<(JFzhAxMI&<61&m$J0NDHdJ3tYx~j0%M-uN6Zl8~_0DOkGXc0001@sz3l12C6Xg{AT~( zm6w64BA|AX`Ve)YY-glyudNN>MAfkXz-T7`_`fEolM;0T0BA)(02-OaW z0*cW7Z~ec94o8&g0D$N>b!COu{=m}^%oXZ4?T8ZyPZuGGBPBA7pbQMoV5HYhiT?%! zcae~`(QAN4&}-=#2f5fkn!SWGWmSeCISBcS=1-U|MEoKq=k?_x3apK>9((R zuu$9X?^8?@(a{qMS%J8SJPq))v}Q-ZyDm6Gbie0m92=`YlwnQPQP1kGSm(N2UJ3P6 z^{p-u)SSCTW~c1rw;cM)-uL2{->wCn2{#%;AtCQ!m%AakVs1K#v@(*-6QavyY&v&*wO_rCJXJuq$c$7ZjsW+pJo-$L^@!7X04CvaOpPyfw|FKvu;e(&Iw>Tbg zL}#8e^?X%TReXTt>gsBByt0kSU20oQx*~P=4`&tcZ7N6t-6LiK{LxX*p6}9c<0Pu^ zLx1w_P4P2V>bX=`F%v$#{sUDdF|;rbI{p#ZW`00Bgh(eB(nOIhy8W9T>3aQ=k8Z9% zB+TusFABF~J?N~fAd}1Rme=@4+1=M{^P`~se7}e3;mY0!%#MJf!XSrUC{0uZqMAd7%q zQY#$A>q}noIB4g54Ue)x>ofVm3DKBbUmS4Z-bm7KdKsUixva)1*&z5rgAG2gxG+_x zqT-KNY4g7eM!?>==;uD9Y4iI(Hu$pl8!LrK_Zb}5nv(XKW{9R144E!cFf36p{i|8pRL~p`_^iNo z{mf7y`#hejw#^#7oKPlN_Td{psNpNnM?{7{R-ICBtYxk>?3}OTH_8WkfaTLw)ZRTfxjW+0>gMe zpKg~`Bc$Y>^VX;ks^J0oKhB#6Ukt{oQhN+o2FKGZx}~j`cQB%vVsMFnm~R_1Y&Ml? zwFfb~d|dW~UktY@?zkau>Owe zRroi(<)c4Ux&wJfY=3I=vg)uh;sL(IYY9r$WK1$F;jYqq1>xT{LCkIMb3t2jN8d`9 z=4(v-z7vHucc_fjkpS}mGC{ND+J-hc_0Ix4kT^~{-2n|;Jmn|Xf9wGudDk7bi*?^+ z7fku8z*mbkGm&xf&lmu#=b5mp{X(AwtLTf!N`7FmOmX=4xwbD=fEo8CaB1d1=$|)+ z+Dlf^GzGOdlqTO8EwO?8;r+b;gkaF^$;+#~2_YYVH!hD6r;PaWdm#V=BJ1gH9ZK_9 zrAiIC-)z)hRq6i5+$JVmR!m4P>3yJ%lH)O&wtCyum3A*})*fHODD2nq!1@M>t@Za+ zH6{(Vf>_7!I-APmpsGLYpl7jww@s5hHOj5LCQXh)YAp+y{gG(0UMm(Ur z3o3n36oFwCkn+H*GZ-c6$Y!5r3z*@z0`NrB2C^q#LkOuooUM8Oek2KBk}o1PU8&2L z4iNkb5CqJWs58aR394iCU^ImDqV;q_Pp?pl=RB2372(Io^GA^+oKguO1(x$0<7w3z z)j{vnqEB679Rz4i4t;8|&Zg77UrklxY9@GDq(ZphH6=sW`;@uIt5B?7Oi?A0-BL}(#1&R;>2aFdq+E{jsvpNHjLx2t{@g1}c~DQcPNmVmy| zNMO@ewD^+T!|!DCOf}s9dLJU}(KZy@Jc&2Nq3^;vHTs}Hgcp`cw&gd7#N}nAFe3cM1TF%vKbKSffd&~FG9y$gLyr{#to)nxz5cCASEzQ}gz8O)phtHuKOW6p z@EQF(R>j%~P63Wfosrz8p(F=D|Mff~chUGn(<=CQbSiZ{t!e zeDU-pPsLgtc#d`3PYr$i*AaT!zF#23htIG&?QfcUk+@k$LZI}v+js|yuGmE!PvAV3 ztzh90rK-0L6P}s?1QH`Ot@ilbgMBzWIs zIs6K<_NL$O4lwR%zH4oJ+}JJp-bL6~%k&p)NGDMNZX7)0kni&%^sH|T?A)`z z=adV?!qnWx^B$|LD3BaA(G=ePL1+}8iu^SnnD;VE1@VLHMVdSN9$d)R(Wk{JEOp(P zm3LtAL$b^*JsQ0W&eLaoYag~=fRRdI>#FaELCO7L>zXe6w*nxN$Iy*Q*ftHUX0+N- zU>{D_;RRVPbQ?U+$^%{lhOMKyE5>$?U1aEPist+r)b47_LehJGTu>TcgZe&J{ z{q&D{^Ps~z7|zj~rpoh2I_{gAYNoCIJmio3B}$!5vTF*h$Q*vFj~qbo%bJCCRy509 zHTdDh_HYH8Zb9`}D5;;J9fkWOQi%Y$B1!b9+ESj+B@dtAztlY2O3NE<6HFiqOF&p_ zW-K`KiY@RPSY-p9Q99}Hcd05DT79_pfb{BV7r~?9pWh=;mcKBLTen%THFPo2NN~Nf zriOtFnqx}rtO|A6k!r6 zf-z?y-UD{dT0kT9FJ`-oWuPHbo+3wBS(}?2ql(+e@VTExmfnB*liCb zmeI+v5*+W_L;&kQN^ChW{jE0Mw#0Tfs}`9bk3&7UjxP^Ke(%eJu2{VnW?tu7Iqecm zB5|=-QdzK$=h50~{X3*w4%o1FS_u(dG2s&427$lJ?6bkLet}yYXCy)u_Io1&g^c#( z-$yYmSpxz{>BL;~c+~sxJIe1$7eZI_9t`eB^Pr0)5CuA}w;;7#RvPq|H6!byRzIJG ziQ7a4y_vhj(AL`8PhIm9edCv|%TX#f50lt8+&V+D4<}IA@S@#f4xId80oH$!_!q?@ zFRGGg2mTv&@76P7aTI{)Hu%>3QS_d)pQ%g8BYi58K~m-Ov^7r8BhX7YC1D3vwz&N8{?H*_U7DI?CI)+et?q|eGu>42NJ?K4SY zD?kc>h@%4IqNYuQ8m10+8xr2HYg2qFNdJl=Tmp&ybF>1>pqVfa%SsV*BY$d6<@iJA ziyvKnZ(~F9xQNokBgMci#pnZ}Igh0@S~cYcU_2Jfuf|d3tuH?ZSSYBfM(Y3-JBsC|S9c;# zyIMkPxgrq};0T09pjj#X?W^TFCMf1-9P{)g88;NDI+S4DXe>7d3Mb~i-h&S|Jy{J< zq3736$bH?@{!amD!1Ys-X)9V=#Z={fzsjVYMX5BG6%}tkzwC#1nQLj1y1f#}8**4Y zAvDZHw8)N)8~oWC88CgzbwOrL9HFbk4}h85^ptuu7A+uc#$f^9`EWv1Vr{5+@~@Uv z#B<;-nt;)!k|fRIg;2DZ(A2M2aC65kOIov|?Mhi1Sl7YOU4c$T(DoRQIGY`ycfkn% zViHzL;E*A{`&L?GP06Foa38+QNGA zw3+Wqs(@q+H{XLJbwZzE(omw%9~LPZfYB|NF5%j%E5kr_xE0u;i?IOIchn~VjeDZ) zAqsqhP0vu2&Tbz3IgJvMpKbThC-@=nk)!|?MIPP>MggZg{cUcKsP8|N#cG5 zUXMXxcXBF9`p>09IR?x$Ry3;q@x*%}G#lnB1}r#!WL88I@uvm}X98cZ8KO&cqT1p> z+gT=IxPsq%n4GWgh-Bk8E4!~`r@t>DaQKsjDqYc&h$p~TCh8_Mck5UB84u6Jl@kUZCU9BA-S!*bf>ZotFX9?a_^y%)yH~rsAz0M5#^Di80_tgoKw(egN z`)#(MqAI&A84J#Z<|4`Co8`iY+Cv&iboMJ^f9ROUK0Lm$;-T*c;TCTED_0|qfhlcS zv;BD*$Zko#nWPL}2K8T-?4}p{u)4xon!v_(yVW8VMpxg4Kh^J6WM{IlD{s?%XRT8P|yCU`R&6gwB~ zg}{At!iWCzOH37!ytcPeC`(({ovP7M5Y@bYYMZ}P2Z3=Y_hT)4DRk}wfeIo%q*M9UvXYJq!-@Ly79m5aLD{hf@BzQB>FdQ4mw z6$@vzSKF^Gnzc9vbccii)==~9H#KW<6)Uy1wb~auBn6s`ct!ZEos`WK8e2%<00b%# zY9Nvnmj@V^K(a_38dw-S*;G-(i(ETuIwyirs?$FFW@|66a38k+a%GLmucL%Wc8qk3 z?h_4!?4Y-xt)ry)>J`SuY**fuq2>u+)VZ+_1Egzctb*xJ6+7q`K$^f~r|!i?(07CD zH!)C_uerf-AHNa?6Y61D_MjGu*|wcO+ZMOo4q2bWpvjEWK9yASk%)QhwZS%N2_F4& z16D18>e%Q1mZb`R;vW{+IUoKE`y3(7p zplg5cBB)dtf^SdLd4n60oWie|(ZjgZa6L*VKq02Aij+?Qfr#1z#fwh92aV-HGd^_w zsucG24j8b|pk>BO7k8dS86>f-jBP^Sa}SF{YNn=^NU9mLOdKcAstv&GV>r zLxKHPkFxpvE8^r@MSF6UA}cG`#yFL8;kA7ccH9D=BGBtW2;H>C`FjnF^P}(G{wU;G z!LXLCbPfsGeLCQ{Ep$^~)@?v`q(uI`CxBY44osPcq@(rR-633!qa zsyb>?v%@X+e|Mg`+kRL*(;X>^BNZz{_kw5+K;w?#pReiw7eU8_Z^hhJ&fj80XQkuU z39?-z)6Fy$I`bEiMheS(iB6uLmiMd1i)cbK*9iPpl+h4x9ch7x- z1h4H;W_G?|)i`z??KNJVwgfuAM=7&Apd3vm#AT8uzQZ!NII}}@!j)eIfn53h{NmN7 zAKG6SnKP%^k&R~m5#@_4B@V?hYyHkm>0SQ@PPiw*@Tp@UhP-?w@jW?nxXuCipMW=L zH*5l*d@+jXm0tIMP_ec6Jcy6$w(gKK@xBX8@%oPaSyG;13qkFb*LuVx3{AgIyy&n3 z@R2_DcEn|75_?-v5_o~%xEt~ONB>M~tpL!nOVBLPN&e5bn5>+7o0?Nm|EGJ5 zmUbF{u|Qn?cu5}n4@9}g(G1JxtzkKv(tqwm_?1`?YSVA2IS4WI+*(2D*wh&6MIEhw z+B+2U<&E&|YA=3>?^i6)@n1&&;WGHF-pqi_sN&^C9xoxME5UgorQ_hh1__zzR#zVC zOQt4q6>ME^iPJ37*(kg4^=EFqyKH@6HEHXy79oLj{vFqZGY?sVjk!BX^h$SFJlJnv z5uw~2jLpA)|0=tp>qG*tuLru?-u`khGG2)o{+iDx&nC}eWj3^zx|T`xn5SuR;Aw8U z`p&>dJw`F17@J8YAuW4=;leBE%qagVTG5SZdh&d)(#ZhowZ|cvWvGMMrfVsbg>_~! z19fRz8CSJdrD|Rl)w!uznBF&2-dg{>y4l+6(L(vzbLA0Bk&`=;oQQ>(M8G=3kto_) zP8HD*n4?MySO2YrG6fwSrVmnesW+D&fxjfEmp=tPd?RKLZJcH&K(-S+x)2~QZ$c(> zru?MND7_HPZJVF%wX(49H)+~!7*!I8w72v&{b={#l9yz+S_aVPc_So%iF8>$XD1q1 zFtucO=rBj0Ctmi0{njN8l@}!LX}@dwl>3yMxZ;7 z0Ff2oh8L)YuaAGOuZ5`-p%Z4H@H$;_XRJQ|&(MhO78E|nyFa158gAxG^SP(vGi^+< zChY}o(_=ci3Wta#|K6MVljNe0T$%Q5ylx-v`R)r8;3+VUpp-)7T`-Y&{Zk z*)1*2MW+_eOJtF5tCMDV`}jg-R(_IzeE9|MBKl;a7&(pCLz}5<Zf+)T7bgNUQ_!gZtMlw=8doE}#W+`Xp~1DlE=d5SPT?ymu!r4z%&#A-@x^=QfvDkfx5-jz+h zoZ1OK)2|}_+UI)i9%8sJ9X<7AA?g&_Wd7g#rttHZE;J*7!e5B^zdb%jBj&dUDg4&B zMMYrJ$Z%t!5z6=pMGuO-VF~2dwjoXY+kvR>`N7UYfIBMZGP|C7*O=tU z2Tg_xi#Q3S=1|=WRfZD;HT<1D?GMR%5kI^KWwGrC@P2@R>mDT^3qsmbBiJc21kip~ zZp<7;^w{R;JqZ)C4z-^wL=&dBYj9WJBh&rd^A^n@07qM$c+kGv^f+~mU5_*|eePF| z3wDo-qaoRjmIw<2DjMTG4$HP{z54_te_{W^gu8$r=q0JgowzgQPct2JNtWPUsjF8R zvit&V8$(;7a_m%%9TqPkCXYUp&k*MRcwr*24>hR! z$4c#E=PVE=P4MLTUBM z7#*RDe0}=B)(3cvNpOmWa*eH#2HR?NVqXdJ=hq);MGD07JIQQ7Y0#iD!$C+mk7x&B zMwkS@H%>|fmSu#+ zI!}Sb(%o29Vkp_Th>&&!k7O>Ba#Om~B_J{pT7BHHd8(Ede(l`7O#`_}19hr_?~JP9 z`q(`<)y>%)x;O7)#-wfCP{?llFMoH!)ZomgsOYFvZ1DxrlYhkWRw#E-#Qf*z@Y-EQ z1~?_=c@M4DO@8AzZ2hKvw8CgitzI9yFd&N1-{|vP#4IqYb*#S0e3hrjsEGlnc4xwk z4o!0rxpUt8j&`mJ8?+P8G{m^jbk)bo_UPM+ifW*y-A*et`#_Ja_3nYyRa9fAG1Xr5 z>#AM_@PY|*u)DGRWJihZvgEh#{*joJN28uN7;i5{kJ*Gb-TERfN{ERe_~$Es~NJCpdKLRvdj4658uYYx{ng7I<6j~w@p%F<7a(Ssib|j z51;=Py(Nu*#hnLx@w&8X%=jrADn3TW>kplnb zYbFIWWVQXN7%Cwn6KnR)kYePEBmvM45I)UJb$)ninpdYg3a5N6pm_7Q+9>!_^xy?k za8@tJ@OOs-pRAAfT>Nc2x=>sZUs2!9Dwa%TTmDggH4fq(x^MW>mcRyJINlAqK$YQCMgR8`>6=Sg$ zFnJZsA8xUBXIN3i70Q%8px@yQPMgVP=>xcPI38jNJK<=6hC={a07+n@R|$bnhB)X$ z(Zc%tadp70vBTnW{OUIjTMe38F}JIH$#A}PB&RosPyFZMD}q}5W%$rh>5#U;m`z2K zc(&WRxx7DQLM-+--^w*EWAIS%bi>h587qkwu|H=hma3T^bGD&Z!`u(RKLeNZ&pI=q$|HOcji(0P1QC!YkAp*u z3%S$kumxR}jU<@6`;*-9=5-&LYRA<~uFrwO3U0k*4|xUTp4ZY7;Zbjx|uw&BWU$zK(w55pWa~#=f$c zNDW0O68N!xCy>G}(CX=;8hJLxAKn@Aj(dbZxO8a$+L$jK8$N-h@4$i8)WqD_%Snh4 zR?{O%k}>lr>w$b$g=VP8mckcCrjnp>uQl5F_6dPM8FWRqs}h`DpfCv20uZhyY~tr8 zkAYW4#yM;*je)n=EAb(q@5BWD8b1_--m$Q-3wbh1hM{8ihq7UUQfg@)l06}y+#=$( z$x>oVYJ47zAC^>HLRE-!HitjUixP6!R98WU+h>zct7g4eD;Mj#FL*a!VW!v-@b(Jv zj@@xM5noCp5%Vk3vY{tyI#oyDV7<$`KG`tktVyC&0DqxA#>V;-3oH%NW|Q&=UQ&zU zXNIT67J4D%5R1k#bW0F}TD`hlW7b)-=-%X4;UxQ*u4bK$mTAp%y&-(?{sXF%e_VH6 zTkt(X)SSN|;8q@8XX6qfR;*$r#HbIrvOj*-5ND8RCrcw4u8D$LXm5zlj@E5<3S0R# z??=E$p{tOk96$SloZ~ARe5`J=dB|Nj?u|zy2r(-*(q^@YwZiTF@QzQyPx_l=IDKa) zqD@0?IHJqSqZ_5`)81?4^~`yiGh6>7?|dKa8!e|}5@&qV!Iu9<@G?E}Vx9EzomB3t zEbMEm$TKGwkHDpirp;FZD#6P5qIlQJ8}rf;lHoz#h4TFFPYmS3+8(13_Mx2`?^=8S z|0)0&dQLJTU6{b%*yrpQe#OKKCrL8}YKw+<#|m`SkgeoN69TzIBQOl_Yg)W*w?NW) z*WxhEp$zQBBazJSE6ygu@O^!@Fr46j=|K`Mmb~xbggw7<)BuC@cT@Bwb^k?o-A zKX^9AyqR?zBtW5UA#siILztgOp?r4qgC`9jYJG_fxlsVSugGprremg-W(K0{O!Nw-DN%=FYCyfYA3&p*K>+|Q}s4rx#CQK zNj^U;sLM#q8}#|PeC$p&jAjqMu(lkp-_50Y&n=qF9`a3`Pr9f;b`-~YZ+Bb0r~c+V z*JJ&|^T{}IHkwjNAaM^V*IQ;rk^hnnA@~?YL}7~^St}XfHf6OMMCd9!vhk#gRA*{L zp?&63axj|Si%^NW05#87zpU_>QpFNb+I00v@cHwvdBn+Un)n2Egdt~LcWOeBW4Okm zD$-e~RD+W|UB;KQ;a7GOU&%p*efGu2$@wR74+&iP8|6#_fmnh^WcJLs)rtz{46);F z4v0OL{ZP9550>2%FE(;SbM*#sqMl*UXOb>ch`fJ|(*bOZ9=EB1+V4fkQ)hjsm3-u^Pk-4ji_uDDHdD>84tER!MvbH`*tG zzvbhBR@}Yd`azQGavooV=<WbvWLlO#x`hyO34mKcxrGv=`{ssnP=0Be5#1B;Co9 zh{TR>tjW2Ny$ZxJpYeg57#0`GP#jxDCU0!H15nL@@G*HLQcRdcsUO3sO9xvtmUcc{F*>FQZcZ5bgwaS^k-j5mmt zI7Z{Xnoml|A(&_{imAjK!kf5>g(oDqDI4C{;Bv162k8sFNr;!qPa2LPh>=1n z=^_9)TsLDvTqK7&*Vfm5k;VXjBW^qN3Tl&}K=X5)oXJs$z3gk0_+7`mJvz{pK|FVs zHw!k&7xVjvY;|(Py<;J{)b#Yjj*LZO7x|~pO4^MJ2LqK3X;Irb%nf}L|gck zE#55_BNsy6m+W{e zo!P59DDo*s@VIi+S|v93PwY6d?CE=S&!JLXwE9{i)DMO*_X90;n2*mPDrL%{iqN!?%-_95J^L z=l<*{em(6|h7DR4+4G3Wr;4*}yrBkbe3}=p7sOW1xj!EZVKSMSd;QPw>uhKK z#>MlS@RB@-`ULv|#zI5GytO{=zp*R__uK~R6&p$q{Y{iNkg61yAgB8C^oy&``{~FK z8hE}H&nIihSozKrOONe5Hu?0Zy04U#0$fB7C6y~?8{or}KNvP)an=QP&W80mj&8WL zEZQF&*FhoMMG6tOjeiCIV;T{I>jhi9hiUwz?bkX3NS-k5eWKy)Mo_orMEg4sV6R6X&i-Q%JG;Esl+kLpn@Bsls9O|i9z`tKB^~1D5)RIBB&J<6T@a4$pUvh$IR$%ubH)joi z!7>ON0DPwx=>0DA>Bb^c?L8N0BBrMl#oDB+GOXJh;Y&6I)#GRy$W5xK%a;KS8BrER zX)M>Rdoc*bqP*L9DDA3lF%U8Yzb6RyIsW@}IKq^i7v&{LeIc=*ZHIbO68x=d=+0T( zev=DT9f|x!IWZNTB#N7}V4;9#V$%Wo0%g>*!MdLOEU>My0^gni9ocID{$g9ytD!gy zKRWT`DVN(lcYjR|(}f0?zgBa3SwunLfAhx><%u0uFkrdyqlh8_g zDKt#R6rA2(Vm2LW_>3lBNYKG_F{TEnnKWGGC15y&OebIRhFL4TeMR*v9i0wPoK#H< zu4){s4K&K)K(9~jgGm;H7lS7y_RYfS;&!Oj5*eqbvEcW^a*i67nevzOZxN6F+K~A%TYEtsAVsR z@J=1hc#Dgs7J2^FL|qV&#WBFQyDtEQ2kPO7m2`)WFhqAob)Y>@{crkil6w9VoA?M6 zADGq*#-hyEVhDG5MQj677XmcWY1_-UO40QEP&+D)rZoYv^1B_^w7zAvWGw&pQyCyx zD|ga$w!ODOxxGf_Qq%V9Z7Q2pFiUOIK818AGeZ-~*R zI1O|SSc=3Z?#61Rd|AXx2)K|F@Z1@x!hBBMhAqiU)J=U|Y)T$h3D?ZPPQgkSosnN! zIqw-t$0fqsOlgw3TlHJF*t$Q@bg$9}A3X=cS@-yU3_vNG_!#9}7=q7!LZ?-%U26W4 z$d>_}*s1>Ac%3uFR;tnl*fNlylJ)}r2^Q3&@+is3BIv<}x>-^_ng;jhdaM}6Sg3?p z0jS|b%QyScy3OQ(V*~l~bK>VC{9@FMuW_JUZO?y(V?LKWD6(MXzh}M3r3{7b4eB(#`(q1m{>Be%_<9jw8HO!x#yF6vez$c#kR+}s zZO-_;25Sxngd(}){zv?ccbLqRAlo;yog>4LH&uZUK1n>x?u49C)Y&2evH5Zgt~666 z_2_z|H5AO5Iqxv_Bn~*y1qzRPcob<+Otod5Xd2&z=C;u+F}zBB@b^UdGdUz|s!H}M zXG%KiLzn3G?FZgdY&3pV$nSeY?ZbU^jhLz9!t0K?ep}EFNqR1@E!f*n>x*!uO*~JF zW9UXWrVgbX1n#76_;&0S7z}(5n-bqnII}_iDsNqfmye@)kRk`w~1 z6j4h4BxcPe6}v)xGm%=z2#tB#^KwbgMTl2I*$9eY|EWAHFc3tO48Xo5rW z5oHD!G4kb?MdrOHV=A+8ThlIqL8Uu+7{G@ zb)cGBm|S^Eh5= z^E^SZ=yeC;6nNCdztw&TdnIz}^Of@Ke*@vjt)0g>Y!4AJvWiL~e7+9#Ibhe)> ziNwh>gWZL@FlWc)wzihocz+%+@*euwXhW%Hb>l7tf8aJe5_ZSH1w-uG|B;9qpcBP0 zM`r1Hu#htOl)4Cl1c7oY^t0e4Jh$-I(}M5kzWqh{F=g&IM#JiC`NDSd@BCKX#y<P@Gwl$3a3w z6<(b|K(X5FIR22M)sy$4jY*F4tT{?wZRI+KkZFb<@j@_C316lu1hq2hA|1wCmR+S@ zRN)YNNE{}i_H`_h&VUT5=Y(lN%m?%QX;6$*1P}K-PcPx>*S55v)qZ@r&Vcic-sjkm z! z=nfW&X`}iAqa_H$H%z3Tyz5&P3%+;93_0b;zxLs)t#B|up}JyV$W4~`8E@+BHQ+!y zuIo-jW!~)MN$2eHwyx-{fyGjAWJ(l8TZtUp?wZWBZ%}krT{f*^fqUh+ywHifw)_F> zp76_kj_B&zFmv$FsPm|L7%x-j!WP>_P6dHnUTv!9ZWrrmAUteBa`rT7$2ixO;ga8U z3!91micm}{!Btk+I%pMgcKs?H4`i+=w0@Ws-CS&n^=2hFTQ#QeOmSz6ttIkzmh^`A zYPq)G1l3h(E$mkyr{mvz*MP`x+PULBn%CDhltKkNo6Uqg!vJ#DA@BIYr9TQ`18Un2 zv$}BYzOQuay9}w(?JV63F$H6WmlYPPpH=R|CPb%C@BCv|&Q|&IcW7*LX?Q%epS z`=CPx{1HnJ9_46^=0VmNb>8JvMw-@&+V8SDLRYsa>hZXEeRbtf5eJ>0@Ds47zIY{N z42EOP9J8G@MXXdeiPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?lu1NER9Fe^SItioK@|V(ZWmgL zZT;XwPgVuWM>O%^|Dc$VK;n&?9!&g5)aVsG8cjs5UbtxVVnQNOV~7Mrg3+jnU;rhE z6fhW6P)R>_eXrXo-RW*y6RQ_qcb^s1wTu$TwriZ`=JUws>vRi}5x}MW1MR#7p|gIWJlaLK;~xaN}b< z<-@=RX-%1mt`^O0o^~2=CD7pJ<<$Rp-oUL-7PuG>do^5W_Mk#unlP}6I@6NPxY`Q} zuXJF}!0l)vwPNAW;@5DjPRj?*rZxl zwn;A(cFV!xe^CUu+6SrN?xe#mz?&%N9QHf~=KyK%DoB8HKC)=w=3E?1Bqj9RMJs3U z5am3Uv`@+{jgqO^f}Lx_Jp~CoP3N4AMZr~4&d)T`R?`(M{W5WWJV^z~2B|-oih@h^ zD#DuzGbl(P5>()u*YGo*Och=oRr~3P1wOlKqI)udc$|)(bacG5>~p(y>?{JD7nQf_ z*`T^YL06-O>T(s$bi5v~_fWMfnE7Vn%2*tqV|?~m;wSJEVGkNMD>+xCu#um(7}0so zSEu7?_=Q64Q5D+fz~T=Rr=G_!L*P|(-iOK*@X8r{-?oBlnxMNNgCVCN9Y~ocu+?XA zjjovJ9F1W$Nf!{AEv%W~8oahwM}4Ruc+SLs>_I_*uBxdcn1gQ^2F8a*vGjgAXYyh? zWCE@c5R=tbD(F4nL9NS?$PN1V_2*WR?gjv3)4MQeizuH`;sqrhgykEzj z593&TGlm3h`sIXy_U<7(dpRXGgp0TB{>s?}D{fwLe>IV~exweOfH!qM@CV5kib!YA z6O0gvJi_0J8IdEvyP#;PtqP*=;$iI2t(xG2YI-e!)~kaUn~b{6(&n zp)?iJ`z2)Xh%sCV@BkU`XL%_|FnCA?cVv@h*-FOZhY5erbGh)%Q!Av#fJM3Csc_g zC2I6x%$)80`Tkz#KRA!h1FzY`?0es3t!rKDT5EjPe6B=BLPr7s0GW!if;Ip^!AmGW zL;$`Vdre+|FA!I4r6)keFvAx3M#1`}ijBHDzy)3t0gwjl|qC2YB`SSxFKHr(oY#H$)x{L$LL zBdLKTlsOrmb>T0wd=&6l3+_Te>1!j0OU8%b%N342^opKmT)gni(wV($s(>V-fUv@0p8!f`=>PxC|9=nu ze{ToBBj8b<{PLfXV$h8YPgA~E!_sF9bl;QOF{o6t&JdsX?}rW!_&d`#wlB6T_h;Xf zl{4Tz5>qjF4kZgjO7ZiLPRz_~U@k5%?=30+nxEh9?s78gZ07YHB`FV`4%hlQlMJe@J`+e(qzy+h(9yY^ckv_* zb_E6o4p)ZaWfraIoB2)U7_@l(J0O%jm+Or>8}zSSTkM$ASG^w3F|I? z$+eHt7T~04(_WfKh27zqS$6* zzyy-ZyqvSIZ0!kkSvHknm_P*{5TKLQs8S6M=ONuKAUJWtpxbL#2(_huvY(v~Y%%#~ zYgsq$JbLLprKkV)32`liIT$KKEqs$iYxjFlHiRNvBhxbDg*3@Qefw4UM$>i${R5uB zhvTgmqQsKA{vrKN;TSJU2$f9q=y{$oH{<)woSeV>fkIz6D8@KB zf4M%v%f5U2?<8B(xn}xV+gWP?t&oiapJhJbfa;agtz-YM7=hrSuxl8lAc3GgFna#7 zNjX7;`d?oD`#AK+fQ=ZXqfIZFEk{ApzjJF0=yO~Yj{7oQfXl+6v!wNnoqwEvrs81a zGC?yXeSD2NV!ejp{LdZGEtd1TJ)3g{P6j#2jLR`cpo;YX}~_gU&Gd<+~SUJVh+$7S%`zLy^QqndN<_9 zrLwnXrLvW+ew9zX2)5qw7)zIYawgMrh`{_|(nx%u-ur1B7YcLp&WFa24gAuw~& zKJD3~^`Vp_SR$WGGBaMnttT)#fCc^+P$@UHIyBu+TRJWbcw4`CYL@SVGh!X&y%!x~ zaO*m-bTadEcEL6V6*{>irB8qT5Tqd54TC4`h`PVcd^AM6^Qf=GS->x%N70SY-u?qr>o2*OV7LQ=j)pQGv%4~z zz?X;qv*l$QSNjOuQZ>&WZs2^@G^Qas`T8iM{b19dS>DaXX~=jd4B2u`P;B}JjRBi# z_a@&Z5ev1-VphmKlZEZZd2-Lsw!+1S60YwW6@>+NQ=E5PZ+OUEXjgUaXL-E0fo(E* zsjQ{s>n33o#VZm0e%H{`KJi@2ghl8g>a~`?mFjw+$zlt|VJhSU@Y%0TWs>cnD&61fW4e0vFSaXZa4-c}U{4QR8U z;GV3^@(?Dk5uc@RT|+5C8-24->1snH6-?(nwXSnPcLn#X_}y3XS)MI_?zQ$ZAuyg+ z-pjqsw}|hg{$~f0FzmmbZzFC0He_*Vx|_uLc!Ffeb8#+@m#Z^AYcWcZF(^Os8&Z4g zG)y{$_pgrv#=_rV^D|Y<_b@ICleUv>c<0HzJDOsgJb#Rd-Vt@+EBDPyq7dUM9O{Yp zuGUrO?ma2wpuJuwl1M=*+tb|qx7Doj?!F-3Z>Dq_ihFP=d@_JO;vF{iu-6MWYn#=2 zRX6W=`Q`q-+q@Db|6_a1#8B|#%hskH82lS|9`im0UOJn?N#S;Y0$%xZw3*jR(1h5s z?-7D1tnIafviko>q6$UyqVDq1o@cwyCb*})l~x<@s$5D6N=-Uo1yc49p)xMzxwnuZ zHt!(hu-Ek;Fv4MyNTgbW%rPF*dB=;@r3YnrlFV{#-*gKS_qA(G-~TAlZ@Ti~Yxw;k za1EYyX_Up|`rpbZ0&Iv#$;eC|c0r4XGaQ-1mw@M_4p3vKIIpKs49a8Ns#ni)G314Z z8$Ei?AhiT5dQGWUYdCS|IC7r z=-8ol>V?u!n%F*J^^PZ(ONT&$Ph;r6X;pj|03HlDY6r~0g~X#zuzVU%a&!fs_f|m?qYvg^Z{y?9Qh7Rn?T*F%7lUtA6U&={HzhYEzA`knx1VH> z{tqv?p@I(&ObD5L4|YJV$QM>Nh-X3cx{I&!$FoPC_2iIEJfPk-$;4wz>adRu@n`_y z_R6aN|MDHdK;+IJmyw(hMoDCFCQ(6?hCAG5&7p{y->0Uckv# zvooVuu04$+pqof777ftk<#42@KQ((5DPcSMQyzGOJ{e9H$a9<2Qi_oHjl{#=FUL9d z+~0^2`tcvmp0hENwfHR`Ce|<1S@p;MNGInXCtHnrDPXCKmMTZQ{HVm_cZ>@?Wa6}O zHsJc7wE)mc@1OR2DWY%ZIPK1J2p6XDO$ar`$RXkbW}=@rFZ(t85AS>>U0!yt9f49^ zA9@pc0P#k;>+o5bJfx0t)Lq#v4`OcQn~av__dZ-RYOYu}F#pdsl31C^+Qgro}$q~5A<*c|kypzd} ziYGZ~?}5o`S5lw^B{O@laad9M_DuJle- z*9C7o=CJh#QL=V^sFlJ0c?BaB#4bV^T(DS6&Ne&DBM_3E$S^S13qC$7_Z?GYXTpR@wqr70wu$7+qvf-SEUa5mdHvFbu^7ew!Z1a^ zo}xKOuT*gtGws-a{Tx}{#(>G~Y_h&5P@Q8&p!{*s37^QX_Ibx<6XU*AtDOIvk|^{~ zPlS}&DM5$Ffyu-T&0|KS;Wnaqw{9DB&B3}vcO14wn;)O_e@2*9B&0I_ zZz{}CMxx`hv-XouY>^$Y@J(_INeM>lIQI@I>dBAqq1)}?Xmx(qRuX^i4IV%=MF306 z9g)i*79pP%_7Ex?m6ag-4Tlm=Z;?DQDyC-NpUIb#_^~V_tsL<~5<&;Gf2N+p?(msn zzUD~g>OoW@O}y0@Z;RN)wjam`CipmT&O7a|YljZqU=U86 zedayEdY)2F#BJ6xvmW8K&ffdS*0!%N<%RB!2~PAT4AD*$W7yzHbX#Eja9%3aD+Ah2 zf#T;XJW-GMxpE=d4Y>}jE=#U`IqgSoWcuvgaWQ9j1CKzG zDkoMDDT)B;Byl3R2PtC`ip=yGybfzmVNEx{xi_1|Cbqj>=FxQc{g`xj6fIfy`D8fA z##!-H_e6o0>6Su&$H2kQTujtbtyNFeKc}2=|4IfLTnye#@$Au7Kv4)dnA;-fz@D_8 z)>irG$)dkBY~zX zC!ZXLy*L3xr6cb70QqfN#Q>lFIc<>}>la4@3%7#>a1$PU&O^&VszpxLC%*!m-cO{B z-Y}rQr4$84(hvy#R69H{H zJ*O#uJh)TF6fbXy;fZkk%X=CjsTK}o5N1a`d7kgYYZLPxsHx%9*_XN8VWXEkVJZ%A z1A+5(B;0^{T4aPYr8%i@i32h)_)|q?9vws)r+=5u)1YNftF5mknwfd*%jXA2TeP}Z zQ!m?xJ3?9LpPM?_A3$hQ1QxNbR&}^m z!F999s?p^ak#C4NM_x2p9FoXWJ$>r?lJ)2bG)sX{gExgLA2s5RwHV!h6!C~d_H||J z>9{E{mEv{Z1z~65Vix@dqM4ZqiU|!)eWX$mwS5mLSufxbpBqqS!jShq1bmwCR6 z4uBri7ezMeS6ycaXPVu(i2up$L; zjpMtB`k~WaNrdgM_R=e#SN?Oa*u%nQy01?()h4A(jyfeNfx;5o+kX?maO4#1A^L}0 zYNyIh@QVXIFiS0*tE}2SWTrWNP3pH}1Vz1;E{@JbbgDFM-_Mky^7gH}LEhl~Ve5PexgbIyZ(IN%PqcaV@*_`ZFb=`EjspSz%5m2E34BVT)d=LGyHVz@-e%9Ova*{5@RD;7=Ebkc2GP%pIP^P7KzKapnh`UpH?@h z$RBpD*{b?vhohOKf-JG3?A|AX|2pQ?(>dwIbWhZ38GbTm4AImRNdv_&<99ySX;kJ| zo|5YgbHZC#HYgjBZrvGAT4NZYbp}qkVSa;C-LGsR26Co+i_HM&{awuO9l)Ml{G8zD zs$M8R`r+>PT#Rg!J(K6T4xHq7+tscU(}N$HY;Yz*cUObX7J7h0#u)S7b~t^Oj}TBF zuzsugnst;F#^1jm>22*AC$heublWtaQyM6RuaquFd8V#hJ60Z3j7@bAs&?dD#*>H0SJaDwp%U~27>zdtn+ z|8sZzklZy$%S|+^ie&P6++>zbrq&?+{Yy11Y>@_ce@vU4ZulS@6yziG6;iu3Iu`M= zf3rcWG<+3F`K|*(`0mE<$89F@jSq;j=W#E>(R}2drCB7D*0-|D;S;(;TwzIJkGs|q z2qH{m_zZ+el`b;Bv-#bQ>}*VPYC|7`rgBFf2oivXS^>v<&HHTypvd4|-zn|=h=TG{ z05TH2+{T%EnADO>3i|CB zCu60#qk`}GW{n4l-E$VrqgZGbI zbQW690KgZt4U3F^5@bdO1!xu~p@7Y~*_FfWg2CdvED5P5#w#V46LH`<&V0{t&Ml~4 zHNi7lIa+#i+^Z6EnxO7KJQw)wD)4~&S-Ki8)3=jpqxmx6c&zU&<&h%*c$I(5{1HZT zc9WE}ijcWJiVa^Q^xC|WX0habl89qycOyeViIbi(LFsEY_8a|+X^+%Qv+W4vzj>`y zpuRnjc-eHNkvXvI_f{=*FX=OKQzT?bck#2*qoKTHmDe>CDb&3AngA1O)1b}QJ1Tun z_<@yVEM>qG7664Pa@dzL@;DEh`#?yM+M|_fQS<7yv|i*pw)|Z8)9IR+QB7N3v3K(wv4OY*TXnH&X0nQB}?|h2XQeGL^q~N7N zDFa@x0E(UyN7k9g%IFq7Sf+EAfE#K%%#`)!90_)Dmy3Bll&e1vHQyPA87TaF(xbqMpDntVp?;8*$87STop$!EAnGhZ?>mqPJ(X zFsr336p3P{PpZCGn&^LP(JjnBbl_3P3Kcq+m}xVFMVr1zdCPJMDIV_ki#c=vvTwbU z*gKtfic&{<5ozL6Vfpx>o2Tts?3fkhWnJD&^$&+Mh5WGGyO7fG@6WDE`tEe(8<;+q z@Ld~g08XDzF8xtmpIj`#q^(Ty{Hq>t*v`pedHnuj(0%L(%sjkwp%s}wMd!a<*L~9T z9MM@s)Km~ogxlqEhIw5(lc46gCPsSosUFsgGDr8H{mj%OzJz{N#;bQ;KkV+ZWA1(9 zu0PXzyh+C<4OBYQ0v3z~Lr;=C@qmt8===Ov2lJ1=DeLfq*#jgT{YQCuwz?j{&3o_6 zsqp2Z_q-YWJg?C6=!Or|b@(zxTlg$ng2eUQzuC<+o)k<6^9ju_Z*#x+oioZ5T8Z_L zz9^A1h2eFS0O5muq8;LuDKwOv4A9pxmOjgb6L*i!-(0`Ie^d5Fsgspon%X|7 zC{RRXEmYn!5zP9XjG*{pLa)!2;PJB2<-tH@R7+E1cRo=Wz_5Ko8h8bB$QU%t9#vol zAoq?C$~~AsYC|AQQ)>>7BJ@{Cal)ZpqE=gjT+Juf!RD-;U0mbV1ED5PbvFD6M=qj1 zZ{QERT5@(&LQ~1X9xSf&@%r|3`S#ZCE=sWD`D4YQZ`MR`G&s>lN{y2+HqCfvgcw3E z-}Kp(dfGG?V|97kAHQX+OcKCZS`Q%}HD6u*e$~Ki&Vx53&FC!x94xJd4F2l^qQeFO z?&JdmgrdVjroKNJx64C!H&Vncr^w zzR#XI}Dn&o8jB~_YlVM^+#0W(G1LZH5K^|uYT@KSR z^Y5>^*Bc45E1({~EJB(t@4n9gb-eT#s@@7)J^^<_VV`Pm!h7av8XH6^5zO zOcQBhTGr;|MbRsgxCW69w{bl4EW#A~);L?d4*y#j8Ne=Z@fmJP0k4{_cQ~KA|Y#_#BuUiYx8y*za3_6Y}c=GSe7(2|KAfhdzud!Zq&}j)=o4 z7R|&&oX7~e@~HmyOOsCCwy`AR+deNjZ3bf6ijI_*tKP*_5JP3;0d;L_p(c>W1b%sG zJ*$wcO$ng^aW0E(5ldckV9unU7}OB7s?Wx(761?1^&8tA5y0_(ieV>(x-e@}1`lWC z-YH~G$D>#ud!SxK2_Iw{K%92=+{4yb-_XC>ji&j7)1ofp(OGa4jjF;Hd*`6YQL+Jf zffg+6CPc8F@EDPN{Kn96yip;?g@)qgkPo^nVKFqY?8!=h$G$V=<>%5J&iVjwR!7H0 z$@QL|_Q81I;Bnq8-5JyNRv$Y>`sWl{qhq>u+X|)@cMlsG!{*lu?*H`Tp|!uv z9oEPU1jUEj@ueBr}%Y)7Luyi)REaJV>eQ{+uy4uh0ep0){t;OU8D*RZ& zE-Z-&=BrWQLAD^A&qut&4{ZfhqK1ZQB0fACP)=zgx(0(o-`U62EzTkBkG@mXqbjXm z>w`HNeQM?Is&4xq@BB(K;wv5nI6EXas)XXAkUuf}5uSrZLYxRCQPefn-1^#OCd4aO zzF=dQ*CREEyWf@n6h7(uXLNgJIwGp#Xrsj6S<^bzQ7N0B0N{XlT;`=m9Olg<>KL}9 zlp>EKTx-h|%d1Ncqa=wnQEuE;sIO-f#%Bs?g4}&xS?$9MG?n$isHky0caj za8W+B^ERK#&h?(x)7LLpOqApV5F>sqB`sntV%SV>Q1;ax67qs+WcssfFeF3Xk=e4^ zjR2^(%K1oBq%0%Rf!y&WT;lu2Co(rHi|r1_uW)n{<7fGc-c=ft7Z0Q}r4W$o$@tQF#i?jDBwZ8h+=SC}3?anUp3mtRVv9l#H?-UD;HjTF zQ*>|}e=6gDrgI9p%c&4iMUkQa4zziS$bO&i#DI$Wu$7dz7-}XLk%!US^XUIFf2obO zFCTjVEtkvYSKWB;<0C;_B{HHs~ax_48^Cml*mjfBC5*7^HJZiLDir(3k&BerVIZF8zF;0q80eX8c zPN4tc+Dc5DqEAq$Y3B3R&XPZ=AQfFMXv#!RQnGecJONe0H;+!f^h5x0wS<+%;D}MpUbTNUBA}S2n&U59-_5HKr{L^jPsV8B^%NaH|tUr)mq=qCBv_- ziZ1xUp(ZzxUYTCF@C}To;u60?RIfTGS?#JnB8S8@j`TKPkAa)$My+6ziGaBcA@){d z91)%+v2_ba7gNecdj^8*I4#<11l!{XKl6s0zkXfJPxhP+@b+5ev{a>p*W-3*25c&} zmCf{g9mPWVQ$?Sp*4V|lT@~>RR)9iNdN^7KT@>*MU3&v^3e?=NTbG9!h6C|9zO097 zN{Qs6YwR-5$)~ z`b~qs`a1Dbx8P>%V=1XGjBptMf%P~sl1qbHVm1HYpY|-Z^Dar8^HqjIw}xaeRlsYa zJ_@Apy-??`gxPmb`m`0`z`#G7*_C}qiSZe~l2z65tE~IwMw$1|-u&t|z-8SxliH00 zlh1#kuqB56s+E&PWQ7Nz17?c}pN+A@-c^xLqh(j;mS|?>(Pf7(?qd z5q@jkc^nA&!K-}-1P=Ry0yyze0W!+h^iW}7jzC1{?|rEFFWbE^Yu7Y}t?jmP-D$f+ zmqFT7nTl0HL|4jwGm7w@a>9 zKD)V~+g~ysmei$OT5}%$&LK8?ib|8aY|>W3;P+0B;=oD=?1rg+PxKcP(d;OEzq1CKA&y#boc51P^ZJPPS)z5 zAZ)dd2$glGQXFj$`XBBJyl2y-aoBA8121JC9&~|_nY>nkmW>TLi%mWdn-^Jks-Jv| zSR*wij;A3Fcy8KsDjQ15?Z9oOj|Qw2;jgJiq>dxG(2I2RE- z$As!#zSFIskebqU2bnoM^N<4VWD2#>!;saPSsY8OaCCQqkCMdje$C?Sp%V}f2~tG5 z0whMYk6tcaABwu*x)ak@n4sMElGPX1_lmv@bgdI2jPdD|2-<~Jf`L`@>Lj7{<-uLQ zE3S_#3e10q-ra=vaDQ42QUY^@edh>tnTtpBiiDVUk5+Po@%RmuTntOlE29I4MeJI?;`7;{3e4Qst#i-RH6s;>e(Sc+ubF2_gwf5Qi%P!aa89fx6^{~A*&B4Q zKTF|Kx^NkiWx=RDhe<{PWXMQ;2)=SC=yZC&mh?T&CvFVz?5cW~ritRjG2?I0Av_cI z)=s!@MXpXbarYm>Kj0wOxl=eFMgSMc?62U#2gM^li@wKPK9^;;0_h7B>F>0>I3P`{ zr^ygPYp~WVm?Qbp6O3*O2)(`y)x>%ZXtztz zMAcwKDr=TCMY!S-MJ8|2MJCVNUBI0BkJV6?(!~W!_dC{TS=eh}t#X+2D>Kp&)ZN~q zvg!ogxUXu^y(P*;Q+y_rDoGeSCYxkaGPldDDx)k;ocJvvGO#1YKoQLHUf2h_pjm&1 zqh&!_KFH03FcJvSdfgUYMp=5EpigZ*8}7N_W%Ms^WSQ4hH`9>3061OEcxmf~TcYn5_oHtscWn zo5!ayj<_fZ)vHu3!A!7M;4y1QIr8YGy$P2qDD_4+T8^=^dB6uNsz|D>p~4pF3Nrb6 zcpRK*($<~JUqOya#M1=#IhOZ zG)W+rJS-x(6EoVz)P zsSo>JtnChdj9^);su%SkFG~_7JPM zEDz3gk2T7Y%x>1tWyia|op(ilEzvAujW?Xwlw>J6d7yEi8E zv30riR|a_MM%ZZX&n!qm0{2agq(s?x9E@=*tyT$nND+{Djpm7Rsy!+c$j+wqMwTOF zZL8BQ|I`<^bGW)5apO{lh(Asqen?_U`$_n0-Ob~Yd%^89oEe%9yGumQ_8Be+l2k+n zCxT%s?bMpv|AdWP7M1LQwLm|x+igA~;+iK-*+tClF&ueX_V}>=4gvZ01xpubQWXD_ zi?Un>&3=$fu)dgk-Z;0Ll}HK5_YM->l^Czrd0^cJ))(DwL2g3aZuza7ga9^|mT_70 z))}A}r1#-(9cxtn<9jGRwOB4hb9kK@YCgjfOM-90I$8@l=H^`K$cyhe2mTM|FY9vW znH~h)I<_aa#V1xmhk?Ng@$Jw-s%a!$BI4Us+Df+?J&gKAF-M`v}j`OWKP3>6`X`tEmhe#y*(Xm$_^Ybbs=%;L7h zp7q^C*qM}Krqsinq|WolR99>_!GL#Z71Hhz|IwQQv<>Ds09B?Je(lhI1(FInO8mc} zl$RyKCUmfku+Cd^8s0|t+e}5g7M{ZPJQH=UB3(~U&(w#Bz#@DTDHy>_UaS~AtN>4O zJ-I#U@R($fgupHebcpuEBX`SZ>kN!rW$#9>s{^3`86ZRQRtYTY)hiFm_9wU3c`SC8 z-5M%g)h}3Pt|wyj#F%}pGC@VL`9&>9P+_UbudCkS%y2w&*o})hBplrB*@Z?gel5q+ z%|*59(sR9GMk3xME}wd%&k?7~J)OL`rK#4d-haC7uaU8-L@?$K6(r<0e<;y83rK&` z3Q!1rD9WkcB8WBQ|WT|$u^lkr0UL4WH4EQTJyk@5gzHb18cOte4w zS`fLv8q;PvAZyY;*Go3Qw1~5#gP0D0ERla6M6#{; zr1l?bR}Nh+OC7)4bfAs(0ZD(axaw6j9v`^jh5>*Eo&$dAnt?c|Y*ckEORIiJXfGcM zEo`bmIq6rJm`XhkXR-^3d8^RTK2;nmVetHfUNugJG(4XLOu>HJA;0EWb~?&|0abr6 zxqVp@p=b3MN^|~?djPe!=eex(u!x>RYFAj|*T$cTi*Sd3Bme7Pri1tkK9N`KtRmXf zZYNBNtik97ct1R^vamQBfo9ZUR@k*LhIg8OR9d_{iv#t)LQV91^5}K5u{eyxwOFoU zHMVq$C>tfa@uNDW^_>EmO~WYQd(@!nKmAvSSIb&hPO|}g-3985t?|R&WZXvxS}Kt2i^eRe>WHb_;-K5cM4=@AN1>E&1c$k!w4O*oscx(f=<1K6l#8Exi)U(ZiZ zdr#YTP6?m1e1dOKysUjQ^>-MR={OuD00g6+(a^cvcmn#A_%Fh3Of%(qP5nvjS1=(> z|Ld8{u%(J}%2SY~+$4pjy{()5HN2MYUjg1X9umxOMFFPdM+IwOVEs4Z(olynvT%G) zt9|#VR}%O2@f6=+6uvbZv{3U)l;C{tuc zZ{K$rut=eS%3_~fQv^@$HV6#9)K9>|0qD$EV2$G^XUNBLM|5-ZmFF!KV)$4l^KVj@ zZ4fI}Knv*K%zPqK77}B-h_V{66VrmoZP2>@^euu8Rc}#qwRwt5uEBWcJJE5*5rT2t zA4Jpx`QQ~1Sh_n_a9x%Il!t1&B~J6p54zxAJx`REov${jeuL8h8x-z=?qwMAmPK5i z_*ES)BW(NZluu#Bmn1-NUKQip_X&_WzJy~J`WYxEJQ&Gu7DD< z&F9urE;}8S{x4{yB zaq~1Zrz%8)<`prSQv$eu5@1RY2WLu=waPTrn`WK%;G5(jt^FeM;gOdvXQjYhax~_> z{bS_`;t#$RYMu-;_Dd&o+LD<5Afg6v{NK?0d8dD5ohAN?QoocETBj?y{MB)jQ%UQ}#t3j&iL!qr@#6JEajR3@^k5wgLfI9S9dT2^f`2wd z%I#Q*@Ctk@w=(u)@QC}yBvUP&fFRR-uYKJ){Wp3&$s(o~W7OzgsUIPx0|ph2L1(r*_Pa@T@mcH^JxBjh09#fgo|W#gG7}|)k&uD1iZxb0 z@|Y)W79SKj9sS&EhmTD;uI#)FE6VwQ*YAr&foK$RI5H8_ripb$^=;U%gWbrrk4!5P zXDcyscEZoSH~n6VJu8$^6LE6)>+=o#Q-~*jmob^@191+Ot1w454e3)WMliLtY6~^w zW|n#R@~{5K#P+(w+XC%(+UcOrk|yzkEes=!qW%imu6>zjdb!B#`efaliKtN}_c!Jp zfyZa`n+Nx8;*AquvMT2;c8fnYszdDA*0(R`bsof1W<#O{v%O!1IO4WZe=>XBu_D%d zOwWDaEtX%@B>4V%f1+dKqcXT>m2!|&?}(GK8e&R=&w?V`*Vj)sCetWp9lr@@{xe6a zE)JL&;p}OnOO}Nw?vFyoccXT*z*?r}E8{uPtd;4<(hmX;d$rqJhEF}I+kD+m(ke;J z7Cm$W*CSdcD=RYEBhedg>tuT{PHqwCdDP*NkHv4rvQTXkzEn*Mb0oJz&+WfWIOS4@ zzpPJ|e%a-PIwOaOC7uQcHQ-q(SE(e@fj+7oC@34wzaBNaP;cw&gm{Z8yYX?V(lIv5 zKbg*zo1m5aGA4^lwJ|bAU=j3*d8S{vp!~fLFcK8s6%Ng55_qW_d*3R%e=34aDZPfD z&Le39j|ahp6E7B0*9OVdeMNrTErFatiE+=Z!XZ^tv0y%zZKXRTBuPyP&C{5(H?t)S zKV24_-TKpOmCPzU&by8R1Q5HY^@IDoeDA9MbgizgQ*F1Er~HVmvSU>vx}pZVQ&tr| zOtZl8vfY2#L<)gZ=ba&wG~EI*Vd?}lRMCf+!b5CDz$8~be-HKMo5omk$w7p4`Mym*IR8WiTz4^kKcUo^8Hkcsu14u z`Pkg`#-Y^A%CqJ0O@UF|caAulf68@(zhqp~YjzInh7qSN7Ov%Aj(Qz%{3zW|xubJ- ztNE_u_MO7Q_585r;xD?e=Er}@U1G@BKW5v$UM((eByhH2p!^g9W}99OD8VV@7d{#H zv)Eam+^K(5>-Ot~U!R$Um3prQmM)7DyK=iM%vy>BRX4#aH7*oCMmz07YB(EL!^%F7?CA#>zXqiYDhS;e?LYPTf(bte6B ztrfvDXYG*T;ExK-w?Knt{jNv)>KMk*sM^ngZ-WiUN;=0Ev^GIDMs=AyLg2V@3R z7ugNc45;4!RPxvzoT}3NCMeK$7j#q3r_xV(@t@OPRyoKBzHJ#IepkDsm$EJRxL)A* zf{_GQYttu^OXr$jHQn}zs$Eh|s|Z!r?Yi+bS-bi+PE*lH zo|6ztu6$r_?|B~S#m>imI!kQP9`6X426uHRri!wGcK;J;`%sFM(D#*Le~W*t2uH`Q z(HEO9-c_`mhA@4QhbW+tgtt9Pzx=_*3Kh~TB$SKmU4yx-Ay&)n%PZPKg#rD4H{%Ke zdMY@rf5EAFfqtrf?Vmk&N(_d-<=bvfOdPrYwY*;5%j@O6@O#Qj7LJTk-x3LN+dEKy+X z>~U8j3Ql`exr1jR>+S4nEy+4c2f{-Q!3_9)yY758tLGg7k^=nt<6h$YE$ltA+13S<}uOg#XHe6 zZHKdNsAnMQ_RIuB;mdoZ%RWpandzLR-BnjN2j@lkBbBd+?i ze*!5mC}!Qj(Q!rTu`KrRRqp22c=hF6<^v&iCDB`n7mHl;vdclcer%;{;=kA(PwdGG zdX#BWoC!leBC4);^J^tPkPbIe<)~nYb6R3u{HvC!NOQa?DC^Q`|_@ zcz;rk`a!4rSLAS>_=b@g?Yab4%=J3Cc7pRv8?_rHMl_aK*HSPU%0pG2Fyhef_biA!aW|-(( z*RIdG&Lmk(=(nk28Q1k1Oa$8Oa-phG%Mc6dT3>JIylcMMIc{&FsBYBD^n@#~>C?HG z*1&FpYVvXOU@~r2(BUa+KZv;tZ15#RewooEM0LFb>guQN;Z0EBFMFMZ=-m$a3;gVD z)2EBD4+*=6ZF?+)P`z@DOT;azK0Q4p4>NfwDR#Pd;no|{q_qB!zk1O8QojE;>zhPu z1Q=1z^0MYHo1*``H3ex|bW-Zy==5J4fE2;g6sq6YcXMYK5i|S^9(OSw#v!3^!EB<% zZF~J~CleS`V-peStyf*I%1^R88D;+8{{qN6-t!@gTARDg^w2`uSzFZbPQ!)q^oC}m zPo8VOQxq2BaIN`pAVFGu8!{p3}(+iZ`f4ck2ygVpEZMQW38nLpj3NQx+&sAkb8`}P3- zc>N*k6AG?r}bfO6_vccTuKX+*- z7W4Q#2``P0jIHYs)F>uG#AM#I6W2)!Nu2nD5{CRV_PmkDS2ditmbd#pggqEgAo%5oC?|CP zGa0CV)wA*ko!xC7pZYkqo{10CN_e00FX5SjWkI3?@XG}}bze!(&+k2$C-C`6temSk z_YyYpB^wh3woo`B zrMSTd4T?(X-jh`FeO76C(3xsOm9s2BP_b%ospg^!#*2*o9N;tf4(X9$qc_d(()yz5 zDk@1}u_Xd+86vy5RBs?LQCuYKCGPS;E4uFOi@V%1JTK&|eRf~lp$AV#;*#O}iRI2=i3rFL8{ zA^ptDZ0l6k-mq=hUJ0x$Y@J>UNfz~I5l63H(`~*v;qX`Z{zwsQQD-!wp0D&hyB8&Z z7$R07gIKGJ^%AvQ{4KM0edM39iFRx=P^6`!<1(s0t|JbB2tXs_B_IH9#ajH0C=-n+ z`nz`fKMBKLlf?2AC+|83M+0rqR%uhNGD;uKA6jOjp7YDe^4%0fRB<^bcjlS2KF~F; zu09wh1x0&4pG&76M;x8$u`b134t=dEPBn6PV|X29<#T4F1mxGF*HOgiWU8tN@cguI z_F@o+XL7FJztR63wC|j4x_DANzcX94r7Iz-O2x$({&qd*mdLG=-Rv)uZ}UlMR+F&q zU}=lkfb0p1>1Ho){o$@}mSKIV;h*$AND7~Dl)QzpFBlSM99Kx+F7GsVK5xcR? z_4Q(Z%cgk8ST}U;;=!LwyZVu^S$>B-Waeik%wzcKTIqeX=0FP(TGQ=nxi=dsS5BYF zl@?}NT!Y!Iyos^@v7XWXA{_bV~1lxz7gC?xuXxy0_?GaN!AhRRM5>)^t%&ODd;@HN5L{MD3 zc>i2keQZVm#?NrDwbfd}_<*5^U&w0zv~n-y8=GGN-!=_`FU^cM8oVCWRFxw?BM^YD zi=Vxz4q|jwPTg+?q7_XI)-S@gQkh>w0ZUB}a{^ z_i;`Y(~fvpI!vmW*A^|P7(6+@C4UeL2WATf{P1?H5rk`5{TL zcf!CgP6Mi{MvjZS)rfo7JLDZK7M7ANd$3`{j9baD*7{#Zu-33fOYUzjvtKzR2)_T1I1s7fe&z|=)QkX;=`zX8!Byw-veM#yr;|wjO^II>!B*B z0+w%;0(=*G3V@88t!}~zx)&do(uF=073Yeh*fEhZb3Vn>t!m(9p~Y_FdV3IgR)9eT z)~e9xpI%2deTWyHlXA(7srrfc_`7ACm!R>SoIgkuF8 z!wkOhrixFy9y@)GdxAntd!!7@=L_tFD2T5OdSUO)I%yj02le`qeQ=yKq$g^h)NG;# za(0J@#VBi^5YI|QI=rq{KlxwGabZJ0dKmfWDROkcM}lUN$@DV`K7fU?8CP2H23QPi zG?YF*=Vn=kTK*#Y_{AQN&oLju|0#E=fx%YVh>S{puu&K$b;BN*jIo@VYhqPiJPzzM>#kxoy0vW9i;ne2_BIG0zyRFp<3M(iY(%*M_>q0ulV2K}Tg zkG{EWKS{i%4DUuHi%DVKy%e+Q!~Uf`>>F6NgD{{I8~nO4!VgOvtFOc7(O)X`|7n*f zxBa4CJ-v9fUUH+`7sPVvpM_C*udZ@OTGTzx56QM5y~OlrZc&w9=)B?nmd@keRn+^= zvm~4sa5987LFDnU{(N|N zJAR8H@}p1fC+H(yTI4n#%~TbImMpuqYn9cQ<0QQ%=PzZItLkC*ef9WJUvfITKWh#D zc#__8`4am9%#NslIUw+<82#SR8AYG|woLfBg#!-&dqq}@P>|I0%lbdy0lSMmNe+}o zj0zZuFr6Wb?Y{Qy-S=|r`bdrDmhnmvkRnkdn`YCleU>Q$=je}LGhh>_QAj6aa_0Oc z%Swsmui;IRx7bN*=AAS@5yW&Y2hy;3&|HAiA8}!HT6!Z!RVn~MZg`RmI6&%#tBZDx zfD+y@Z~NWlk*4l13vmt3AK2wP!fQlnBbECL>?p)F?T)<`w&QN>cP_V>r7UTcsTaaP zTOb$f!P@zf$6>890NVKbIkG8rE?9!Y97sMSZjfF?A zYR8lp`LMoz~O?iaZN;gcX;LC-%Ia*R%A&SLx!YIf29?P+=XAAojK8!^OU*@?R&DK!#G_lsn!#;S375uZ&B0HH1|BO0R90$U>qs zSvHv>H~mAgNCcjo-e+;RjY6B9NCbQrZ|BHjTkehaU<9CSkdd>Vl*ifA2LNOP&R2Qdy3k3-TQ+ zbq=#vI43x`s=%~cGyN&y4Y!FxhwgDe@i6uv8^BLL&3z*SO=D0aLjih?gY4-9uWp5or)H+v~w6n5X#F-I52z=Z_p4JB(;M| zeaVFhuR2|3UD2MzVc~^nSoD2(dD#uL_1PdnIxeA{V5n`#3xf1Zx@4lw(DsQ&H$h zw#%3O<1173hjg2_nhKi!d1ej=h7y`hVjCNB6|HTnx>SWuCE-kgTnfT+YGX4_Lun({ zDv2`>d3vrS)tTf7ps_vvh!Cx^e1BFuWnEAh0(7fkNk|-3oU|iRWdsC6U)?Raft~HN z;^$U}vZK5O8|LV$>6X5T(uYkblv{zwPxnQBh(BQ5tA~J!vGiAMYP^_ki~pkIxDfOZ zUJDwq%O~WueeV6%uN<54&u*c&E4y431cklBNrb06zGOOy4XNT~JS-q(s6@)F@ovbe ze`fial(O4(-su%6@@1+V0MsdLLMyE8;)nou(7}czU(5ASaZYDT(kUZ0L(&g$nF^n9 z9-Pi`ZZLX&)^*M6As4_2Mmc9S7OT)F8KkL2NJ)KJcnCuWU=Wy402A&45#Q9Id~BBH z0cY*xlv!uXzKrXLH!xQu(OtJvEj|0-DmRj1vjFz{c*I4$Pe(+_V|^b~S!0xm{8lq= zZv)@NlcyL3Xdz+*|L137F7y6L-2VsrKw=q^S>F6i%<{Fr8zk06$Ay-(!L$fY@7mcng!2}L0t zgi|KxfB63Xtk_Q8#ZPipQ@!zgjdpEIbK_?q17Hoi4Eiyun$hrc>T(7pOLVLQE=lgGwA+A308p& z7@=09(|$>eLy5gLe{*|3b(M;1n;C^~v?o88jYib48eR4$QGsBFzd}3QuwO^_XE(=B zq+hMi0UFC|dB{LCwch7;zYT=NK})O%sgi0k#yV;My@24^B1+CuZmYOh0^b)5Ba_)) zC%i#_Iev&nsu%I|1N5=MVc#PrlunKAs&hY|3s5;@}`>sB>}gzxuB zB=2vrRyB3uiyW(hkDUNe1@&(b`;>ZvGgw|@s{zVC#_`HXIN_^J@Etb zA7A+F?ot37T{<-vTy8h&b3e+WKHE1oh;pUQrN4yRRrx?mT_9jRa2i4l1fUnLW^Cbl z!I1>VzyFe?VELWWhM?@?t-YPZkD-Qjo@bC2(o#ZtZmr{KZsdFWItV`rs$gp{724@C zL8K5}E0+DHcWcL^{BGei4>@J-3%a#$y6;I}=upc};-NDv-z#kPX26ylOpH)Ov1uU{ zkLj6oiH6l_s+B~_z;|Jc2oi?naS7#3H63~~lWj4rUnd=fCnKdkik<@R&kch9q##G{ z4u!%=rlM~Yp3jk*t8}1B`Sv6<%Z^}~1e@aq zg|JQ`QO2pSjAm-g*?IrNc$^~sIrNBo2$m|Sxanr?Mfs>2@Auu49 zGXlsS<9XS1&8h(dD*Hl&5HBDG!^pJ*lkau_Ur+7`7z;rcs$hT4we?3bT=7Fe<>{5( z2m2(c+hUz2BTHM8dCe*Z3XX&Av;b~a=$6EF>&^E8%nyxO@m_n!q&XD^A{SRjRZQ0L~qDeC=j&0$j6=LNIz@`ni^>ch|sv}^6 zlm>?28yPl@WmDPR?Y-A9X{U9Dv_IsbXJnzKCjkRksLOg#42uG2mE_acbTQ4)J|1V>%U@K(FP3AYhL0U zdeOCPN1qLv!|#c=p!_+%VNV(GHt`RuLRV^vz<5tt-r)yOK**kUWPspVAf|}ZL{LS= z@k(@@!P&W!>wwe`x{+GrFSWhHov7hu?{KuuT%kl#WO@*WX$i_@retlhQBj++SVNCx z5$78LxP>Z=^aJ)D280r_jj=zFfMJFXCIe^B{~V@d1rl_F(qo&AB4bC-vYL>x2jSKX zpuTG-6kgp3e^T&+dtV*i6a~)v@n?n*MffN59y}<0djUX zt27R+SE#hp8bzc#;rk$jw3r4)Q@eI$*`_)=Pvge8@8|8>H3X)<9YX6cXa=ii#Le;(qKm@%0-7$>2ShnYc`j#zJ7gu_FE^?uAkL|H)UIH#gPu^40!6^J=^ zr`}iwa^!4tzW~vOMZAaKF>*8A{^8m$i(VK)>?=#l`xrVe>wseSvM_aF zATNkY>kM_P3?1kE`uIq#mvr-wuTgUH0N<&JhF=(E9%^NS*HLm!4GZ4_XI zL=R5tlG5Mk_1rPfg)sk^llFuKPMPBhuU|L5q#yP_mzxp1o&pAzi-X31sgFpIHn@($ z_>=`AB5(8tP6p2zS5VEvH5J$M` z_much3>S7t3Yo`Yx!>83-hW9LYzDKP?mKdkD#QAK8*M((sx{eBQdrR<^3ZhFP81+& zBnJMUefQyNBji~$5d88Wfw1Lv59aJN9t2!pABLg;ewJ#LXL-10;QcJl+Y4Mtngb)k6JZlCf)3uD_u)J3sYyN;NN5hNbg$%W!i-GK%e&!Us)2IExWSss$YG(hm3kJ-h%yD z>8q^n$+4I(_y_mbT{du4P%h1j3oSpjhY97{+IZ`aA4ug!vNJ6*p?<2H(2w+GD3j$I z1TUXGyNzdf>_yB3grP~FZUs<2Quw;eEi*7s(-MiIkQ%@J^+WGdQvYSUN+TRiD-xto zJ=OUU+kxGYc!HCLNbCvR4lGTp~#L;DFzGd-#gJe*xf(P3hDQz|y)?b9mwU3WUVnpcqXM<@w%r-k*Wr^gzAv)8T^sqA=Ye z!7qy&exJmAcAt~CwS#@yNmjr8*T*!A6w4~E*ibaLRs0CFo(;R3=ODhDt6zWNodmo0 zXx&bT$6&+5c>a|WJ)F4G-^GjY0H#*tY=UNyYr_q5fsrcjk(c^~e*7Lf`!Jd`)p412 zn|^*hV= zFI4UbwA%X@smDd$cQOiMC%jfitTxTb+#`9`G=2rJDfK!E=5ra|So>lc{X1$~w28i+ z4p&cTGwZ#5VueiXS9O8#;RR$yg7tL9!^)Sz&pZYIzlSh}0}V{LxL$Cu%B4U5_}k}- zm~|CsD<076x@<>m=6w6N?WaThIBP`!u{-;WF)xc=2otx*lwf|5+MkdJePjh(B z9SH+%cHGCMAXNxB{_3^otDWdsV7Ob6n{0 z+&!(;iaHOX__5z_$Qk{%xYV%Ig@7iokGBwR`3642ZP#H#v9QGbWl8<|MS*=@qO@Uj z6+SZ_v9`1paUe5tFN~v(b#J3a_Lx0+;r9giZIx-A5TxdbG>xi#AZ5_z1V}B^n)sxT zz49}eK7EWb6wR!6-qQOrHQHkUvshvq%=G2d&@(#XM*Am1;WbnJ{X_!a{ZkphD$^TQ z=Iskb&}=lBm(RHiwJoGg`*NiQ6#RB$T#LF+>#ef;Jne&MxKPX!#r`&TVEFsp2jnNx>dClzpcPy&G&13a_<0qaR3i+k212~hoQ z8nMk{JP-t04I{GW5gUBqcJW-jSMrlw}>p)ptx?WKuCUV77taMiV zHok9V=6yv+Uts@fMY&A}amC=!Yj}eL@=e%XJ#%?agkt1jWF+10{(E9mHLDa>Ll7Vj zG=3cp%ljIB-6pC}6&`xJ*6WCP|IlglLWJ^?yviI8Ve)?V_i4%n;olzny62_`-|IGi z^=}p_O>Z8M;c4|RExu70E7ePW(HWVS&E$+LL6xSQgB`QfMQJ|4pCTFowA39p5P-|$ zUtM_H2HnP8_RoS~Vwk(FhbG zH41licj%=0a;Ln2STFBvU}Ne&O&%8bYKj!h1FA#sNM`232fX|U3QPp#3C?mN2;hE9 z;)!@5ixSPl<89^7gwhHc2YAX1KJK$#*3`KOMIQ253q7-*RJ5k)zp9GBO|Ga~X*^}US5oN@aG&waHV%vi~r{t^`ptTxb zL}q1W8S7*>7oWwvgV4uFLZ(@k`R*=LO_|Gu`prs~!WQXj-NLIa^2(7IHg>BG^N zc|i{-^=&Cek9dkJFQys|sjG9i>LLz|;yCv{^1i%c*h>8zF91kLvS9HBQi~ZU!JL`B zK8N+U0fr1*6??Ium)AF!6tc1eGhXIYL6IRT7rmKp7+>?%5Pa6zC5)KY$ycF0ZJ`G5nEQDG100U-jLkH8^UE4g6wq?sg%pP=-$&G#bcN`^?w3a6 z((s$6eRKcSEIslW-kk5Qi|5Mg-(xdLF}PxxVh$PuO}#aR6pW1kV4Af!Bqh*btXNNZ z>-4(IUl+L4dw+3LcpGut=qB45O+W)Q5?*zZ2A6rJcg`qkSvWA!j^r2mqKuCm6`Py? z@^T#Ux04HemPGd!Hs7NkZdVn1}8_j`o?)*OKZGS!`ff)gF zG?v-lj$wWNWCcw2Mg2o18D~1?3_b0XzdiKBNkYSDpcv@&kp0POmweJE2ZkIQ3B!a! zIgIoE+Xv?;34kyo^QYjZk+tEqZvq^#QG(OzX4~X+KtsoQoddTWUR(yo8R+ObEF1j<-syWOb>)JQ&Zbdu(sctU%Mt zW&YR0{ttY2TTXYZ?~WNU&cES1Z2q(7SrWDh``!J(JM+Nk$!hu&Y;(7E`ZNKTe0w+% zJc?Qnw2B+%UR}0;cB0Rufa(7-3FF}?629@LgTiEC&2uyL6NxexOp?AKT^aAx3gi(W zao>r>MPw0eQ3>IV02uLsC@>yK_epX6GRg4{NEL2wPPF9=*L2RV3yyK8DhuEK>rmmV z`&Q~#c`lgR&93TdOCja|ewOXmPNRh7!&dMT(1ett#iDr8HZW~VqWW@7fe9B6;7S+? zbC`d4@MEau&mKlOPKd>*10q0c{~^baw6!a*w^sY#0Xim{oOsiXiDOhbG&kl3c$$n1 zMRrD83&QucDSEcV*7LIp8VTA@F<%qe+_c`L;6on(>SjAU^}5c9!BCffT>$VQhe=)z z8(=Ej{5>jhmjB3{xDfj2R@VmHQ!CqjlO4KnuOmvHy3K#po$yp_V;p_MKjh1`(rzj6 zHW956k1yvntz{_g?Xbs`avK(IjlTnsu%htO;D7 z?J#x^EzuvVn&NA=!MEj7cwe5A-Z$Zk2LBZH$~%E* zf`((xH0?`}hs|HA%mtwfOEsZJxxrennkTYcwP#FKO5%Lpc^JXhSpV|ZH$Wr;`}`_( zIP==gd3LYyVtwD|*ZJGi{7~x8{=^bGVqu0RJ`n_BZH9+}kz%-4ZRsImi@rx%=ZEKs zcPnUXo6hbJV>fH;@1|bAHIe0ijYI*&kdT|HkDS$9No9 zCHo=*HWb~U+Dtzxr+Esao}6@|;Pf+E$ay0$kQp#s{wlw+7aIKbMdf`OqhoG*;Tco0 zjrP}VQG#Y2cJuqoJg&5({)S(BA}q9T1lGeWRyu=Je|)I!6a+aj!IP^1({)ZYe&x6w zt3a)Dq^TB+A7CdB0-}#z2Ur$W&h3YVw8==!xONy$uQmDWh-@15iEOt!q2m&?ZLA|w z8loSb(0}7y6Xu0?M5Uf4>VZGluB`wMf2oh;m)ghxVda>3m}4%V)r^0nVQ5V6f3>*) z0&VN!N0~GC^P}vj$`EDMZEmVV;N&RISY2C;$0;2(<{Lt&PKzqRByQdiEHGAbwtbS zPj`Da5%U6k1oEtVzI}QNw;!hT6F+~|@=c@$C4NtO@=xgP?|5MyZAyuCzcvq4rdAv@C06%gZ`9%I);R6UGiGJobfux+<0DLS&|MSG4UH z_~o{^^9>ixMg~mY!-@Fai{xaE4^;qy9iZN15Gbn5ZqHWf>Jc5Rv6(#n8`1NcCsdmG zab*dSXVPaE?)wCalD;$ivF%@nB#7D`@YG04p6ed9m}4iJW|pfVMLE<-c{=-8$e?cH zUdU#mCj4gb zZKA^b9p*9S(}8@tw~1RNPHr7tQr;P+-)D8|sq=*o)G%RGqt> zzP5yf`pVxb)I51D_G~Xp^GNK zVI6sAX)a9s)e{8N3?35YA6aQTXuyszK3ah~CemzA&CII#8F&F#KN41~8I^&_%}6MCNb{W87qAF`zj_Y^szhb> z3p3}KbOxotY|(lD=;)`fYE_*{S}x;f^SW#)SU&5X#o|-R|trpa|L5PS5aa0 zTHw8%SDSVtU4?vyrhnq+^@dgFS)|(y{~(4j%3UEiO-rBM9%`)8(dh33pMLiuurNY# z#10AsQ7%*0Cu_DSAU}P;X(JwA64~Q_^R%d_zSm^6Aux?Pn70PM>9EvLeOX z&w9c)pGmcL22;MO3C_B>=NC0RJpMp8?#ZUf=GWRvy z6RHq3B}=MGVg?9@iKFBpsvnkVh3{Vpp=`CcD=u~@ql{my|6?3ssi3mCOPnjI&E}VC zc@X+Yl>;;DNo0W0`0th!X{?luDhOC{E8N=?!w}K1{V=)+1={m(f`Oc|N=07>}3;z{-(A zm{JL=j?Sro5iecmE2-pWlRf(r%|HEQ7kgwQ9+kt=NBhtQI7OwcZ#3%$Uf%^r2nhjY zoQ08MfC%_X{O9~WcirMZMhn#z^ux4Erx-tf-6bHD)9eH&^L>^jvAd^9A^DCDs?0;k zkm7LE*KjP6`2d17MrQaaLqd_Rka}J$csvUec#hw78<=s(hyR>065~YCVCA9+#Q+; za(*L0IEw!r5P|@-;x33L$Lv9 zcuN8YG&g{<(SeJG18~(b!5yywSqQiLAX0;---;}mF5&b4lg|T?LwKREa{9YX_-zL@ZE?Zqi@HxK^2KO1>0LATu{te=T zprmHtY)bDVfxI1S}KBE7V zznP7KQ8HekWU#W6mw`dr-boV}pMQR==&5=Q5T=_q091jfc;R*jX#&=MQ%~@E@9^?`$v48ks<>(fI(F6L(5ppKy|$HWng*bKOb(4|cMUB&z$#ob#XV z5-mg)gmFIybZf=znm3ZPyUO^GJfxt0kmHjaTZ|sthsxXw&}Y)fOUSg=JhRSR^UjZ- zhqqb}Wsyw4zdnj6@#BAJa#-PdI4_dgafFXh85DsEQ_cT+5)XpZq$fZlBA_9UsE9r6 zEFec5?uqN@QhJ^IzwZrwl-5J`CmVPv{(YDTqEqWR^dI;5hXc~cxP%B3v&~s0`Ct89 z@S`i~a^c%V^N81dDT*ItFS*&IN;@O$EgzX0e7x&}TD=!zS}hTpezBLS>mdX(5< z)8DEI(-o_D)c-UX@dA1MuJ*yc>Hf4|`*B2S_O>w*-tbUwtiu`;W(Ud{HTty@(&x(T(F&;M zJ=?H>6`B7nf-90e8V`WSVp|0oEKB-P2M{}4ZDawzvM&a!y>`Y#jCsD%T_l``@ah(I2nJs~Q|%uSKu@k!m~*8B*IoA{*TgtF<(5sHCGG;n@NE%~Xt(G$^&<87u;}Na zx-8cq0g`uA(&RBFo=-4Y1GUZ<``Zw{xL4jfHkZw~%~wvtGueszcXt)_QwH8g!; z%s&3kSa~R$dO$-%L-)c@_hi7&>{6L_M>OZFkUQu;{sL_bUMStNrt{{&O(Wn~*zPOk zB>dnfszb29NSTf2pqIs68k|p-UrSrxgLHqi?3N-UFa!LHy9n1)=s>`yS+J{MEzS@ zNlfGtpma7kG&LR3JE@wB%rFA*h~~KitlO=IP)ZjN6dQLM6qsry zHkB#cyNh#n`)}bCrN1My*;k)^@>e4gJ`LJK?2)Pwp?4Tl4)4FA0(tvY+#1jOUM)xw zlMz4x-f@g^+yKUN`?Vu)|AwujArnM~Pa@y*Q9S8eS(u{-S%(Z5=R~pRl5ZGDjdqH% zC8rW&{##wOpU_oTIG4WXMk4&%2t1;lWcW5&!yxmOT*!hBcKyTqEcNoO+R2;Q?Yj+W z1-Y4?59fijz4(MIDwGe4-baYf08UCs;r|YefD-Md2ST;=cxwpgW=tR76-dQVAhn^= zG9Wk5lQk%jIR@KNU!UMp6@BfU;r+;y4VQ)D2!Il9HX%yW-9nOzV+m$YKzVaO`B8S7t z$!S2Mz`xw>V(RjE`0>bQp<0y&h~Y=M#jpy!#=dE>`=e_AjSZq6u!Dy1xJf~-7|0F! zPR9|n`e_7D2DIV2H(CESQ}hA>U>n|6`%z?YKEA~)BOVY%y=jPV zT=44R!L?J)736X#csn|lfBJ)o8ixaZclguWgrGO<`TN2FMfO}7;5}d+BlK0yTSH3* z4!=;5rOh85&2|x=46hkNaz?)U8&=bcfh=N_#8BNpZ2v$aVBo;sk^*X`v;4-LU;D>! zM*h12MxXIQy)SfAqE4;jY)wgnppazZkdNNVVF;(PLf^qK$FgY9+VFyBKE7UC|f z`R|?&egV11K3s$rJ6!GvoeW=jV*!-e(wA;x(2=d0E_e_%0x--0o8#~m^H1%AH5Z^B zn!TNPn927*bvaf0pt}zhK0o^V@WlGwwKo(*nQ|Q~4_;>~-8y20`HP>@UJa)3nEnGG z5Hwhs|FcmFG16ZVNb5hL`2Gc1{zWIMM{_OiKewV!hCi}U!VuE?s9wU-QbZ!)+Y^tS zGzp5OSi5iq6hmEr$w}&9DFgoB+i*`q`8TBi^MVS{SKEb8Aw%@K7@XCo(De2A`6%mf&a2#~y1N)+kJLD$1HCP!22)(U}xo2|j?WRzt(11j8Z_*v;P$R+Ug*Gy3VxV4K; zGGUGabnW*`Z}~`ydXL-l9e=GC$pY#z|63vy>E*m=$=j}iWP{sRTh0%H54`t>2xYH% zsk+M&u&pNgMCM@3e)Xc?jBWX-TIR_cQ1Z!RW7!B zBjZX=+^3}?SE)B+$EP+0oi1Fp5blDT?*}nsP>filqXH{ms zxU<$hetC`u)Wi+x|EKL-`y^#aQX+sDYIa{M;V%LqLrOk~lR>u0Q!+pyQSU4zY`?E^ z|5@)C)w6G_=i5YYC5SE_u(7hDNYr}uKT|@DSqF%S++lTIbIk^$a>{~0IH8KNFEy%+ zW#$&!ynpgNJh>6uR~?2c)ZMW+h0OKu231(7L_vETPaR+(P)Zy%0~yGm>E9?@@x!Jy z3PYgS}Q@b}x}E#F27@F+j}0=&Ql4gES&f8acMrPAVlVs9$97`FR))R5wI zc&}KFI1UIewh>3PkhnB7u zS3AT8_*|nexznG|Z*DU0c!K@jsI4J)5#DyNi#|e#`l1Vv1`1)*NVcy0LZ``aL0n8B zecupJ(rhq3u8bW0NIRhKYq$v1li+jp*4hfAd&wxYDE8vn1TQ7S@bTM|I2Ob z8vMOIxA7&_j{AKmD+O@EyXT`|dElt0pED^@IV0m)RPBUs*5jW60>>w1!@_G3aBKzG z_f(KfAPBk}-jQtR*Sroq!*3rbQ_m27e+YdzQjUb<_*k8vc_C)y!@cj5E>NxUhPu&g z@Z2<~esU`)ih+4opWe+K7sbN9n*9@n>#@n3*o z?xoROgDuvhq>jJ;Ve{6i<3roQNfgo5^4Q4(|GNExO2Dr7GjgA2zWuKp_K)K0R(6lv z!l$!zW-+T6mb3gQaAFviTQi{|*t%>{(mhTdy+y;Re4qT@kccy#{b z&zWy~kLO@>*WPj2k#H)|7L&gAJ37DmHQAme#@m;(Y8Nu^`D5vf8sZFW#+lA2!HK=( zJ)#hO6JD*`o~&c*&46d}g=Qj@SsoB5ikC z^1V8E+&<-OzuS_C`p5<<(A6fB`LXT(!kV^0_~hL6PpW4={l%|#xgdh?5EIk~lu8{D z2hiyhv3Yxij_#$Wu>P@7SYsl`-~3;}Ktx{34_NL^Kwin&=?!HDv3elQDbcU*qyYpN z(#yw~f1vFGK-t%CC-qa-4FYHbA^h>bag-I&*qaxwn?Qv|idE$<>1H|Gr6JtUu(he2$eg!N z@HTF@dG1)*y;4fxe)4_ZkpaBHH9hXp9p4|gLrRQyuevRd@gSS}JhRnWqrvm|U@>qM z=yl7RQROTKwQtzP3!zUF)_6Ld#NGA6v~2{J9Dd`h6{%+XsU#qGLh%`fB1Hc?wfayK zN`H4BpDp)npVQuu$DVW1qsBS&AJ2eP%6Qw>;k{)Z$8%HL=Q4(a$Ng2_vHw&vA!1L+9zc8vaX2GtqJ{L-;gvF0IR$em zMQ8@{Qp3+3Quk)TJ$?I<8KmwzD*7#(q<@Mc`dchngW}cRG14(Z6K7{T|LhFXwhqUQ;BET;cYqPcAcMgt6M$V9$(?jHo@Sud$an$U&5F zZ1QNh^ztt)E*d#Ij;<43oSKKnd+WNr$_r}+s_O_x6DZSB10*5Q{ourqq>mTl| zx4y^(cy+9;t@R=*j>3_dmm_m)$k$#937V(sllby&5)Xex^UD-|m|q<(jEd#@DV(of zAd7sSdmS*zUDqJ9|K%O2J2OfdUiK{{b{PCy)pi<;hp~7v1CQj&4-10 zgO<3dqhYH1#-Fa}Q{pjql5>>P6gZH21zLfxZ4$SK4T@7b!|`nWF9b*84Bq8&Eht;9 z*P72x&NUCZ7*@B$`FtE=hz5b}S`|c6Ey+j@D1ZibjJaRlR;{cxAWv z?Nqa>QqV*H-*zzaPvpLMHt~nl(x6?vrPpR?zn7~wow?oj*1TKmx4j71>$hvtC$DLD zUrz0^tiP0792U&dxJxNv@r}Elsjn^aSLUu=9#mD{&9n8|ayIL$!H3s>%KEvbchBFW z%cd?VU83mGF#Dar9*s~w&AnmQRQIOvR+uWsuZ?+|a=TzApXO@q^(r%8=}iv#wCnFq z=K9}JbqU@k99Q%j-}NNk+qLCP)jXfmOO|)@?mHcnynd6({mJisP1_}u7k)|eYHXWK z63eQ)E$ufFi!3CWUY2gw%e>omCv}qEX66aH-k&35f9`Q@Us|NPetVqe8=dX*VxJdn ze`q7b=Dn(UA(2sf&g)cOmQFhNJ#<-aMELJZbA#@to>25@kbW<)&!X01 z%NMJt>1ST)tyX)h@?`DxhbgCHr>S4wv}WC&Nw-!{+Z7$2D}74QAcXTvip=M0%Tp_N zor=k`)t|ra^ySr-+(|R9mB(E=`MX#y(wSw)$!iymzB;^c*>%&^*7HxTnRga=soSZT zdDl+9s;r!v8hk6POtzBaig4pRp7eWF(<8gufvNHPu6xs-=e{;mnHzJyGKE+8L0j}; z@%8-e^UCL5HhMiR>sD3Rve&yVZ#{Q1*CO8c+qSr^Z#CN;)(X5>tGG5yUw3<+CfhaL z%bP;hZ?jvgJU67BWyiy74_)6r)_nSxttxn0`0?HE^5(uydHVgP+HE$V?Lv)Leti43 zWA|;f-RqX``95>)^P-fw!Vi{3KNsII-*5f){gdxqd%gVdB1sOBNe=nEW%;i~g_P8J w!5uhoe-Jcg1nPN%MiEAtgE$;km@@t6ukO)1^!cY^83Pb_y85}Sb4q9e0FIsP9{>OV diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/poc/flutter_rust_bridge_hello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfddf3d9dade342351e627a0a75609fb46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/AppInfo.xcconfig b/poc/flutter_rust_bridge_hello/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 9334c14..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// 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_rust_bridge_hello - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterRustBridgeHello - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Debug.xcconfig b/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Release.xcconfig b/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Warnings.xcconfig b/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -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/poc/flutter_rust_bridge_hello/macos/Runner/DebugProfile.entitlements b/poc/flutter_rust_bridge_hello/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a3..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/poc/flutter_rust_bridge_hello/macos/Runner/Info.plist b/poc/flutter_rust_bridge_hello/macos/Runner/Info.plist deleted file mode 100644 index 4789daa..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - 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/poc/flutter_rust_bridge_hello/macos/Runner/MainFlutterWindow.swift b/poc/flutter_rust_bridge_hello/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -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/poc/flutter_rust_bridge_hello/macos/Runner/Release.entitlements b/poc/flutter_rust_bridge_hello/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a..0000000 --- a/poc/flutter_rust_bridge_hello/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/poc/flutter_rust_bridge_hello/macos/RunnerTests/RunnerTests.swift b/poc/flutter_rust_bridge_hello/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1..0000000 --- a/poc/flutter_rust_bridge_hello/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -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/poc/flutter_rust_bridge_hello/pubspec.lock b/poc/flutter_rust_bridge_hello/pubspec.lock deleted file mode 100644 index b5956de..0000000 --- a/poc/flutter_rust_bridge_hello/pubspec.lock +++ /dev/null @@ -1,315 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - 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 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" - source: hosted - version: "1.0.9" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_driver: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_rust_bridge: - dependency: "direct main" - description: - name: flutter_rust_bridge - sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a - url: "https://pub.dev" - source: hosted - version: "2.12.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - integration_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - process: - dependency: transitive - description: - name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" - source: hosted - version: "5.0.5" - rust_lib_flutter_rust_bridge_hello: - dependency: "direct main" - description: - path: rust_builder - relative: true - source: path - version: "0.0.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - sync_http: - dependency: transitive - description: - name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" - source: hosted - version: "15.2.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - webdriver: - dependency: transitive - description: - name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" - url: "https://pub.dev" - source: hosted - version: "3.1.0" -sdks: - dart: ">=3.11.5 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" diff --git a/poc/flutter_rust_bridge_hello/pubspec.yaml b/poc/flutter_rust_bridge_hello/pubspec.yaml deleted file mode 100644 index f5c06a5..0000000 --- a/poc/flutter_rust_bridge_hello/pubspec.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: flutter_rust_bridge_hello -description: "A new Flutter project." -# 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.11.5 - -# 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 - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - rust_lib_flutter_rust_bridge_hello: - path: rust_builder - flutter_rust_bridge: 2.12.0 - -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: ^6.0.0 - integration_test: - sdk: flutter - -# 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: - # - images/a_dot_burr.jpeg - # - 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/poc/flutter_rust_bridge_hello/rust/.gitignore b/poc/flutter_rust_bridge_hello/rust/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/poc/flutter_rust_bridge_hello/rust/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/poc/flutter_rust_bridge_hello/rust/Cargo.lock b/poc/flutter_rust_bridge_hello/rust/Cargo.lock deleted file mode 100644 index 6699ef6..0000000 --- a/poc/flutter_rust_bridge_hello/rust/Cargo.lock +++ /dev/null @@ -1,763 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -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 = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[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.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "bytemuck" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[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 = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[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", - "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 = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "flutter_rust_bridge" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0884853aae8a6517b5b58cf36f55da487f2fe110e1686938eb29b6640aae4a5" -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.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce32f35f710ced8c5aa557f023f1a624e737b5460cee2b70fcd3a8df09e1b" -dependencies = [ - "hex", - "md-5", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" - -[[package]] -name = "futures-executor" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" - -[[package]] -name = "futures-macro" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" - -[[package]] -name = "futures-task" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" - -[[package]] -name = "futures-util" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[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.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[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.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[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_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 = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[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 = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[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.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "rust_lib_flutter_rust_bridge_hello" -version = "0.1.0" -dependencies = [ - "flutter_rust_bridge", - "thiserror", - "tokio", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[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 = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[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 = "tokio" -version = "1.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d" -dependencies = [ - "backtrace", - "num_cpus", - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "web-sys" -version = "0.3.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/poc/flutter_rust_bridge_hello/rust/Cargo.toml b/poc/flutter_rust_bridge_hello/rust/Cargo.toml deleted file mode 100644 index 0251364..0000000 --- a/poc/flutter_rust_bridge_hello/rust/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "rust_lib_flutter_rust_bridge_hello" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "staticlib"] - -[dependencies] -flutter_rust_bridge = "=2.12.0" -thiserror = "2" -tokio = { version = "1", features = ["rt-multi-thread", "time", "macros"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/poc/flutter_rust_bridge_hello/rust/src/api/mod.rs b/poc/flutter_rust_bridge_hello/rust/src/api/mod.rs deleted file mode 100644 index b252f36..0000000 --- a/poc/flutter_rust_bridge_hello/rust/src/api/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod simple; diff --git a/poc/flutter_rust_bridge_hello/rust/src/api/simple.rs b/poc/flutter_rust_bridge_hello/rust/src/api/simple.rs deleted file mode 100644 index 77b8b8f..0000000 --- a/poc/flutter_rust_bridge_hello/rust/src/api/simple.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Chanora PoC API — `flutter_rust_bridge_hello`. -//! -//! Two flows are proven here: -//! 1. **Command:** `greet` — typed input → typed `Result` DTO, -//! including typed error mapping at the boundary. -//! 2. **Event stream:** `counterStream` — Rust pushes typed events to -//! Dart via a `StreamSink`, observed in Dart as a `Stream`. -//! -//! Together they satisfy the PoC exit criterion from -//! `docs/architecture/proof-of-concept-plan.md` §2: -//! "Flutter can call Rust and receive event stream data." -//! -//! This is PoC code only. The Chanora DTO catalogue belongs to the -//! `chanora_bridge` crate; this PoC does not stand in for it. - -use std::time::Duration; - -use flutter_rust_bridge::frb; - -use crate::frb_generated::StreamSink; - -// -------- Command DTOs -------- - -#[derive(Debug, Clone)] -pub struct GreetResult { - pub message: String, - pub elapsed_micros: u64, -} - -#[derive(Debug, Clone)] -pub enum GreetErrorKind { - EmptyName, -} - -/// Typed error DTO surfaced as a Dart exception by FRB. -#[derive(Debug, Clone, thiserror::Error)] -#[error("greet error: {kind:?}: {detail}")] -pub struct GreetError { - pub kind: GreetErrorKind, - pub detail: String, -} - -// -------- Lifecycle -------- - -#[frb(init)] -pub fn init_app() { - flutter_rust_bridge::setup_default_user_utils(); -} - -// -------- Command -------- - -/// Synchronous fallible command. Proves Dart → Rust call with a typed -/// `Result` DTO crossing the boundary. -#[frb(sync)] -pub fn greet(name: String) -> Result { - let start = std::time::Instant::now(); - let trimmed = name.trim(); - if trimmed.is_empty() { - return Err(GreetError { - kind: GreetErrorKind::EmptyName, - detail: "name must not be empty".into(), - }); - } - Ok(GreetResult { - message: format!("Hello, {trimmed}! — greetings from Rust."), - elapsed_micros: start.elapsed().as_micros() as u64, - }) -} - -// -------- Event stream -------- - -#[derive(Debug, Clone)] -pub struct CounterTick { - pub seq: u64, - pub note: String, -} - -/// Rust → Dart event stream. -/// `StreamSink` is provided in scope by `flutter_rust_bridge` glue; -/// the codegen wires it to a Dart `Stream`. -pub async fn counter_stream( - sink: StreamSink, - count: u32, - interval_ms: u32, -) { - let interval = Duration::from_millis(interval_ms.max(1) as u64); - for i in 0..count { - let tick = CounterTick { - seq: i as u64, - note: format!("tick {i}/{count}"), - }; - if sink.add(tick).is_err() { - break; - } - tokio::time::sleep(interval).await; - } -} diff --git a/poc/flutter_rust_bridge_hello/rust/src/frb_generated.rs b/poc/flutter_rust_bridge_hello/rust/src/frb_generated.rs deleted file mode 100644 index 5175640..0000000 --- a/poc/flutter_rust_bridge_hello/rust/src/frb_generated.rs +++ /dev/null @@ -1,558 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. - -#![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::uninlined_format_args, - clippy::needless_borrow -)] - -// Section: imports - -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.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1685941124; - -// Section: executor - -flutter_rust_bridge::frb_generated_default_handler!(); - -// Section: wire_funcs - -fn wire__crate__api__simple__counter_stream_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: "counter_stream", - 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_sink = >::sse_decode(&mut deserializer); - let api_count = ::sse_decode(&mut deserializer); - let api_interval_ms = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, ()>( - (move || async move { - let output_ok = Result::<_, ()>::Ok({ - crate::api::simple::counter_stream( - api_sink, - api_count, - api_interval_ms, - ) - .await; - })?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__simple__greet_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "greet", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - 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_name = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, crate::api::simple::GreetError>((move || { - let output_ok = crate::api::simple::greet(api_name)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__simple__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::simple::init_app(); - })?; - Ok(output_ok) - })()) - } - }, - ) -} - -// Section: dart2rust - -impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { - // 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::anyhow::anyhow!("{}", inner); - } -} - -impl SseDecode - for StreamSink -{ - // 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 StreamSink::deserialize(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 crate::api::simple::CounterTick { - // 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_seq = ::sse_decode(deserializer); - let mut var_note = ::sse_decode(deserializer); - return crate::api::simple::CounterTick { - seq: var_seq, - note: var_note, - }; - } -} - -impl SseDecode for crate::api::simple::GreetError { - // 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_kind = ::sse_decode(deserializer); - let mut var_detail = ::sse_decode(deserializer); - return crate::api::simple::GreetError { - kind: var_kind, - detail: var_detail, - }; - } -} - -impl SseDecode for crate::api::simple::GreetErrorKind { - // 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 match inner { - 0 => crate::api::simple::GreetErrorKind::EmptyName, - _ => unreachable!("Invalid variant for GreetErrorKind: {}", inner), - }; - } -} - -impl SseDecode for crate::api::simple::GreetResult { - // 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_message = ::sse_decode(deserializer); - let mut var_elapsedMicros = ::sse_decode(deserializer); - return crate::api::simple::GreetResult { - message: var_message, - elapsed_micros: var_elapsedMicros, - }; - } -} - -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 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::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -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 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__simple__counter_stream_impl(port, ptr, rust_vec_len, data_len), - 3 => wire__crate__api__simple__init_app_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 { - 2 => wire__crate__api__simple__greet_impl(ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} - -// Section: rust2dart - -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::simple::CounterTick { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.seq.into_into_dart().into_dart(), - self.note.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::simple::CounterTick -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::simple::CounterTick -{ - fn into_into_dart(self) -> crate::api::simple::CounterTick { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::simple::GreetError { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.kind.into_into_dart().into_dart(), - self.detail.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::simple::GreetError -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::simple::GreetError -{ - fn into_into_dart(self) -> crate::api::simple::GreetError { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::simple::GreetErrorKind { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - Self::EmptyName => 0.into_dart(), - _ => unreachable!(), - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::simple::GreetErrorKind -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::simple::GreetErrorKind -{ - fn into_into_dart(self) -> crate::api::simple::GreetErrorKind { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::simple::GreetResult { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.message.into_into_dart().into_dart(), - self.elapsed_micros.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::simple::GreetResult -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::simple::GreetResult -{ - fn into_into_dart(self) -> crate::api::simple::GreetResult { - self - } -} - -impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(format!("{:?}", self), serializer); - } -} - -impl SseEncode - for StreamSink -{ - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - unimplemented!("") - } -} - -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 crate::api::simple::CounterTick { - // 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.seq, serializer); - ::sse_encode(self.note, serializer); - } -} - -impl SseEncode for crate::api::simple::GreetError { - // 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.kind, serializer); - ::sse_encode(self.detail, serializer); - } -} - -impl SseEncode for crate::api::simple::GreetErrorKind { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode( - match self { - crate::api::simple::GreetErrorKind::EmptyName => 0, - _ => { - unimplemented!(""); - } - }, - serializer, - ); - } -} - -impl SseEncode for crate::api::simple::GreetResult { - // 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.message, serializer); - ::sse_encode(self.elapsed_micros, serializer); - } -} - -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 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 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 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.12.0. - - // Section: imports - - use super::*; - 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!(); -} -#[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.12.0. - - // Section: imports - - use super::*; - 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!(); -} -#[cfg(target_family = "wasm")] -pub use web::*; diff --git a/poc/flutter_rust_bridge_hello/rust/src/lib.rs b/poc/flutter_rust_bridge_hello/rust/src/lib.rs deleted file mode 100644 index cbb071f..0000000 --- a/poc/flutter_rust_bridge_hello/rust/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod api; -mod frb_generated; diff --git a/poc/flutter_rust_bridge_hello/rust_builder/.gitignore b/poc/flutter_rust_bridge_hello/rust_builder/.gitignore deleted file mode 100644 index ac5aa98..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -build/ diff --git a/poc/flutter_rust_bridge_hello/rust_builder/README.md b/poc/flutter_rust_bridge_hello/rust_builder/README.md deleted file mode 100644 index 922615f..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/README.md +++ /dev/null @@ -1 +0,0 @@ -Please ignore this folder, which is just glue to build Rust with Flutter. \ No newline at end of file diff --git a/poc/flutter_rust_bridge_hello/rust_builder/android/.gitignore b/poc/flutter_rust_bridge_hello/rust_builder/android/.gitignore deleted file mode 100644 index 161bdcd..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/poc/flutter_rust_bridge_hello/rust_builder/android/build.gradle b/poc/flutter_rust_bridge_hello/rust_builder/android/build.gradle deleted file mode 100644 index 35f28a1..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/android/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// The Android Gradle Plugin builds the native code with the Android NDK. - -group 'com.flutter_rust_bridge.rust_lib_flutter_rust_bridge_hello' -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.rust_lib_flutter_rust_bridge_hello' - } - - // 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 android.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdkVersion 19 - } -} - -apply from: "../cargokit/gradle/plugin.gradle" -cargokit { - manifestDir = "../../rust" - libname = "rust_lib_flutter_rust_bridge_hello" -} diff --git a/poc/flutter_rust_bridge_hello/rust_builder/android/settings.gradle b/poc/flutter_rust_bridge_hello/rust_builder/android/settings.gradle deleted file mode 100644 index 88671e7..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'rust_lib_flutter_rust_bridge_hello' diff --git a/poc/flutter_rust_bridge_hello/rust_builder/android/src/main/AndroidManifest.xml b/poc/flutter_rust_bridge_hello/rust_builder/android/src/main/AndroidManifest.xml deleted file mode 100644 index 5891961..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/.gitignore b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/.gitignore deleted file mode 100644 index cf7bb86..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -.dart_tool -*.iml -!pubspec.lock diff --git a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/LICENSE b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/LICENSE deleted file mode 100644 index d33a5fe..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/README b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/README deleted file mode 100644 index 398474d..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/README +++ /dev/null @@ -1,11 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_pod.sh b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_pod.sh deleted file mode 100755 index ed0e0d9..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_pod.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/README.md b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/README.md deleted file mode 100644 index a878c27..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/README.md +++ /dev/null @@ -1,5 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/analysis_options.yaml b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/analysis_options.yaml deleted file mode 100644 index 0e16a8b..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/analysis_options.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/bin/build_tool.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/bin/build_tool.dart deleted file mode 100644 index 268eb52..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/bin/build_tool.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/build_tool.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/build_tool.dart deleted file mode 100644 index 7c1bb75..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/build_tool.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/android_environment.dart deleted file mode 100644 index 15fc9ee..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/android_environment.dart +++ /dev/null @@ -1,195 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart deleted file mode 100644 index e608cec..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart +++ /dev/null @@ -1,266 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart deleted file mode 100644 index 6f3b2a4..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart +++ /dev/null @@ -1,40 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart deleted file mode 100644 index 7e61fcb..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart +++ /dev/null @@ -1,49 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_pod.dart deleted file mode 100644 index 8a9c0db..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_pod.dart +++ /dev/null @@ -1,89 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_tool.dart deleted file mode 100644 index 70dfe0e..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/build_tool.dart +++ /dev/null @@ -1,276 +0,0 @@ -/// 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', - ) - ..addOption( - 'glibc-version', - help: 'GLIBC version to use for linux builds', - ) - ..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?, - glibcVersion: argResults!['glibc-version'] 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/builder.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/builder.dart deleted file mode 100644 index cd5269f..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/builder.dart +++ /dev/null @@ -1,209 +0,0 @@ -/// 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; - - final String? glibcVersion; - - 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, - this.glibcVersion, - }); - - 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); - } - if (environment.glibcVersion != null) { - rustup.installZigBuild(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', - (target.android == null && environment.glibcVersion != null) - ? 'zigbuild' - : 'build', - ...extraArgs, - '--manifest-path', - manifestPath, - '-p', - environment.crateInfo.packageName, - if (!environment.configuration.isDebug) '--release', - '--target', - target.rust + - ((target.android == null && environment.glibcVersion != null) - ? '.${environment.glibcVersion!}' - : ""), - '--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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/cargo.dart deleted file mode 100644 index 0d8958f..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/cargo.dart +++ /dev/null @@ -1,48 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart deleted file mode 100644 index 0c4d88d..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart +++ /dev/null @@ -1,124 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/environment.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/environment.dart deleted file mode 100644 index 996483a..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/environment.dart +++ /dev/null @@ -1,68 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/logging.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/logging.dart deleted file mode 100644 index 5edd4fd..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/logging.dart +++ /dev/null @@ -1,52 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/options.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/options.dart deleted file mode 100644 index 22aef1d..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/options.dart +++ /dev/null @@ -1,309 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart deleted file mode 100644 index 019859c..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart +++ /dev/null @@ -1,205 +0,0 @@ -/// 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, - this.glibcVersion, - }); - - 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; - final String? glibcVersion; - - 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, - glibcVersion: glibcVersion, - ); - - 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/rustup.dart deleted file mode 100644 index e46722b..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/rustup.dart +++ /dev/null @@ -1,149 +0,0 @@ -/// 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); - } - - bool _didInstallZigBuild = false; - - void installZigBuild(String toolchain) { - if (_didInstallZigBuild) { - return; - } - - log.info("Installing Zig build"); - runCommand("rustup", [ - 'run', - toolchain, - 'cargo', - 'install', - '--locked', - 'cargo-zigbuild', - ]); - _didInstallZigBuild = true; - } - - 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/target.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/target.dart deleted file mode 100644 index 624504e..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/target.dart +++ /dev/null @@ -1,147 +0,0 @@ -/// 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: 'aarch64-pc-windows-msvc', - flutter: 'windows-arm64', - ), - Target( - rust: 'x86_64-unknown-linux-gnu', - flutter: 'linux-x64', - ), - Target( - rust: 'aarch64-unknown-linux-gnu', - flutter: 'linux-arm64', - ), - Target(rust: 'riscv64gc-unknown-linux-gnu', flutter: 'linux-riscv64'), - 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).trim(); - if (arch == 'aarch64') { - return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; - } else if (arch == 'riscv64') { - return [Target.forRustTriple('riscv64gc-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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/util.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/util.dart deleted file mode 100644 index 8bb6a87..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/util.dart +++ /dev/null @@ -1,172 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart deleted file mode 100644 index 2366b57..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart +++ /dev/null @@ -1,84 +0,0 @@ -/// 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.lock b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.lock deleted file mode 100644 index 343bdd3..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.lock +++ /dev/null @@ -1,453 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.yaml b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.yaml deleted file mode 100644 index 18c61e3..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/build_tool/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/cargokit.cmake b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/cargokit.cmake deleted file mode 100644 index ddd05df..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/cargokit.cmake +++ /dev/null @@ -1,99 +0,0 @@ -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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/resolve_symlinks.ps1 deleted file mode 100644 index 2ac593a..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/cmake/resolve_symlinks.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/gradle/plugin.gradle b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/gradle/plugin.gradle deleted file mode 100644 index 68ff649..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/gradle/plugin.gradle +++ /dev/null @@ -1,184 +0,0 @@ -/// 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" || plugin.class.name == "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 List platforms - try { - platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() - } catch (Exception ignored) { - platforms = plugin.getTargetPlatforms().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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.cmd b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.cmd deleted file mode 100755 index c45d0aa..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.cmd +++ /dev/null @@ -1,91 +0,0 @@ -@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/poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.sh b/poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.sh deleted file mode 100755 index 24b0ed8..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/cargokit/run_build_tool.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/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/poc/flutter_rust_bridge_hello/rust_builder/ios/Classes/dummy_file.c b/poc/flutter_rust_bridge_hello/rust_builder/ios/Classes/dummy_file.c deleted file mode 100644 index e06dab9..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/ios/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/poc/flutter_rust_bridge_hello/rust_builder/ios/rust_lib_flutter_rust_bridge_hello.podspec b/poc/flutter_rust_bridge_hello/rust_builder/ios/rust_lib_flutter_rust_bridge_hello.podspec deleted file mode 100644 index 6624e86..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/ios/rust_lib_flutter_rust_bridge_hello.podspec +++ /dev/null @@ -1,45 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint rust_lib_flutter_rust_bridge_hello.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'rust_lib_flutter_rust_bridge_hello' - 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 rust_lib_flutter_rust_bridge_hello', - :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}/librust_lib_flutter_rust_bridge_hello.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}/librust_lib_flutter_rust_bridge_hello.a', - } -end \ No newline at end of file diff --git a/poc/flutter_rust_bridge_hello/rust_builder/linux/CMakeLists.txt b/poc/flutter_rust_bridge_hello/rust_builder/linux/CMakeLists.txt deleted file mode 100644 index 0fafe58..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/linux/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# 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 "rust_lib_flutter_rust_bridge_hello") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../rust rust_lib_flutter_rust_bridge_hello "") - -# 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(rust_lib_flutter_rust_bridge_hello_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/poc/flutter_rust_bridge_hello/rust_builder/macos/Classes/dummy_file.c b/poc/flutter_rust_bridge_hello/rust_builder/macos/Classes/dummy_file.c deleted file mode 100644 index e06dab9..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/macos/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/poc/flutter_rust_bridge_hello/rust_builder/macos/rust_lib_flutter_rust_bridge_hello.podspec b/poc/flutter_rust_bridge_hello/rust_builder/macos/rust_lib_flutter_rust_bridge_hello.podspec deleted file mode 100644 index cd6c09d..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/macos/rust_lib_flutter_rust_bridge_hello.podspec +++ /dev/null @@ -1,44 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint rust_lib_flutter_rust_bridge_hello.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'rust_lib_flutter_rust_bridge_hello' - 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 rust_lib_flutter_rust_bridge_hello', - :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}/librust_lib_flutter_rust_bridge_hello.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}/librust_lib_flutter_rust_bridge_hello.a', - } -end \ No newline at end of file diff --git a/poc/flutter_rust_bridge_hello/rust_builder/pubspec.yaml b/poc/flutter_rust_bridge_hello/rust_builder/pubspec.yaml deleted file mode 100644 index 27e8fe9..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: rust_lib_flutter_rust_bridge_hello -description: "Utility to build Rust code" -version: 0.0.1 -publish_to: none - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - ffi: ^2.0.2 - ffigen: ^11.0.0 - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - ffiPlugin: true - ios: - ffiPlugin: true - linux: - ffiPlugin: true - macos: - ffiPlugin: true - windows: - ffiPlugin: true diff --git a/poc/flutter_rust_bridge_hello/rust_builder/windows/.gitignore b/poc/flutter_rust_bridge_hello/rust_builder/windows/.gitignore deleted file mode 100644 index b3eb2be..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/poc/flutter_rust_bridge_hello/rust_builder/windows/CMakeLists.txt b/poc/flutter_rust_bridge_hello/rust_builder/windows/CMakeLists.txt deleted file mode 100644 index 534a116..0000000 --- a/poc/flutter_rust_bridge_hello/rust_builder/windows/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# 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 "rust_lib_flutter_rust_bridge_hello") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../../../../../rust rust_lib_flutter_rust_bridge_hello "") - -# 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(rust_lib_flutter_rust_bridge_hello_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/poc/flutter_rust_bridge_hello/test/poc_verification_test.dart b/poc/flutter_rust_bridge_hello/test/poc_verification_test.dart deleted file mode 100644 index 28c585f..0000000 --- a/poc/flutter_rust_bridge_hello/test/poc_verification_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Same content as integration_test/simple_test.dart, but runnable via -// `flutter test` (flutter_tester, no display required). -// -// Reasoning: the PoC exit criterion is "Flutter can call Rust and receive -// event stream data". flutter_rust_bridge v2 uses dart:ffi on desktop, -// which works inside flutter_tester. A full integration_test driver run -// is not required to evidence the criterion. - -import 'package:flutter_rust_bridge_hello/src/rust/api/simple.dart'; -import 'package:flutter_rust_bridge_hello/src/rust/frb_generated.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - setUpAll(() async => await RustLib.init()); - - group('Chanora FRB hello PoC', () { - test('greet() returns typed result for valid input', () { - final r = greet(name: 'Chanora'); - expect(r.message, contains('Hello, Chanora!')); - expect(r.elapsedMicros.toInt(), greaterThanOrEqualTo(0)); - }); - - test('greet() surfaces typed error for empty input', () { - expect(() => greet(name: ' '), throwsA(isA())); - }); - - test('counterStream() delivers the expected event sequence', () async { - const count = 4; - final events = []; - await for (final tick in counterStream(count: count, intervalMs: 50)) { - events.add(tick); - } - expect(events, hasLength(count)); - for (var i = 0; i < count; i++) { - expect(events[i].seq.toInt(), i); - expect(events[i].note, 'tick $i/$count'); - } - }); - }); -} diff --git a/poc/flutter_rust_bridge_hello/test_driver/integration_test.dart b/poc/flutter_rust_bridge_hello/test_driver/integration_test.dart deleted file mode 100644 index b38629c..0000000 --- a/poc/flutter_rust_bridge_hello/test_driver/integration_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/poc/flutter_rust_bridge_hello/web/favicon.png b/poc/flutter_rust_bridge_hello/web/favicon.png deleted file mode 100644 index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM diff --git a/poc/flutter_rust_bridge_hello/web/icons/Icon-192.png b/poc/flutter_rust_bridge_hello/web/icons/Icon-192.png deleted file mode 100644 index b749bfef07473333cf1dd31e9eed89862a5d52aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 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 diff --git a/poc/flutter_rust_bridge_hello/web/icons/Icon-512.png b/poc/flutter_rust_bridge_hello/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48dff1169879ba46840804b412fe02fefd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8252 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 diff --git a/poc/flutter_rust_bridge_hello/web/icons/Icon-maskable-192.png b/poc/flutter_rust_bridge_hello/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e525556d5d89141648c724331630325d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/poc/flutter_rust_bridge_hello/web/icons/Icon-maskable-512.png b/poc/flutter_rust_bridge_hello/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20998 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 MPgg&ebxsLQ025IeI{*Lx diff --git a/poc/flutter_rust_bridge_hello/web/index.html b/poc/flutter_rust_bridge_hello/web/index.html deleted file mode 100644 index e772291..0000000 --- a/poc/flutter_rust_bridge_hello/web/index.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - flutter_rust_bridge_hello - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/web/manifest.json b/poc/flutter_rust_bridge_hello/web/manifest.json deleted file mode 100644 index 52e4d55..0000000 --- a/poc/flutter_rust_bridge_hello/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "flutter_rust_bridge_hello", - "short_name": "flutter_rust_bridge_hello", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/poc/flutter_rust_bridge_hello/windows/.gitignore b/poc/flutter_rust_bridge_hello/windows/.gitignore deleted file mode 100644 index d492d0d..0000000 --- a/poc/flutter_rust_bridge_hello/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/poc/flutter_rust_bridge_hello/windows/CMakeLists.txt b/poc/flutter_rust_bridge_hello/windows/CMakeLists.txt deleted file mode 100644 index 3a81dd5..0000000 --- a/poc/flutter_rust_bridge_hello/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(flutter_rust_bridge_hello 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_rust_bridge_hello") - -# 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/poc/flutter_rust_bridge_hello/windows/flutter/CMakeLists.txt b/poc/flutter_rust_bridge_hello/windows/flutter/CMakeLists.txt deleted file mode 100644 index 903f489..0000000 --- a/poc/flutter_rust_bridge_hello/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# 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/poc/flutter_rust_bridge_hello/windows/flutter/generated_plugins.cmake b/poc/flutter_rust_bridge_hello/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 759b511..0000000 --- a/poc/flutter_rust_bridge_hello/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - rust_lib_flutter_rust_bridge_hello -) - -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/poc/flutter_rust_bridge_hello/windows/runner/CMakeLists.txt b/poc/flutter_rust_bridge_hello/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -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/poc/flutter_rust_bridge_hello/windows/runner/Runner.rc b/poc/flutter_rust_bridge_hello/windows/runner/Runner.rc deleted file mode 100644 index f34fba3..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// 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_rust_bridge_hello" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "flutter_rust_bridge_hello" "\0" - VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "flutter_rust_bridge_hello.exe" "\0" - VALUE "ProductName", "flutter_rust_bridge_hello" "\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/poc/flutter_rust_bridge_hello/windows/runner/flutter_window.cpp b/poc/flutter_rust_bridge_hello/windows/runner/flutter_window.cpp deleted file mode 100644 index 955ee30..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/windows/runner/flutter_window.h b/poc/flutter_rust_bridge_hello/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/windows/runner/main.cpp b/poc/flutter_rust_bridge_hello/windows/runner/main.cpp deleted file mode 100644 index d52838a..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#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_rust_bridge_hello", 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/poc/flutter_rust_bridge_hello/windows/runner/resource.h b/poc/flutter_rust_bridge_hello/windows/runner/resource.h deleted file mode 100644 index 66a65d1..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{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/poc/flutter_rust_bridge_hello/windows/runner/resources/app_icon.ico b/poc/flutter_rust_bridge_hello/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20caf6370ebb9253ad831cc31de4a9c965f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33772 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK diff --git a/poc/flutter_rust_bridge_hello/windows/runner/runner.exe.manifest b/poc/flutter_rust_bridge_hello/windows/runner/runner.exe.manifest deleted file mode 100644 index 153653e..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,14 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - diff --git a/poc/flutter_rust_bridge_hello/windows/runner/utils.cpp b/poc/flutter_rust_bridge_hello/windows/runner/utils.cpp deleted file mode 100644 index 3a0b465..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/windows/runner/utils.h b/poc/flutter_rust_bridge_hello/windows/runner/utils.h deleted file mode 100644 index 3879d54..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/windows/runner/win32_window.cpp b/poc/flutter_rust_bridge_hello/windows/runner/win32_window.cpp deleted file mode 100644 index 60608d0..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#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/poc/flutter_rust_bridge_hello/windows/runner/win32_window.h b/poc/flutter_rust_bridge_hello/windows/runner/win32_window.h deleted file mode 100644 index e901dde..0000000 --- a/poc/flutter_rust_bridge_hello/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#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/poc/linux_voice_test/Cargo.lock b/poc/linux_voice_test/Cargo.lock deleted file mode 100644 index 43b20b7..0000000 --- a/poc/linux_voice_test/Cargo.lock +++ /dev/null @@ -1,1293 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.11.1", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "annotate-snippets" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" -dependencies = [ - "anstyle", - "unicode-width", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "annotate-snippets", - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[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.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -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 = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[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 = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" - -[[package]] -name = "coreaudio-rs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" -dependencies = [ - "bitflags 1.3.2", - "libc", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "cpal" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" -dependencies = [ - "alsa", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows", -] - -[[package]] -name = "ctrlc" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" -dependencies = [ - "dispatch2", - "nix", - "windows-sys 0.61.2", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.11.1", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[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 = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[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 = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -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 = "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", - "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.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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 = "libpulse-binding" -version = "2.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" -dependencies = [ - "bitflags 2.11.1", - "libc", - "libpulse-sys", - "num-derive", - "num-traits", - "winapi", -] - -[[package]] -name = "libpulse-simple-binding" -version = "2.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7bebef0381c8e3e4b23cc24aaf36fab37472bece128de96f6a111efa464cfef" -dependencies = [ - "libpulse-binding", - "libpulse-simple-sys", - "libpulse-sys", -] - -[[package]] -name = "libpulse-simple-sys" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd96888fe37ad270d16abf5e82cccca1424871cf6afa2861824d2a52758eebc" -dependencies = [ - "libpulse-sys", - "pkg-config", -] - -[[package]] -name = "libpulse-sys" -version = "1.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74371848b22e989f829cc1621d2ebd74960711557d8b45cfe740f60d0a05e61" -dependencies = [ - "libc", - "num-derive", - "num-traits", - "pkg-config", - "winapi", -] - -[[package]] -name = "libspa" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2909f3be29d674e7f10604aff18d1bbe1bb03c4cd61c8a8ba19c0b1d162f7d4e" -dependencies = [ - "bitflags 2.11.1", - "cc", - "cookie-factory", - "libc", - "libspa-sys", - "nom 8.0.0", - "rustix", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ad52764fca54818486f3cf75afec844d1f1a1568c24dcee25d41b1ab007dda" -dependencies = [ - "bindgen", - "cc", - "system-deps", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "linux_voice_test" -version = "0.1.0" -dependencies = [ - "cpal", - "ctrlc", - "libpulse-binding", - "libpulse-simple-binding", - "pipewire", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.11.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[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 = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-audio-toolbox" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" -dependencies = [ - "bitflags 2.11.1", - "libc", - "objc2", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" -dependencies = [ - "bitflags 2.11.1", - "objc2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "objc2", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pipewire" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8585aba8a52ad74ccc633b8e293c1dc4277976bd5d510b925533f34fd6685f38" -dependencies = [ - "bitflags 2.11.1", - "libc", - "libspa", - "libspa-sys", - "pipewire-sys", - "rustix", -] - -[[package]] -name = "pipewire-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2089f245b548723e60325773c27f586b7a2372c79ea941b246cd0d654706adc" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[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 = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[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 = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[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 = "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_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[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" - -[[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 = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[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 = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[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 = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[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.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.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" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - -[[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.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[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.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 = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] diff --git a/poc/linux_voice_test/Cargo.toml b/poc/linux_voice_test/Cargo.toml deleted file mode 100644 index 7e1f2c4..0000000 --- a/poc/linux_voice_test/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "linux_voice_test" -version = "0.1.0" -edition = "2021" - -[workspace] - -[dependencies] -ctrlc = "3.5" - -[target.'cfg(target_os = "linux")'.dependencies] -libpulse-binding = "2.30" -libpulse-simple-binding = "2.29" -pipewire = { version = "0.10", features = ["v0_3_44"] } - -[target.'cfg(target_os = "windows")'.dependencies] -cpal = "0.16" diff --git a/poc/linux_voice_test/README.md b/poc/linux_voice_test/README.md deleted file mode 100644 index 1f6491f..0000000 --- a/poc/linux_voice_test/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# linux_voice_test - -Purpose: prove a Linux-native voice loopback path that enumerates input/output -devices and can target PipeWire or PulseAudio directly instead of relying on -`cpal`'s generic ALSA path. - -Scope: -- enumerate Linux capture and playback devices through PipeWire or PulseAudio -- open a selected input and output device pair -- run local microphone-to-speaker loopback at 48 kHz mono f32 - -Out of scope: -- Chanora product integration -- Opus encode/decode -- jitter buffer, per-user playback controls, or persistent settings -- packaging and service lifecycle - -Current validation: -- `cargo check` succeeds on the Fedora-based host after isolating the PoC from - the workspace with a local `[workspace]` table -- `pkg-config --modversion libpipewire-0.3` and `pkg-config --modversion - libspa-0.2` both succeed on that host; `pipewire-0.3` is not the correct - module name -- `cargo run -- --list-devices` succeeds and lists real PipeWire devices on the - host - -Reproduction: - -```bash -cd poc/linux_voice_test -cargo check -cargo run -- --list-devices -cargo run -- --backend pipewire --input 0 --output 0 -``` - -See `VERIFICATION.md` for the exact commands and observed results captured in -this repository session. diff --git a/poc/linux_voice_test/VERIFICATION.md b/poc/linux_voice_test/VERIFICATION.md deleted file mode 100644 index 0e80bd6..0000000 --- a/poc/linux_voice_test/VERIFICATION.md +++ /dev/null @@ -1,59 +0,0 @@ -# linux_voice_test verification - -Date: 2026-05-25 -Host: Fedora-based Linux sandbox host - -## Commands - -```bash -pkg-config --modversion libpipewire-0.3 -pkg-config --modversion libspa-0.2 -cd poc/linux_voice_test -cargo check -cargo run -- --list-devices -``` - -## Observed results - -`pkg-config` resolved the product Linux capture dependencies successfully: - -```text -libpipewire-0.3 -> 1.6.5 -libspa-0.2 -> 0.2 -``` - -Note: `pipewire-0.3` is not the correct pkg-config module name for the Rust -PipeWire crates used here; the expected module is `libpipewire-0.3`. - -`cargo check` completed successfully after adding an empty `[workspace]` table -to `poc/linux_voice_test/Cargo.toml` so the PoC can build independently from -the root workspace. - -`cargo run -- --list-devices` completed successfully and printed: - -```text -Backend: pipewire -Input devices: - [0] Brio 500 Analog Stereo [43] - [1] Built-in Audio Analog Stereo [34] -Backend: pipewire -Output devices: - [0] Built-in Audio Digital Stereo (IEC958) [46] - [1] Navi 21/23 HDMI/DP Audio Controller Digital Stereo (HDMI 4) [50] -``` - -## What this does validate - -- PipeWire crate dependencies compile on this host -- the host exposes the exact `libpipewire-0.3` and `libspa-0.2` pkg-config - modules used by the product Linux capture backend -- PipeWire node enumeration works for both input and output -- the PoC can target real device node ids reported by the running desktop audio - graph - -## What this does not validate - -- PulseAudio fallback runtime on this host -- full microphone-to-speaker loopback quality over an extended session -- Chanora product audio-engine integration -- push-to-talk, VAD, Opus, jitter buffering, or per-user playback controls diff --git a/poc/linux_voice_test/src/audio_buffer.rs b/poc/linux_voice_test/src/audio_buffer.rs deleted file mode 100644 index e071ca2..0000000 --- a/poc/linux_voice_test/src/audio_buffer.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; - -#[cfg(target_os = "linux")] -pub const SAMPLE_RATE: u32 = 48_000; -#[cfg(target_os = "linux")] -pub const CHANNELS: u32 = 1; -#[cfg(target_os = "linux")] -pub const BUFFER_FRAMES: usize = 256; -pub const RING_CAPACITY_SAMPLES: usize = 48_000 * 2; - -pub type SharedBuffer = Arc>>; - -pub fn make_shared_buffer() -> SharedBuffer { - Arc::new(Mutex::new(VecDeque::with_capacity(RING_CAPACITY_SAMPLES))) -} - -pub fn push_sample(buffer: &SharedBuffer, sample: f32) { - let mut queue = buffer.lock().expect("audio buffer mutex poisoned"); - if queue.len() >= RING_CAPACITY_SAMPLES { - let _ = queue.pop_front(); - } - queue.push_back(sample); -} - -pub fn pop_sample(buffer: &SharedBuffer) -> f32 { - let mut queue = buffer.lock().expect("audio buffer mutex poisoned"); - queue.pop_front().unwrap_or(0.0) -} diff --git a/poc/linux_voice_test/src/linux_pipewire.rs b/poc/linux_voice_test/src/linux_pipewire.rs deleted file mode 100644 index 3521656..0000000 --- a/poc/linux_voice_test/src/linux_pipewire.rs +++ /dev/null @@ -1,318 +0,0 @@ -use crate::audio_buffer::{ - make_shared_buffer, pop_sample, push_sample, SharedBuffer, CHANNELS, SAMPLE_RATE, -}; -use crate::linux_shared::{ - sort_devices, wait_for_startup, LinuxAudioBackend, LinuxDevice, LinuxLoopbackSession, - ThreadedLoopbackSession, -}; -use pipewire as pw; -use pw::spa::pod::Pod; -use pw::{properties::properties, spa}; -use std::cell::{Cell, RefCell}; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; -use std::sync::{mpsc, Arc}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; - -pub struct PipeWireBackend; - -impl LinuxAudioBackend for PipeWireBackend { - fn name(&self) -> &'static str { - "pipewire" - } - - fn list_input_devices(&self) -> Result, String> { - list_nodes("Audio/Source") - } - - fn list_output_devices(&self) -> Result, String> { - list_nodes("Audio/Sink") - } - - fn start_loopback( - &self, - input: &LinuxDevice, - output: &LinuxDevice, - stop: Arc, - ) -> Result, String> { - let input_id = input - .id - .parse::() - .map_err(|_| format!("invalid PipeWire input node id `{}`", input.id))?; - let output_id = output - .id - .parse::() - .map_err(|_| format!("invalid PipeWire output node id `{}`", output.id))?; - - let buffer = make_shared_buffer(); - let capture = spawn_capture(buffer.clone(), input_id, stop.clone())?; - let playback = match spawn_playback(buffer, output_id, stop.clone()) { - Ok(handle) => handle, - Err(err) => { - stop.store(true, AtomicOrdering::SeqCst); - let _ = capture.join(); - return Err(err); - } - }; - - Ok(Box::new(ThreadedLoopbackSession { - capture, - playback, - stop, - })) - } -} - -fn list_nodes(class_prefix: &str) -> Result, String> { - let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?; - let context = pw::context::ContextRc::new(&mainloop, None).map_err(|err| err.to_string())?; - let core = context.connect_rc(None).map_err(|err| err.to_string())?; - let registry = core.get_registry().map_err(|err| err.to_string())?; - - let done = Rc::new(Cell::new(false)); - let devices = Rc::new(RefCell::new(Vec::::new())); - - let devices_for_registry = devices.clone(); - let class_prefix = class_prefix.to_string(); - let class_prefix_for_registry = class_prefix.clone(); - let _registry_listener = registry - .add_listener_local() - .global(move |global| { - if global.type_ != pw::types::ObjectType::Node { - return; - } - - let Some(props) = global.props else { - return; - }; - - let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { - return; - }; - - if !media_class.starts_with(class_prefix_for_registry.as_str()) { - return; - } - - let label = props - .get(*pw::keys::NODE_DESCRIPTION) - .or_else(|| props.get(*pw::keys::NODE_NICK)) - .or_else(|| props.get(*pw::keys::NODE_NAME)) - .unwrap_or("Unnamed PipeWire node") - .to_string(); - - devices_for_registry.borrow_mut().push(LinuxDevice { - id: global.id.to_string(), - label, - }); - }) - .register(); - - let pending = core.sync(0).map_err(|err| err.to_string())?; - let done_for_core = done.clone(); - let loop_for_core = mainloop.clone(); - let _core_listener = core - .add_listener_local() - .done(move |id, seq| { - if id == pw::core::PW_ID_CORE && seq == pending { - done_for_core.set(true); - loop_for_core.quit(); - } - }) - .error(move |_id, _seq, _res, message| { - eprintln!("PipeWire core error: {message}"); - }) - .register(); - - while !done.get() { - mainloop.run(); - } - - let mut devices = devices.borrow().clone(); - devices.sort_by(|left, right| sort_devices(left, right)); - - if devices.is_empty() { - return Err(format!("no PipeWire nodes found for {class_prefix}")); - } - - Ok(devices) -} - -fn spawn_capture( - buffer: SharedBuffer, - source_id: u32, - stop: Arc, -) -> Result>, String> { - let (ready_tx, ready_rx) = mpsc::sync_channel::>(1); - let handle = thread::spawn(move || { - let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?; - let context = - pw::context::ContextRc::new(&mainloop, None).map_err(|err| err.to_string())?; - let core = context.connect_rc(None).map_err(|err| err.to_string())?; - - let mut props = properties! { - *pw::keys::MEDIA_TYPE => "Audio", - *pw::keys::MEDIA_CATEGORY => "Capture", - *pw::keys::MEDIA_ROLE => "Communication", - }; - let target = source_id.to_string(); - props.insert(*pw::keys::TARGET_OBJECT, target.as_str()); - - let stream = pw::stream::StreamBox::new(&core, "linux-voice-test-capture", props) - .map_err(|err| err.to_string())?; - - let _listener = stream - .add_local_listener_with_user_data(buffer) - .process(|stream, queue| { - if let Some(mut buffer) = stream.dequeue_buffer() { - let datas = buffer.datas_mut(); - if datas.is_empty() { - return; - } - - let data_ref = &mut datas[0]; - let byte_count = data_ref.chunk().size() as usize; - let Some(bytes) = data_ref.data() else { - return; - }; - - for chunk in bytes[..byte_count].chunks_exact(4) { - let sample = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - push_sample(queue, sample); - } - } - }) - .register() - .map_err(|err| err.to_string())?; - - let values = build_format_param()?; - let mut params = [Pod::from_bytes(&values).ok_or("invalid PipeWire capture format pod")?]; - - stream - .connect( - spa::utils::Direction::Input, - None, - pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::MAP_BUFFERS - | pw::stream::StreamFlags::RT_PROCESS, - &mut params, - ) - .map_err(|err| err.to_string())?; - - let _ = ready_tx.send(Ok(())); - - while !stop.load(AtomicOrdering::SeqCst) { - let _ = mainloop - .loop_() - .iterate(pw::loop_::Timeout::Finite(Duration::from_millis(100))); - } - - mainloop.quit(); - Ok(()) - }); - - wait_for_startup(ready_rx, handle, "PipeWire capture") -} - -fn spawn_playback( - buffer: SharedBuffer, - sink_id: u32, - stop: Arc, -) -> Result>, String> { - let (ready_tx, ready_rx) = mpsc::sync_channel::>(1); - let handle = thread::spawn(move || { - let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?; - let context = - pw::context::ContextRc::new(&mainloop, None).map_err(|err| err.to_string())?; - let core = context.connect_rc(None).map_err(|err| err.to_string())?; - - let mut props = properties! { - *pw::keys::MEDIA_TYPE => "Audio", - *pw::keys::MEDIA_CATEGORY => "Playback", - *pw::keys::MEDIA_ROLE => "Communication", - }; - let target = sink_id.to_string(); - props.insert(*pw::keys::TARGET_OBJECT, target.as_str()); - - let stream = pw::stream::StreamBox::new(&core, "linux-voice-test-playback", props) - .map_err(|err| err.to_string())?; - - let _listener = stream - .add_local_listener_with_user_data(buffer) - .process(|stream, queue| { - if let Some(mut buffer) = stream.dequeue_buffer() { - let datas = buffer.datas_mut(); - if datas.is_empty() { - return; - } - - let data_ref = &mut datas[0]; - let byte_len = { - let Some(bytes) = data_ref.data() else { - return; - }; - - for chunk in bytes.chunks_exact_mut(4) { - chunk.copy_from_slice(&pop_sample(queue).to_le_bytes()); - } - - bytes.len() - }; - let chunk = data_ref.chunk_mut(); - *chunk.offset_mut() = 0; - *chunk.stride_mut() = 4; - *chunk.size_mut() = byte_len as u32; - } - }) - .register() - .map_err(|err| err.to_string())?; - - let values = build_format_param()?; - let mut params = [Pod::from_bytes(&values).ok_or("invalid PipeWire playback format pod")?]; - - stream - .connect( - spa::utils::Direction::Output, - None, - pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::MAP_BUFFERS - | pw::stream::StreamFlags::RT_PROCESS, - &mut params, - ) - .map_err(|err| err.to_string())?; - - let _ = ready_tx.send(Ok(())); - - while !stop.load(AtomicOrdering::SeqCst) { - let _ = mainloop - .loop_() - .iterate(pw::loop_::Timeout::Finite(Duration::from_millis(100))); - } - - mainloop.quit(); - Ok(()) - }); - - wait_for_startup(ready_rx, handle, "PipeWire playback") -} - -fn build_format_param() -> Result, String> { - let mut audio_info = spa::param::audio::AudioInfoRaw::new(); - audio_info.set_format(spa::param::audio::AudioFormat::F32LE); - audio_info.set_rate(SAMPLE_RATE); - audio_info.set_channels(CHANNELS); - - let object = pw::spa::pod::Object { - type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(), - id: pw::spa::param::ParamType::EnumFormat.as_raw(), - properties: audio_info.into(), - }; - - pw::spa::pod::serialize::PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &pw::spa::pod::Value::Object(object), - ) - .map_err(|err| err.to_string()) - .map(|serializer| serializer.0.into_inner()) -} diff --git a/poc/linux_voice_test/src/linux_pulseaudio.rs b/poc/linux_voice_test/src/linux_pulseaudio.rs deleted file mode 100644 index 1b3bf54..0000000 --- a/poc/linux_voice_test/src/linux_pulseaudio.rs +++ /dev/null @@ -1,309 +0,0 @@ -use crate::audio_buffer::{ - make_shared_buffer, pop_sample, push_sample, SharedBuffer, BUFFER_FRAMES, CHANNELS, SAMPLE_RATE, -}; -use crate::linux_shared::{ - sort_devices, wait_for_startup, LinuxAudioBackend, LinuxDevice, LinuxLoopbackSession, - ThreadedLoopbackSession, -}; -use libpulse_binding as pulse; -use libpulse_simple_binding as psimple; -use pulse::callbacks::ListResult; -use pulse::context::{self, Context}; -use pulse::mainloop::threaded::Mainloop; -use pulse::proplist::Proplist; -use pulse::sample::{Format, Spec}; -use pulse::stream::Direction; -use std::cell::{Cell, RefCell}; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; -use std::sync::{mpsc, Arc}; -use std::thread::{self, JoinHandle}; - -pub struct PulseAudioBackend; - -impl LinuxAudioBackend for PulseAudioBackend { - fn name(&self) -> &'static str { - "pulseaudio" - } - - fn list_input_devices(&self) -> Result, String> { - with_pulse_context(|context, mainloop| { - let done = Rc::new(Cell::new(false)); - let devices = Rc::new(RefCell::new(Vec::::new())); - - let done_for_cb = done.clone(); - let devices_for_cb = devices.clone(); - let mainloop_for_cb = mainloop.clone(); - let introspector = context.borrow().introspect(); - let _operation = introspector.get_source_info_list(move |result| match result { - ListResult::Item(info) => { - if info.monitor_of_sink.is_some() { - return; - } - - let Some(name) = info.name.as_ref() else { - return; - }; - - let label = info - .description - .as_ref() - .map(|value| value.to_string()) - .unwrap_or_else(|| name.to_string()); - - devices_for_cb.borrow_mut().push(LinuxDevice { - id: name.to_string(), - label, - }); - } - ListResult::End | ListResult::Error => { - done_for_cb.set(true); - unsafe { (*mainloop_for_cb.as_ptr()).signal(false) }; - } - }); - - while !done.get() { - mainloop.borrow_mut().wait(); - } - - let mut devices = devices.borrow().clone(); - devices.sort_by(|left, right| sort_devices(left, right)); - - if devices.is_empty() { - return Err("no PulseAudio sources found".to_string()); - } - - Ok(devices) - }) - } - - fn list_output_devices(&self) -> Result, String> { - with_pulse_context(|context, mainloop| { - let done = Rc::new(Cell::new(false)); - let devices = Rc::new(RefCell::new(Vec::::new())); - - let done_for_cb = done.clone(); - let devices_for_cb = devices.clone(); - let mainloop_for_cb = mainloop.clone(); - let introspector = context.borrow().introspect(); - let _operation = introspector.get_sink_info_list(move |result| match result { - ListResult::Item(info) => { - let Some(name) = info.name.as_ref() else { - return; - }; - - let label = info - .description - .as_ref() - .map(|value| value.to_string()) - .unwrap_or_else(|| name.to_string()); - - devices_for_cb.borrow_mut().push(LinuxDevice { - id: name.to_string(), - label, - }); - } - ListResult::End | ListResult::Error => { - done_for_cb.set(true); - unsafe { (*mainloop_for_cb.as_ptr()).signal(false) }; - } - }); - - while !done.get() { - mainloop.borrow_mut().wait(); - } - - let mut devices = devices.borrow().clone(); - devices.sort_by(|left, right| sort_devices(left, right)); - - if devices.is_empty() { - return Err("no PulseAudio sinks found".to_string()); - } - - Ok(devices) - }) - } - - fn start_loopback( - &self, - input: &LinuxDevice, - output: &LinuxDevice, - stop: Arc, - ) -> Result, String> { - let buffer = make_shared_buffer(); - let capture = spawn_capture(buffer.clone(), input.id.clone(), stop.clone())?; - let playback = match spawn_playback(buffer, output.id.clone(), stop.clone()) { - Ok(handle) => handle, - Err(err) => { - stop.store(true, AtomicOrdering::SeqCst); - let _ = capture.join(); - return Err(err); - } - }; - - Ok(Box::new(ThreadedLoopbackSession { - capture, - playback, - stop, - })) - } -} - -fn with_pulse_context(operation: F) -> Result -where - F: FnOnce(&Rc>, &Rc>) -> Result, -{ - let proplist = build_proplist()?; - let mainloop = Rc::new(RefCell::new( - Mainloop::new().ok_or("failed to create PulseAudio mainloop")?, - )); - let context = Rc::new(RefCell::new( - Context::new_with_proplist(&*mainloop.borrow(), "linux-voice-test", &proplist) - .ok_or("failed to create PulseAudio context")?, - )); - - let mainloop_for_cb = mainloop.clone(); - let context_for_cb = context.clone(); - context - .borrow_mut() - .set_state_callback(Some(Box::new(move || { - let state = unsafe { (*context_for_cb.as_ptr()).get_state() }; - match state { - context::State::Ready | context::State::Failed | context::State::Terminated => { - unsafe { (*mainloop_for_cb.as_ptr()).signal(false) }; - } - _ => {} - } - }))); - - context - .borrow_mut() - .connect(None, context::FlagSet::NOFLAGS, None) - .map_err(pa_error)?; - - mainloop.borrow_mut().lock(); - mainloop.borrow_mut().start().map_err(pa_error)?; - - loop { - match context.borrow().get_state() { - context::State::Ready => break, - context::State::Failed | context::State::Terminated => { - mainloop.borrow_mut().unlock(); - mainloop.borrow_mut().stop(); - return Err(format!( - "PulseAudio context state {:?}", - context.borrow().get_state() - )); - } - _ => mainloop.borrow_mut().wait(), - } - } - - context.borrow_mut().set_state_callback(None); - - let result = operation(&context, &mainloop); - mainloop.borrow_mut().unlock(); - mainloop.borrow_mut().stop(); - result -} - -fn spawn_capture( - buffer: SharedBuffer, - source_name: String, - stop: Arc, -) -> Result>, String> { - let (ready_tx, ready_rx) = mpsc::sync_channel::>(1); - let handle = thread::spawn(move || { - let spec = pulse_spec(); - let stream = psimple::Simple::new( - None, - "linux-voice-test", - Direction::Record, - Some(source_name.as_str()), - "linux-voice-test-record", - &spec, - None, - None, - ) - .map_err(pa_error)?; - - let _ = ready_tx.send(Ok(())); - - let mut bytes = vec![0_u8; BUFFER_FRAMES * std::mem::size_of::()]; - while !stop.load(AtomicOrdering::SeqCst) { - stream.read(&mut bytes).map_err(pa_error)?; - for chunk in bytes.chunks_exact(4) { - let sample = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - push_sample(&buffer, sample); - } - } - - stream.flush().map_err(pa_error)?; - Ok(()) - }); - - wait_for_startup(ready_rx, handle, "PulseAudio capture") -} - -fn spawn_playback( - buffer: SharedBuffer, - sink_name: String, - stop: Arc, -) -> Result>, String> { - let (ready_tx, ready_rx) = mpsc::sync_channel::>(1); - let handle = thread::spawn(move || { - let spec = pulse_spec(); - let stream = psimple::Simple::new( - None, - "linux-voice-test", - Direction::Playback, - Some(sink_name.as_str()), - "linux-voice-test-playback", - &spec, - None, - None, - ) - .map_err(pa_error)?; - - let _ = ready_tx.send(Ok(())); - - let mut bytes = vec![0_u8; BUFFER_FRAMES * std::mem::size_of::()]; - while !stop.load(AtomicOrdering::SeqCst) { - for chunk in bytes.chunks_exact_mut(4) { - chunk.copy_from_slice(&pop_sample(&buffer).to_le_bytes()); - } - stream.write(&bytes).map_err(pa_error)?; - } - - stream.drain().map_err(pa_error)?; - Ok(()) - }); - - wait_for_startup(ready_rx, handle, "PulseAudio playback") -} - -fn build_proplist() -> Result { - let mut proplist = Proplist::new().ok_or("failed to create PulseAudio proplist")?; - proplist - .set_str( - pulse::proplist::properties::APPLICATION_NAME, - "linux-voice-test", - ) - .map_err(|_| "failed to set PulseAudio application name".to_string())?; - Ok(proplist) -} - -fn pulse_spec() -> Spec { - let spec = Spec { - format: Format::F32le, - channels: CHANNELS as u8, - rate: SAMPLE_RATE, - }; - debug_assert!(spec.is_valid()); - spec -} - -fn pa_error(err: pulse::error::PAErr) -> String { - err.to_string() - .unwrap_or_else(|| format!("PulseAudio error {err:?}")) -} diff --git a/poc/linux_voice_test/src/linux_shared.rs b/poc/linux_voice_test/src/linux_shared.rs deleted file mode 100644 index 5e0fdaa..0000000 --- a/poc/linux_voice_test/src/linux_shared.rs +++ /dev/null @@ -1,384 +0,0 @@ -use crate::linux_pipewire::PipeWireBackend; -use crate::linux_pulseaudio::PulseAudioBackend; -use std::env; -use std::fmt; -use std::io::{self, Write}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; -use std::sync::Arc; -use std::thread; -use std::thread::JoinHandle; -use std::time::Duration; - -#[derive(Debug, Clone)] -pub struct LinuxDevice { - pub id: String, - pub label: String, -} - -impl fmt::Display for LinuxDevice { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} [{}]", self.label, self.id) - } -} - -pub trait LinuxLoopbackSession { - fn wait(self: Box) -> Result<(), String>; -} - -pub trait LinuxAudioBackend { - fn name(&self) -> &'static str; - fn list_input_devices(&self) -> Result, String>; - fn list_output_devices(&self) -> Result, String>; - fn start_loopback( - &self, - input: &LinuxDevice, - output: &LinuxDevice, - stop: Arc, - ) -> Result, String>; -} - -#[derive(Clone, Copy)] -enum LinuxBackendKind { - Auto, - PipeWire, - PulseAudio, -} - -const USAGE: &str = concat!( - "usage: cargo run -- [--backend auto|pipewire|pulseaudio] [--list-devices] ", - "[--input ] [--output ]\n\n", - "Examples:\n", - " cargo run -- --list-devices\n", - " cargo run -- --backend pipewire --list-devices\n", - " cargo run -- --input 0 --output 1\n", - " cargo run -- --backend pulseaudio --input alsa_input.pci-0000_00_1f.3.analog-stereo ", - "--output alsa_output.pci-0000_00_1f.3.analog-stereo" -); - -pub fn run_from_env() -> Result<(), String> { - let args: Vec = env::args().skip(1).collect(); - let cli = parse_cli(&args)?; - let backend = create_backend(cli.backend)?; - let inputs = backend.list_input_devices()?; - let outputs = backend.list_output_devices()?; - - if cli.list_devices { - print_devices(backend.name(), "Input devices:", &inputs); - print_devices(backend.name(), "Output devices:", &outputs); - return Ok(()); - } - - let input = select_device("input", &inputs, cli.input.as_deref())?; - let output = select_device("output", &outputs, cli.output.as_deref())?; - - let stop = Arc::new(AtomicBool::new(false)); - let stop_for_handler = stop.clone(); - ctrlc::set_handler(move || { - stop_for_handler.store(true, Ordering::SeqCst); - }) - .map_err(|err| err.to_string())?; - - println!("Backend: {}", backend.name()); - println!("Input: {input}"); - println!("Output: {output}"); - println!("Streaming. Press Ctrl+C to stop."); - - let session = backend.start_loopback(&input, &output, stop.clone())?; - - while !stop.load(Ordering::SeqCst) { - thread::sleep(Duration::from_millis(100)); - } - - session.wait() -} - -struct LinuxCli { - backend: LinuxBackendKind, - list_devices: bool, - input: Option, - output: Option, -} - -fn parse_cli(args: &[String]) -> Result { - let mut backend = LinuxBackendKind::Auto; - let mut list_devices = false; - let mut input = None; - let mut output = None; - let mut index = 0; - - while index < args.len() { - match args[index].as_str() { - "--backend" => { - index += 1; - let value = args - .get(index) - .ok_or_else(|| format!("missing value for --backend\n{USAGE}"))?; - backend = match value.as_str() { - "auto" => LinuxBackendKind::Auto, - "pipewire" => LinuxBackendKind::PipeWire, - "pulseaudio" => LinuxBackendKind::PulseAudio, - _ => return Err(format!("unsupported backend `{value}`\n{USAGE}")), - }; - } - "--list-devices" => { - list_devices = true; - } - "--input" => { - index += 1; - input = Some( - args.get(index) - .ok_or_else(|| format!("missing value for --input\n{USAGE}"))? - .clone(), - ); - } - "--output" => { - index += 1; - output = Some( - args.get(index) - .ok_or_else(|| format!("missing value for --output\n{USAGE}"))? - .clone(), - ); - } - "--help" | "-h" => { - return Err(USAGE.to_string()); - } - other => { - return Err(format!("unexpected argument `{other}`\n{USAGE}")); - } - } - - index += 1; - } - - Ok(LinuxCli { - backend, - list_devices, - input, - output, - }) -} - -fn create_backend(kind: LinuxBackendKind) -> Result, String> { - match kind { - LinuxBackendKind::Auto => { - let mut errors = Vec::new(); - - match create_backend(LinuxBackendKind::PipeWire) { - Ok(backend) => return Ok(backend), - Err(err) => errors.push(format!("pipewire: {err}")), - } - - match create_backend(LinuxBackendKind::PulseAudio) { - Ok(backend) => return Ok(backend), - Err(err) => errors.push(format!("pulseaudio: {err}")), - } - - Err(format!( - "failed to initialize a Linux audio backend\n{}", - errors.join("\n") - )) - } - LinuxBackendKind::PipeWire => { - pipewire::init(); - Ok(Box::new(PipeWireBackend)) - } - LinuxBackendKind::PulseAudio => Ok(Box::new(PulseAudioBackend)), - } -} - -fn select_device( - kind: &str, - items: &[LinuxDevice], - selector: Option<&str>, -) -> Result { - match selector { - Some(selector) => resolve_device_selector(kind, items, selector), - None => prompt_choice(&format!("{kind} devices:"), items), - } -} - -fn resolve_device_selector( - kind: &str, - items: &[LinuxDevice], - selector: &str, -) -> Result { - if items.is_empty() { - return Err(format!("no {kind} devices available")); - } - - if let Ok(index) = selector.parse::() { - return items - .get(index) - .cloned() - .ok_or_else(|| format!("{kind} device index `{selector}` is out of range")); - } - - let mut matches = items - .iter() - .filter(|item| item.id == selector || item.label == selector) - .cloned(); - - let Some(device) = matches.next() else { - return Err(format!( - "no {kind} device matched `{selector}`\nRun with `--list-devices` to inspect valid choices." - )); - }; - - if matches.next().is_some() { - return Err(format!( - "multiple {kind} devices matched `{selector}`\nUse the numeric index from `--list-devices` or the exact device id." - )); - } - - Ok(device) -} - -fn print_devices(backend_name: &str, title: &str, items: &[LinuxDevice]) { - println!("Backend: {backend_name}"); - println!("{title}"); - for (index, item) in items.iter().enumerate() { - println!(" [{index}] {item}"); - } -} - -fn prompt_choice(title: &str, items: &[LinuxDevice]) -> Result { - if items.is_empty() { - return Err(format!("no items available for `{title}`")); - } - - println!("{title}"); - for (index, item) in items.iter().enumerate() { - println!(" [{index}] {item}"); - } - - print!("Select number: "); - io::stdout().flush().map_err(|err| err.to_string())?; - - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .map_err(|err| err.to_string())?; - - let choice = input - .trim() - .parse::() - .map_err(|_| "invalid selection".to_string())?; - - items - .get(choice) - .cloned() - .ok_or_else(|| "selection out of range".to_string()) -} - -pub struct ThreadedLoopbackSession { - pub capture: JoinHandle>, - pub playback: JoinHandle>, - pub stop: Arc, -} - -impl LinuxLoopbackSession for ThreadedLoopbackSession { - fn wait(self: Box) -> Result<(), String> { - let ThreadedLoopbackSession { - capture, - playback, - stop, - } = *self; - stop.store(true, Ordering::SeqCst); - join_backend_thread(capture)?; - join_backend_thread(playback)?; - Ok(()) - } -} - -pub fn wait_for_startup( - ready_rx: mpsc::Receiver>, - handle: JoinHandle>, - label: &str, -) -> Result>, String> { - match ready_rx.recv() { - Ok(Ok(())) => Ok(handle), - Ok(Err(err)) => { - let _ = handle.join(); - Err(err) - } - Err(err) => { - let _ = handle.join(); - Err(format!("{label} startup failed: {err}")) - } - } -} - -pub fn join_backend_thread(handle: JoinHandle>) -> Result<(), String> { - match handle.join() { - Ok(result) => result, - Err(_) => Err("backend thread panicked".to_string()), - } -} - -pub fn sort_devices(left: &LinuxDevice, right: &LinuxDevice) -> std::cmp::Ordering { - left.label - .cmp(&right.label) - .then_with(|| left.id.cmp(&right.id)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn device(id: &str, label: &str) -> LinuxDevice { - LinuxDevice { - id: id.to_string(), - label: label.to_string(), - } - } - - #[test] - fn parse_cli_defaults_to_auto_backend() { - let cli = parse_cli(&[]).expect("parse_cli should accept empty args"); - assert!(matches!(cli.backend, LinuxBackendKind::Auto)); - assert!(!cli.list_devices); - assert_eq!(cli.input, None); - assert_eq!(cli.output, None); - } - - #[test] - fn parse_cli_reads_non_interactive_selection() { - let cli = parse_cli(&[ - "--backend".into(), - "pulseaudio".into(), - "--input".into(), - "1".into(), - "--output".into(), - "sink-id".into(), - "--list-devices".into(), - ]) - .expect("parse_cli should parse explicit flags"); - - assert!(matches!(cli.backend, LinuxBackendKind::PulseAudio)); - assert!(cli.list_devices); - assert_eq!(cli.input.as_deref(), Some("1")); - assert_eq!(cli.output.as_deref(), Some("sink-id")); - } - - #[test] - fn resolve_device_selector_supports_index_and_exact_id() { - let devices = vec![device("mic-id", "Mic"), device("sink-id", "Sink")]; - - let by_index = resolve_device_selector("input", &devices, "0") - .expect("selector should resolve by index"); - assert_eq!(by_index.id, "mic-id"); - - let by_id = resolve_device_selector("output", &devices, "sink-id") - .expect("selector should resolve by id"); - assert_eq!(by_id.label, "Sink"); - } - - #[test] - fn resolve_device_selector_rejects_ambiguous_labels() { - let devices = vec![device("mic-1", "Mic"), device("mic-2", "Mic")]; - let err = resolve_device_selector("input", &devices, "Mic") - .expect_err("selector should reject ambiguous labels"); - assert!(err.contains("multiple input devices matched `Mic`")); - } -} diff --git a/poc/linux_voice_test/src/main.rs b/poc/linux_voice_test/src/main.rs deleted file mode 100644 index 1b7eaeb..0000000 --- a/poc/linux_voice_test/src/main.rs +++ /dev/null @@ -1,26 +0,0 @@ -mod audio_buffer; - -#[cfg(target_os = "linux")] -mod linux_pipewire; -#[cfg(target_os = "linux")] -mod linux_pulseaudio; -#[cfg(target_os = "linux")] -mod linux_shared; - -#[cfg(target_os = "windows")] -mod windows_cpal; - -#[cfg(target_os = "linux")] -fn main() -> Result<(), String> { - linux_shared::run_from_env() -} - -#[cfg(target_os = "windows")] -fn main() -> Result<(), String> { - windows_cpal::run() -} - -#[cfg(not(any(target_os = "linux", target_os = "windows")))] -fn main() { - eprintln!("This POC currently supports Windows via cpal and Linux via PipeWire or PulseAudio."); -} diff --git a/poc/linux_voice_test/src/windows_cpal.rs b/poc/linux_voice_test/src/windows_cpal.rs deleted file mode 100644 index 769dcb3..0000000 --- a/poc/linux_voice_test/src/windows_cpal.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::audio_buffer::{make_shared_buffer, pop_sample, push_sample}; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{SampleFormat, Stream, StreamConfig}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::thread; -use std::time::Duration; - -pub fn run() -> Result<(), String> { - let host = cpal::default_host(); - let input = host - .default_input_device() - .ok_or("no default input device available")?; - let output = host - .default_output_device() - .ok_or("no default output device available")?; - - println!( - "Windows cpal input: {}", - input.name().map_err(|err| err.to_string())? - ); - println!( - "Windows cpal output: {}", - output.name().map_err(|err| err.to_string())? - ); - - let input_config = input - .default_input_config() - .map_err(|err| err.to_string())?; - let output_config = output - .default_output_config() - .map_err(|err| err.to_string())?; - let config: StreamConfig = input_config.config(); - let buffer = make_shared_buffer(); - - let input_stream = build_input_stream( - &input, - &config, - input_config.sample_format(), - buffer.clone(), - )?; - let output_stream = build_output_stream( - &output, - &config, - output_config.sample_format(), - buffer.clone(), - )?; - - let stop = Arc::new(AtomicBool::new(false)); - let stop_for_handler = stop.clone(); - ctrlc::set_handler(move || { - stop_for_handler.store(true, Ordering::SeqCst); - }) - .map_err(|err| err.to_string())?; - - input_stream.play().map_err(|err| err.to_string())?; - output_stream.play().map_err(|err| err.to_string())?; - - println!("Streaming with cpal. Press Ctrl+C to stop."); - while !stop.load(Ordering::SeqCst) { - thread::sleep(Duration::from_millis(100)); - } - - drop(input_stream); - drop(output_stream); - Ok(()) -} - -fn build_input_stream( - device: &cpal::Device, - config: &StreamConfig, - sample_format: SampleFormat, - buffer: crate::audio_buffer::SharedBuffer, -) -> Result { - let err_fn = |err| eprintln!("cpal input stream error: {err}"); - - match sample_format { - SampleFormat::F32 => device - .build_input_stream( - config, - move |data: &[f32], _| { - for &sample in data { - push_sample(&buffer, sample); - } - }, - err_fn, - None, - ) - .map_err(|err| err.to_string()), - SampleFormat::I16 => device - .build_input_stream( - config, - move |data: &[i16], _| { - for &sample in data { - push_sample(&buffer, sample as f32 / i16::MAX as f32); - } - }, - err_fn, - None, - ) - .map_err(|err| err.to_string()), - SampleFormat::U16 => device - .build_input_stream( - config, - move |data: &[u16], _| { - for &sample in data { - let normalized = (sample as f32 / u16::MAX as f32) * 2.0 - 1.0; - push_sample(&buffer, normalized); - } - }, - err_fn, - None, - ) - .map_err(|err| err.to_string()), - other => Err(format!("unsupported cpal input sample format: {other:?}")), - } -} - -fn build_output_stream( - device: &cpal::Device, - config: &StreamConfig, - sample_format: SampleFormat, - buffer: crate::audio_buffer::SharedBuffer, -) -> Result { - let err_fn = |err| eprintln!("cpal output stream error: {err}"); - - match sample_format { - SampleFormat::F32 => device - .build_output_stream( - config, - move |data: &mut [f32], _| { - for sample in data { - *sample = pop_sample(&buffer); - } - }, - err_fn, - None, - ) - .map_err(|err| err.to_string()), - SampleFormat::I16 => device - .build_output_stream( - config, - move |data: &mut [i16], _| { - for sample in data { - let value = pop_sample(&buffer).clamp(-1.0, 1.0); - *sample = (value * i16::MAX as f32) as i16; - } - }, - err_fn, - None, - ) - .map_err(|err| err.to_string()), - SampleFormat::U16 => device - .build_output_stream( - config, - move |data: &mut [u16], _| { - for sample in data { - let value = pop_sample(&buffer).clamp(-1.0, 1.0); - *sample = (((value + 1.0) * 0.5) * u16::MAX as f32) as u16; - } - }, - err_fn, - None, - ) - .map_err(|err| err.to_string()), - other => Err(format!("unsupported cpal output sample format: {other:?}")), - } -} diff --git a/poc/secure-storage-spike/Cargo.lock b/poc/secure-storage-spike/Cargo.lock deleted file mode 100644 index b695780..0000000 --- a/poc/secure-storage-spike/Cargo.lock +++ /dev/null @@ -1,1854 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[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-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[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 = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[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 = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[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 = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[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 = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[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 = "dbus" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "dbus-secret-service" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" -dependencies = [ - "aes", - "block-padding", - "cbc", - "dbus", - "fastrand", - "hkdf", - "num", - "once_cell", - "sha2", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "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", - "pin-project-lite", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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-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 = "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-core", - "futures-macro", - "futures-sink", - "futures-task", - "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", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[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 = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "keyring" -version = "3.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" -dependencies = [ - "dbus-secret-service", - "linux-keyutils", - "log", - "secret-service", - "zeroize", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-keyutils" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[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 = "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 = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[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" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[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-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[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-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[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 = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - -[[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-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[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 = "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 = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[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-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 = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - -[[package]] -name = "secret-service" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" -dependencies = [ - "aes", - "cbc", - "futures-util", - "generic-array", - "hkdf", - "num", - "once_cell", - "rand", - "serde", - "sha2", - "zbus", -] - -[[package]] -name = "secure-storage-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "keyring", - "rusqlite", - "serial_test", - "tempfile", - "thiserror", - "tracing", - "tracing-subscriber", - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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 = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serial_test" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" -dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[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" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[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 = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[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 = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[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.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[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.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "zbus" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" -dependencies = [ - "async-broadcast", - "async-process", - "async-recursion", - "async-trait", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "ordered-stream", - "rand", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - -[[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 = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zvariant" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/poc/secure-storage-spike/Cargo.toml b/poc/secure-storage-spike/Cargo.toml deleted file mode 100644 index d26f770..0000000 --- a/poc/secure-storage-spike/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "secure-storage-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: SecureStore trait + Linux Secret Service adapter; prove SS-AUD-001..006 for the audit report." - -# Not product code. See docs/architecture/proof-of-concept-plan.md §4 and -# docs/security/secure-storage-audit-report.md. - -[dependencies] -thiserror = "2" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -zeroize = { version = "1", features = ["derive"] } - -# Linux secure storage: keyring crate. -# * sync-secret-service: libsecret-compatible Secret Service via D-Bus. -# * linux-native: Kernel session keyring (keyutils). No D-Bus required. -# Both are acceptable Linux backends per SysRS-053 / SysRS-162 -# ("Secret Service, libsecret, or equivalent"). The adapter picks at -# construction; the test suite exercises the keyutils backend because -# headless CI commonly lacks an unlocked Secret Service collection. -[target.'cfg(target_os = "linux")'.dependencies] -keyring = { version = "3", default-features = false, features = ["sync-secret-service", "crypto-rust", "linux-native"] } - -# rusqlite is a stand-in for the production `LocalDatabaseRepository`. Bundled -# build avoids depending on a system libsqlite3. -rusqlite = { version = "0.32", features = ["bundled"] } - -[dev-dependencies] -anyhow = "1" -serial_test = "3" -tempfile = "3" - -[[bin]] -name = "secure-storage-cli" -path = "src/main.rs" - -[lib] -name = "secure_storage_spike" -path = "src/lib.rs" diff --git a/poc/secure-storage-spike/README.md b/poc/secure-storage-spike/README.md deleted file mode 100644 index cb93905..0000000 --- a/poc/secure-storage-spike/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Secure Storage Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `secure-storage-spike` | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Prove platform secure storage behaviour | -| Exit criterion | "Secret write/read/delete works through platform secure storage" | -| Authority | ADR-006 (`SecureStore trait + per-platform adapters`), SDD-078, SRS-091..095, SysRS-158..162 | -| Audit refs | `docs/security/secure-storage-audit-report.md` SS-AUD-001..006 / SS-TC-003 | - -## What it proves - -- A typed `SecretStorageRepository` trait that the rest of the Rust core - can depend on without backend leakage (SAD-067). -- A Linux adapter selecting between two equivalent backends per - SysRS-162 ("Secret Service, libsecret, **or equivalent**"): - - **Secret Service / libsecret** (gnome-keyring, kwallet, KeePassXC, …). - - **Kernel keyutils** (`add_key(2)` / `request_key(2)`) — used when no - D-Bus session or unlocked Secret Service collection is available. -- A typed `SecureStoreError` with `NotFound` / `Unavailable` / `Backend` - arms, satisfying SS-AUD-005 (safe error mapping). -- A `Secret` newtype with redacting `Debug` / `Display` and - zero-on-drop, satisfying SS-AUD-003 defence-in-depth. -- A miniature `LocalDatabaseRepository` (rusqlite, bundled) that holds - only **references** to secret names — proving the SAD-067 separation - empirically (SS-AUD-001, SS-AUD-002). - -## Layout - -```text -secure-storage-spike/ - src/ - lib.rs # crate root + re-exports - secret.rs # SecretStorageRepository trait, Secret type, Linux adapter - sqlite_repo.rs # LocalDatabaseRepository (non-secret state only) - test_logger.rs # in-memory log capture for SS-AUD-003 - main.rs # secure-storage-cli driver (SS round-trip) - tests/ - audit.rs # SS-AUD-001/002/003/005/006 + SS-TC-003 - Cargo.toml -``` - -## Reproduce - -Requires Rust stable (developed against 1.95). Linux only — non-Linux -adapters are out of scope for this spike. `cargo` needs network access -on first build. - -```bash -# Run the audit test suite (uses kernel keyutils backend; hermetic). -keyctl session - cargo test --tests - -# Run the CLI driver. It prefers Secret Service and falls back to -# keyutils if the default collection is locked. Either path satisfies -# SysRS-053 / SysRS-162. -keyctl session - cargo run --bin secure-storage-cli -``` - -> **Why `keyctl session -`?** -> The Linux kernel session keyring is inherited from the calling process. -> Cargo's test/run wrappers often inherit an expired or empty -> `_ses` keyring from non-interactive shells. `keyctl session -` creates -> a fresh session keyring before invoking the command, guaranteeing a -> valid backend for keyutils. Interactive desktop sessions normally do -> not need this wrapper. - -## Scope boundaries - -This spike is intentionally narrow. Out of scope: - -- **Windows / macOS / iOS / Android adapters.** SS-TC-001/002/004/005 - remain unverified. The trait is shaped to accommodate them but no - code is shipped here. -- **SS-AUD-004 (diagnostic export redaction).** Owned by the - `diagnostics-redaction-spike`. -- **SS-AUD-007 (per-platform documentation).** Doc-only check; lives - in the audit report itself. -- **SS-AUD-008 (migration / import).** No migration paths exist yet. -- **Encryption at rest** beyond what the platform backend provides. -- **Constant-time secret comparison.** `Secret::eq` is best-effort. -- **Threading / async semantics.** Trait is `Send + Sync` but no - concurrent stress is exercised. - -## Verification log - -See `VERIFICATION.md` in this directory. diff --git a/poc/secure-storage-spike/VERIFICATION.md b/poc/secure-storage-spike/VERIFICATION.md deleted file mode 100644 index 8aad05b..0000000 --- a/poc/secure-storage-spike/VERIFICATION.md +++ /dev/null @@ -1,105 +0,0 @@ -# Verification record — `secure-storage-spike` - -## Result - -PASS. The PoC exit criterion (from -`docs/architecture/proof-of-concept-plan.md` §2: "Secret write/read/delete -works through platform secure storage") is met on Linux through the -kernel keyutils backend, with a documented Secret Service path verified -manually via the CLI driver. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | -| Rust toolchain | stable 1.95.0 | -| Backend (tests) | kernel keyutils (linux-native, `add_key(2)` / `request_key(2)`) | -| Backend (CLI) | tried Secret Service first; fell back to keyutils because the default Secret Service collection was locked (no graphical login) | -| `keyring` crate | 3.6.3 (`sync-secret-service`, `linux-native`, `crypto-rust`) | -| `linux-keyutils` | 0.2.5 | -| `rusqlite` | 0.32.1 (bundled) | - -## Reproduction - -```bash -keyctl session - cargo test --tests -keyctl session - cargo run --bin secure-storage-cli -``` - -## Test run - -``` -running 6 tests -test ss_aud_003_secret_values_not_in_logs ... ok -test ss_aud_006_delete_removes_entry ... ok -test ss_aud_001_identity_secret_absent_from_local_db ... ok -test ss_aud_002_server_password_absent_from_local_db ... ok -test ss_aud_005_safe_error_on_missing_entry ... ok -test ss_tc_003_round_trip_linux ... ok - -test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; - 0 filtered out; finished in 0.00s -``` - -## CLI run - -``` -INFO spike: Secret Service unavailable (Platform secure storage failure: - DBus error: Cannot create an item in a locked collection); - falling back to keyutils -INFO spike: writing secret identity.primary -INFO spike: reading back -INFO spike: round-trip OK (48 bytes) -INFO spike: deleting -INFO spike: post-delete read correctly returned NotFound -OK — Linux Secret Service round-trip verified. -``` - -This CLI output also evidences SS-AUD-005 in practice: a locked Secret -Service collection produced a typed `Backend` error containing no secret -material, which the caller could then route to the fallback adapter -rather than propagating the raw D-Bus message to the user. - -## Audit-check coverage - -| Check | Mapped test(s) | Result | -|---|---|---| -| SS-AUD-001 (Identity secret absent from local DB) | `ss_aud_001_identity_secret_absent_from_local_db` | PASS — raw SQLite file scanned for plaintext; only the lookup name appears. | -| SS-AUD-002 (Server password absent from local DB) | `ss_aud_002_server_password_absent_from_local_db` | PASS — same scan, distinct plaintext marker. | -| SS-AUD-003 (Secret values not in logs) | `ss_aud_003_secret_values_not_in_logs` | PASS — captured `tracing` output contains `` markers; never contains the plaintext. | -| SS-AUD-005 (Failure → safe error) | `ss_aud_005_safe_error_on_missing_entry` + CLI fallback path | PASS — `NotFound` is the typed Display, no leakage. | -| SS-AUD-006 (Delete removes entry) | `ss_aud_006_delete_removes_entry` | PASS — second delete returns `NotFound`, not silent success. | -| SS-TC-003 (Linux round-trip) | `ss_tc_003_round_trip_linux` | PASS — set/get equality + delete + post-delete `NotFound`. | - -## What this spike does NOT validate - -- SS-AUD-004 (diagnostic export redaction) — `diagnostics-redaction-spike`. -- SS-AUD-007 (per-platform documentation completeness). -- SS-AUD-008 (migration / import safety). -- SS-TC-001 (Windows DPAPI / Credential Manager). -- SS-TC-002 (macOS Keychain). -- SS-TC-004 (Android Keystore). -- SS-TC-005 (iOS Keychain). -- Concurrent access from multiple threads / processes. -- Long-lived persistence behaviour across reboots (keyutils backend is - session-scoped by design). -- Behaviour under a locked or absent Secret Service collection in - *production* (mitigation strategy is shown but not policy). - -## Notable observations - -- The kernel keyutils session keyring is inherited from the calling - process; non-interactive shells can present an *expired* `_ses` - keyring. The wrapping `keyctl session -` is the documented workaround - and is universal on Linux. Production code on a graphical session - inherits a valid session from PAM and does not need it. -- gnome-keyring on this host was running but the default collection - was locked because no graphical login had occurred. The CLI's - Secret-Service-first / keyutils-fallback path observed exactly this - condition and reacted correctly, doubling as live evidence for - SS-AUD-005. -- The `Secret` newtype's `Debug` formatter prints - `Secret()` and `Display` prints ``. The - SS-AUD-003 test exercises both representations. diff --git a/poc/secure-storage-spike/src/lib.rs b/poc/secure-storage-spike/src/lib.rs deleted file mode 100644 index ac15ef0..0000000 --- a/poc/secure-storage-spike/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Chanora PoC — secure storage spike. -//! -//! Authority: -//! * `docs/architecture/proof-of-concept-plan.md` §2 — Secure storage spike. -//! * `docs/security/secure-storage-audit-report.md` — audit checks -//! SS-AUD-001..008 and test cases SS-TC-001..005. -//! * `docs/architecture/sysdes.md` ADR-006 — "SecureStore trait and -//! per-platform adapters". -//! * SRS-091..095, SysRS-158..162. -//! -//! This crate models the production shape, in miniature: -//! -//! ┌─────────────────────────┐ ┌─────────────────────────────────┐ -//! │ LocalDatabaseRepository │ │ SecretStorageRepository (trait) │ -//! │ (rusqlite, non-secret) │ │ ── per-platform adapters ── │ -//! └─────────────────────────┘ └─────────────────────────────────┘ -//! SRS-089 / SDD-077 SRS-092 / SDD-078 / ADR-006 -//! -//! Scope: Linux adapter only (Secret Service via the `keyring` crate). -//! Other platforms (Windows DPAPI, macOS/iOS Keychain, Android Keystore) -//! are out of scope here and live in their own spikes/adapters. - -pub mod secret; -pub mod sqlite_repo; -pub mod test_logger; - -pub use secret::{SecretStorageRepository, SecureStoreError}; -pub use sqlite_repo::LocalDatabaseRepository; diff --git a/poc/secure-storage-spike/src/main.rs b/poc/secure-storage-spike/src/main.rs deleted file mode 100644 index 95dabc1..0000000 --- a/poc/secure-storage-spike/src/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! CLI driver — exercises the full secret round-trip against the real -//! platform secure storage. - -use std::process::ExitCode; - -use secure_storage_spike::secret::Secret; -use secure_storage_spike::SecretStorageRepository; -use tracing::info; - -const SERVICE: &str = "app.chanora.poc.secure-storage"; - -fn main() -> ExitCode { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .init(); - - if let Err(e) = run() { - eprintln!("error: {e}"); - return ExitCode::from(1); - } - ExitCode::SUCCESS -} - -fn run() -> Result<(), Box> { - #[cfg(not(target_os = "linux"))] - { - eprintln!("This PoC currently only ships a Linux adapter."); - return Err("unsupported platform".into()); - } - - #[cfg(target_os = "linux")] - { - use secure_storage_spike::secret::linux::LinuxSecureStore; - - // Try Secret Service first (preferred per ADR-006); fall back to - // keyutils if the default collection is locked or unavailable. - let store: Box = { - let ss = LinuxSecureStore::new(SERVICE); - let probe = ss.set("__chanora_probe__", &Secret::from_utf8("probe")); - match probe { - Ok(()) => { - let _ = ss.delete("__chanora_probe__"); - info!(target: "spike", "using Secret Service backend"); - Box::new(ss) - } - Err(e) => { - info!(target: "spike", "Secret Service unavailable ({e}); falling back to keyutils"); - Box::new(LinuxSecureStore::new_keyutils(SERVICE)) - } - } - }; - - let name = "identity.primary"; - - info!(target: "spike", "writing secret {}", name); - let original = Secret::from_utf8("MG0DAgeAAgEgAiAIXJBlj1hQbaH0Eq0DuLlCmH8bl+veTAO2"); - store.set(name, &original)?; - - info!(target: "spike", "reading back"); - let round_trip = store.get(name)?; - assert_eq!(original, round_trip, "round-trip mismatch"); - info!(target: "spike", "round-trip OK ({} bytes)", round_trip.len()); - - info!(target: "spike", "deleting"); - store.delete(name)?; - match store.get(name) { - Err(secure_storage_spike::SecureStoreError::NotFound) => { - info!(target: "spike", "post-delete read correctly returned NotFound"); - } - Ok(_) => return Err("secret still readable after delete".into()), - Err(e) => return Err(format!("unexpected error after delete: {e}").into()), - } - - println!("OK — Linux Secret Service round-trip verified."); - Ok(()) - } -} diff --git a/poc/secure-storage-spike/src/secret.rs b/poc/secure-storage-spike/src/secret.rs deleted file mode 100644 index 5d67cd5..0000000 --- a/poc/secure-storage-spike/src/secret.rs +++ /dev/null @@ -1,206 +0,0 @@ -//! `SecretStorageRepository` trait + Linux Secret Service adapter. -//! -//! Maps to SDD-078 (`SecretStorageRepository shall persist secrets only -//! through platform secure storage service interfaces`) and ADR-006 -//! (`SecureStore trait and per-platform adapters`). - -use std::fmt; - -use zeroize::Zeroize; - -/// Trait every platform adapter implements. -/// -/// The API is intentionally narrow: a secret is identified by a stable -/// `name` (e.g. `"identity.primary"`, `"server..password"`) -/// and treated as an opaque UTF-8 byte sequence. The trait does **not** -/// expose handles, paths, or backend-specific types — the -/// `chanora_storage` crate must not leak those upward (SAD-067). -pub trait SecretStorageRepository: Send + Sync { - /// Store (or replace) a secret under `name`. The implementation - /// must not write `secret` to plaintext disk, logs, or diagnostic - /// exports (SS-AUD-001..004). - fn set(&self, name: &str, secret: &Secret) -> Result<(), SecureStoreError>; - - /// Retrieve a secret previously stored under `name`. - /// - /// Returns `Err(SecureStoreError::NotFound)` if no entry exists. - fn get(&self, name: &str) -> Result; - - /// Remove a secret. Returns `Err(SecureStoreError::NotFound)` if - /// it was already absent. Idempotent variants are intentionally - /// not provided here; callers must handle `NotFound`. - fn delete(&self, name: &str) -> Result<(), SecureStoreError>; -} - -/// Typed error DTO. Production code (`chanora_bridge`) will widen this -/// when more failure modes are observed; the PoC only needs to prove -/// SS-AUD-005 (safe error mapping). -#[derive(Debug, thiserror::Error)] -pub enum SecureStoreError { - #[error("secret not found")] - NotFound, - #[error("platform secure storage unavailable: {0}")] - Unavailable(String), - #[error("platform secure storage backend rejected the operation: {0}")] - Backend(String), -} - -/// Owned, zeroed-on-drop secret material. -/// -/// `Debug` and `Display` deliberately do **not** print the inner bytes -/// — this is part of the SS-AUD-003 (no-secrets-in-logs) defense in -/// depth. Tests in this crate rely on this behavior. -#[derive(Clone, Zeroize)] -#[zeroize(drop)] -pub struct Secret(Vec); - -impl Secret { - pub fn from_utf8(s: impl Into) -> Self { - Self(s.into().into_bytes()) - } - - pub fn from_bytes(b: impl Into>) -> Self { - Self(b.into()) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn as_str(&self) -> Result<&str, std::str::Utf8Error> { - std::str::from_utf8(&self.0) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl fmt::Debug for Secret { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Secret(<{} bytes redacted>)", self.0.len()) - } -} - -impl fmt::Display for Secret { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Same redaction shape as Debug. - write!(f, "") - } -} - -impl PartialEq for Secret { - fn eq(&self, other: &Self) -> bool { - // Constant-time-ish compare via fixed length-first then byte compare. - // The PoC does not promise true constant-time semantics. - self.0 == other.0 - } -} - -// ---------- Linux adapter ---------- - -#[cfg(target_os = "linux")] -pub mod linux { - use super::*; - use keyring::Entry; - - /// Which Linux backend to use. - /// - /// Both are acceptable under SysRS-053 / SysRS-162 ("Secret Service, - /// libsecret, or equivalent"): - /// - /// * `SecretService`: D-Bus Secret Service (gnome-keyring, kwallet, - /// KeePassXC, etc.). Preferred on interactive desktop sessions. - /// * `Keyutils`: kernel session keyring (`add_key(2)` / - /// `request_key(2)`). Always available on Linux, no D-Bus - /// dependency, but secrets live only for the session lifetime. - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum LinuxBackend { - /// Default user keyring backend selected by the `keyring` crate - /// at compile time. With the features this PoC enables, this - /// is Secret Service. - Default, - /// Force the kernel keyutils backend. Useful for headless - /// environments and for production code paths where no - /// graphical session is available. - Keyutils, - } - - /// Linux adapter using the `keyring` crate. The choice of backend - /// is made at construction so the rest of the application can - /// remain backend-agnostic. - pub struct LinuxSecureStore { - service: String, - backend: LinuxBackend, - } - - impl LinuxSecureStore { - /// Construct an adapter backed by the default `keyring` - /// credential store (Secret Service with the features compiled - /// in this PoC). - pub fn new(service: impl Into) -> Self { - Self { service: service.into(), backend: LinuxBackend::Default } - } - - /// Construct an adapter backed by the kernel keyutils session - /// keyring. Production code may select this when no D-Bus - /// session is available. - pub fn new_keyutils(service: impl Into) -> Self { - Self { service: service.into(), backend: LinuxBackend::Keyutils } - } - - fn entry(&self, name: &str) -> Result { - let result = match self.backend { - LinuxBackend::Default => Entry::new(&self.service, name), - LinuxBackend::Keyutils => { - let cred = keyring::keyutils::KeyutilsCredential::new_with_target( - None, - &self.service, - name, - ) - .map_err(|e| SecureStoreError::Unavailable(format!("{e}")))?; - Ok(Entry::new_with_credential(Box::new(cred))) - } - }; - result.map_err(|e| SecureStoreError::Unavailable(format!("{e}"))) - } - } - - impl SecretStorageRepository for LinuxSecureStore { - fn set(&self, name: &str, secret: &Secret) -> Result<(), SecureStoreError> { - let entry = self.entry(name)?; - let s = secret.as_str().map_err(|e| { - SecureStoreError::Backend(format!("secret is not valid utf-8: {e}")) - })?; - entry - .set_password(s) - .map_err(|e| SecureStoreError::Backend(format!("{e}"))) - } - - fn get(&self, name: &str) -> Result { - let entry = self.entry(name)?; - match entry.get_password() { - Ok(s) => Ok(Secret::from_utf8(s)), - Err(keyring::Error::NoEntry) => Err(SecureStoreError::NotFound), - Err(e) => Err(SecureStoreError::Backend(format!("{e}"))), - } - } - - fn delete(&self, name: &str) -> Result<(), SecureStoreError> { - let entry = self.entry(name)?; - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Err(SecureStoreError::NotFound), - Err(e) => Err(SecureStoreError::Backend(format!("{e}"))), - } - } - } - - /// Back-compat type alias — older code in this PoC referred to - /// `SecretServiceAdapter` before the backend choice existed. - pub type SecretServiceAdapter = LinuxSecureStore; -} diff --git a/poc/secure-storage-spike/src/sqlite_repo.rs b/poc/secure-storage-spike/src/sqlite_repo.rs deleted file mode 100644 index 73ce41d..0000000 --- a/poc/secure-storage-spike/src/sqlite_repo.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Minimal `LocalDatabaseRepository` stand-in (SDD-077). Holds *non-secret* -//! state only — bookmarks, in this PoC. Used by the audit tests to prove -//! SS-AUD-001 / SS-AUD-002: a secret stored via the secure-storage -//! adapter never appears in the SQLite file. - -use std::path::Path; - -use rusqlite::{params, Connection}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum LocalDbError { - #[error("sqlite error: {0}")] - Sqlite(#[from] rusqlite::Error), -} - -pub struct LocalDatabaseRepository { - conn: Connection, -} - -impl LocalDatabaseRepository { - pub fn open(path: &Path) -> Result { - let conn = Connection::open(path)?; - conn.execute_batch( - "CREATE TABLE IF NOT EXISTS bookmarks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - server_name TEXT NOT NULL, - server_host TEXT NOT NULL, - identity_ref TEXT NOT NULL - );", - )?; - Ok(Self { conn }) - } - - /// Insert a bookmark. `identity_ref` is a *reference* (e.g. a stable - /// secret name like `identity.primary`), **not** the secret itself. - /// This is the SAD-067 separation in action. - pub fn insert_bookmark( - &self, - server_name: &str, - server_host: &str, - identity_ref: &str, - ) -> Result { - self.conn.execute( - "INSERT INTO bookmarks (server_name, server_host, identity_ref) VALUES (?1, ?2, ?3)", - params![server_name, server_host, identity_ref], - )?; - Ok(self.conn.last_insert_rowid()) - } - - /// For the audit: read the raw bytes of the underlying SQLite file - /// so a test can grep for forbidden plaintext (SS-AUD-001). - pub fn raw_db_bytes(path: &Path) -> std::io::Result> { - std::fs::read(path) - } -} diff --git a/poc/secure-storage-spike/src/test_logger.rs b/poc/secure-storage-spike/src/test_logger.rs deleted file mode 100644 index ab5f2a6..0000000 --- a/poc/secure-storage-spike/src/test_logger.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! In-memory log capture, used by the audit tests to assert that -//! `tracing` output contains no secret material (SS-AUD-003). -//! -//! Production code will use the redaction policy defined in -//! `docs/security/diagnostic-redaction-audit-report.md` and the -//! `chanora_diagnostics` crate. This PoC is intentionally simpler: -//! it captures *all* log output verbatim and lets the test grep it. - -use std::io::{self, Write}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub struct CapturedLog { - buf: Arc>>, -} - -impl CapturedLog { - pub fn new() -> Self { - Self::default() - } - - /// Snapshot the captured bytes as a UTF-8 string (lossy if there's - /// any non-UTF-8 — we never expect non-UTF-8 from `tracing`). - pub fn snapshot(&self) -> String { - let buf = self.buf.lock().unwrap(); - String::from_utf8_lossy(&buf).into_owned() - } - - pub fn writer(&self) -> CapturedLogWriter { - CapturedLogWriter { buf: self.buf.clone() } - } -} - -pub struct CapturedLogWriter { - buf: Arc>>, -} - -impl Write for CapturedLogWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let mut g = self.buf.lock().unwrap(); - g.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturedLog { - type Writer = CapturedLogWriter; - fn make_writer(&'a self) -> Self::Writer { - self.writer() - } -} diff --git a/poc/secure-storage-spike/tests/audit.rs b/poc/secure-storage-spike/tests/audit.rs deleted file mode 100644 index 66d156a..0000000 --- a/poc/secure-storage-spike/tests/audit.rs +++ /dev/null @@ -1,229 +0,0 @@ -//! Audit tests mapped to `docs/security/secure-storage-audit-report.md`. -//! -//! Coverage: -//! * SS-AUD-001 — Identity secret is not stored in local DB. -//! * SS-AUD-002 — Server password is not stored in local DB. -//! * SS-AUD-003 — Secret values are not written to application logs. -//! * SS-AUD-005 — Secure storage failure returns safe error. -//! * SS-AUD-006 — Secret deletion removes secure-storage entry. -//! * SS-TC-003 — Linux: write / read / delete round-trip. -//! -//! NOT covered (out of scope for this PoC): -//! * SS-AUD-004 (diagnostic export redaction) — owned by -//! `diagnostics-redaction-spike`. -//! * SS-AUD-007/008 — process-level documentation / migration paths. -//! * SS-TC-001/002/004/005 — non-Linux platforms. - -#![cfg(target_os = "linux")] - -use std::sync::Once; - -use secure_storage_spike::{ - secret::{linux::LinuxSecureStore, Secret}, - test_logger::CapturedLog, - LocalDatabaseRepository, SecretStorageRepository, SecureStoreError, -}; -use serial_test::serial; -use tracing::{info, subscriber::DefaultGuard, Level}; - -const SERVICE: &str = "app.chanora.poc.secure-storage-tests"; - -static INIT: Once = Once::new(); - -/// Hermetic backend selection. -/// -/// On Linux the trait has two equivalent backends (SysRS-053 / SysRS-162: -/// "Secret Service, libsecret, **or equivalent**"). The test suite uses -/// the kernel keyutils backend so it is deterministic and does not -/// depend on an unlocked Secret Service collection. The CLI driver -/// `src/main.rs` continues to exercise the Secret Service path. -fn store() -> LinuxSecureStore { - LinuxSecureStore::new_keyutils(SERVICE) -} - -/// One-time guard so missing kernel keyutils support is reported clearly -/// rather than as a generic backend error. -fn ensure_backend_or_skip() -> bool { - INIT.call_once(|| {}); - // keyutils is a Linux-kernel feature; if it's compiled out or - // disabled, every set() call will fail with `Unavailable`. We can't - // easily probe without writing, so we rely on the per-test errors. - true -} - -/// Install a per-test tracing subscriber that captures everything. -fn capture_logs() -> (CapturedLog, DefaultGuard) { - let cap = CapturedLog::new(); - let subscriber = tracing_subscriber::fmt() - .with_max_level(Level::TRACE) - .with_writer(cap.clone()) - .without_time() - .with_ansi(false) - .finish(); - let guard = tracing::subscriber::set_default(subscriber); - (cap, guard) -} - -// Distinctive plaintext so a grep in test artifacts unambiguously -// identifies a leak. -const IDENTITY_SECRET: &str = "CHANORA_POC_IDENTITY_SECRET_DO_NOT_LEAK_b8f1a4"; -const SERVER_PASSWORD: &str = "CHANORA_POC_SERVER_PASSWORD_DO_NOT_LEAK_2f7c01"; - -#[test] -#[serial] -fn ss_tc_003_round_trip_linux() { - if !ensure_backend_or_skip() { - return; - } - let store = store(); - let name = "ss-tc-003.identity.primary"; - let _ = store.delete(name); // ensure clean slate - - let s = Secret::from_utf8(IDENTITY_SECRET); - store.set(name, &s).expect("set"); - let got = store.get(name).expect("get"); - assert_eq!(s, got, "round-trip mismatch"); - - store.delete(name).expect("delete"); - assert!(matches!(store.get(name), Err(SecureStoreError::NotFound))); -} - -#[test] -#[serial] -fn ss_aud_001_identity_secret_absent_from_local_db() { - if !ensure_backend_or_skip() { - return; - } - let tmp = tempfile::tempdir().unwrap(); - let db_path = tmp.path().join("chanora.sqlite"); - let db = LocalDatabaseRepository::open(&db_path).expect("open"); - - // Store identity secret via the secure store, by name only in the DB. - let store = store(); - let identity_ref = "ss-aud-001.identity.primary"; - let _ = store.delete(identity_ref); - store - .set(identity_ref, &Secret::from_utf8(IDENTITY_SECRET)) - .expect("set"); - - db.insert_bookmark("Vigorous", "cn.teamspeak.app", identity_ref) - .expect("insert"); - - // Force the DB to flush and inspect the file bytes. - drop(db); - let bytes = LocalDatabaseRepository::raw_db_bytes(&db_path).unwrap(); - assert!( - !bytes.windows(IDENTITY_SECRET.len()).any(|w| w == IDENTITY_SECRET.as_bytes()), - "identity secret plaintext found inside local SQLite file" - ); - - // The reference (= the lookup name) is allowed to be in the DB. - assert!(bytes.windows(identity_ref.len()).any(|w| w == identity_ref.as_bytes())); - - // Cleanup. - store.delete(identity_ref).ok(); -} - -#[test] -#[serial] -fn ss_aud_002_server_password_absent_from_local_db() { - if !ensure_backend_or_skip() { - return; - } - let tmp = tempfile::tempdir().unwrap(); - let db_path = tmp.path().join("chanora.sqlite"); - let db = LocalDatabaseRepository::open(&db_path).expect("open"); - - let store = store(); - let pw_ref = "ss-aud-002.server.42.password"; - let _ = store.delete(pw_ref); - store - .set(pw_ref, &Secret::from_utf8(SERVER_PASSWORD)) - .expect("set"); - - db.insert_bookmark("Some Server", "ts.example.invalid", pw_ref) - .expect("insert"); - - drop(db); - let bytes = LocalDatabaseRepository::raw_db_bytes(&db_path).unwrap(); - assert!( - !bytes.windows(SERVER_PASSWORD.len()).any(|w| w == SERVER_PASSWORD.as_bytes()), - "server password plaintext found inside local SQLite file" - ); - - store.delete(pw_ref).ok(); -} - -#[test] -#[serial] -fn ss_aud_003_secret_values_not_in_logs() { - if !ensure_backend_or_skip() { - return; - } - let (cap, _guard) = capture_logs(); - - let store = store(); - let name = "ss-aud-003.identity.primary"; - let _ = store.delete(name); - - let secret = Secret::from_utf8(IDENTITY_SECRET); - - // The kinds of things a developer is likely to log accidentally. - info!(target: "spike", "writing identity secret name={} value={:?}", name, secret); - store.set(name, &secret).expect("set"); - - let got = store.get(name).expect("get"); - info!(target: "spike", "round-trip ok display=`{}` debug=`{:?}` len={}", got, got, got.len()); - - store.delete(name).ok(); - - let log_output = cap.snapshot(); - assert!( - !log_output.contains(IDENTITY_SECRET), - "identity secret plaintext leaked into logs:\n{log_output}" - ); - // Sanity: redaction marker present. - assert!(log_output.contains("") || log_output.contains("redacted")); -} - -#[test] -#[serial] -fn ss_aud_005_safe_error_on_missing_entry() { - if !ensure_backend_or_skip() { - return; - } - let store = store(); - let name = "ss-aud-005.never-stored"; - let _ = store.delete(name); - - let err = store.get(name).err().expect("expected NotFound"); - // The error must not embed any secret-like content; for `NotFound` - // we additionally check the Display representation is generic. - let msg = format!("{err}"); - assert!(matches!(err, SecureStoreError::NotFound)); - assert!(msg.contains("not found")); - assert!(!msg.contains(IDENTITY_SECRET)); - assert!(!msg.contains(SERVER_PASSWORD)); -} - -#[test] -#[serial] -fn ss_aud_006_delete_removes_entry() { - if !ensure_backend_or_skip() { - return; - } - let store = store(); - let name = "ss-aud-006.identity.primary"; - - let _ = store.delete(name); - store - .set(name, &Secret::from_utf8(IDENTITY_SECRET)) - .expect("set"); - assert!(store.get(name).is_ok()); - - store.delete(name).expect("delete"); - assert!(matches!(store.get(name), Err(SecureStoreError::NotFound))); - - // Second delete must fail with NotFound, never silently succeed. - assert!(matches!(store.delete(name), Err(SecureStoreError::NotFound))); -} diff --git a/poc/sqlite-storage-spike/Cargo.lock b/poc/sqlite-storage-spike/Cargo.lock deleted file mode 100644 index 8592e7c..0000000 --- a/poc/sqlite-storage-spike/Cargo.lock +++ /dev/null @@ -1,746 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[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", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[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 = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[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 = "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 = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[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 = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - -[[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 = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[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 = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "sqlite-storage-spike" -version = "0.1.0" -dependencies = [ - "rusqlite", - "tempfile", - "thiserror", - "tracing", - "tracing-subscriber", -] - -[[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 = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[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 = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[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.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[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 = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/poc/sqlite-storage-spike/Cargo.toml b/poc/sqlite-storage-spike/Cargo.toml deleted file mode 100644 index cb7409c..0000000 --- a/poc/sqlite-storage-spike/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "sqlite-storage-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: SQLite-backed LocalDatabaseRepository with a forward-only migration mechanism and repository pattern." - -# Not product code. See docs/architecture/proof-of-concept-plan.md §4. -# Authority: SRS-089 (acceptance criteria: embedded store + migration mechanism), -# SDD-077, SysDes-033/036/049/091. - -[lib] -name = "sqlite_storage_spike" -path = "src/lib.rs" - -[[bin]] -name = "sqlite-storage-cli" -path = "src/main.rs" - -[dependencies] -rusqlite = { version = "0.32", features = ["bundled"] } -thiserror = "2" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tempfile = "3" - -[dev-dependencies] diff --git a/poc/sqlite-storage-spike/README.md b/poc/sqlite-storage-spike/README.md deleted file mode 100644 index 1561b8b..0000000 --- a/poc/sqlite-storage-spike/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# SQLite Storage Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `sqlite-storage-spike` | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Prove embedded local non-secret persistence | -| Exit criterion | "Schema, migration, and repository pattern are demonstrated" | -| Authority | SRS-089 (acceptance criteria: embedded data store + migration mechanism), SDD-077, SAD-067 | - -## What it proves - -- A **schema** managed entirely inside SQLite (`bookmarks`, `settings`) - with a UNIQUE index, secondary indices, and `ON CONFLICT` upsert - semantics for the settings table. -- A **forward-only migration mechanism** keyed on - `PRAGMA user_version`. Migrations are applied atomically inside an - IMMEDIATE transaction and rolled back on failure. -- A **repository pattern**: `BookmarkRepository` and - `SettingsRepository` traits, implemented once by - `LocalDatabaseRepository`. The rest of the Rust core can depend on - the traits and avoid leaking SQLite-specific types upward - (SAD-067, SDD-077). -- **Cross-spike contract**: `Bookmark.identity_ref` is a *name*, not a - secret. The secret behind that name lives in - `SecretStorageRepository` (see `secure-storage-spike`). - -## Layout - -```text -sqlite-storage-spike/ - src/ - lib.rs # crate root, re-exports - migrate.rs # Migrator + canonical migrations (v1..v3) - repo.rs # traits, DTOs, LocalDatabaseRepository - main.rs # sqlite-storage-cli driver - tests/ - storage.rs # 11 tests — migration + repo + persistence - Cargo.toml -``` - -## Canonical migrations - -| Version | Description | -|---|---| -| 1 | Initial `bookmarks` table + UNIQUE index on (server_host, identity_ref); `settings` key/value table. | -| 2 | `ALTER TABLE bookmarks ADD COLUMN nickname TEXT`. | -| 3 | `ALTER TABLE bookmarks ADD COLUMN last_connected_at INTEGER` + descending index. | - -Future migrations are appended only. The migrator refuses to start if -the DB's `user_version` exceeds the highest known migration (downgrade -guard). - -## Reproduce - -Requires Rust stable (developed against 1.95). `rusqlite` is built with -the `bundled` feature, so no system `libsqlite3` is needed. - -```bash -cargo test -cargo run --bin sqlite-storage-cli -``` - -## Scope boundaries - -- **Non-secret data only.** Any secret material (identity private - keys, server passwords) belongs to `secure-storage-spike` / - `SecretStorageRepository`. This is the SAD-067 separation. -- **Single-process access.** Connection pooling, multi-process - contention, and concurrent transactions are not exercised here. - Production code will likely run on a single Rust core worker. -- **No encryption at rest.** SQLCipher and similar are not in scope. - Secrets must not land in this DB in the first place. -- **No backup / restore.** -- **No reverse migrations.** Forward-only is the documented policy; - rollbacks require a fresh DB. -- The migration-engine choice (`refinery`, `sqlx::migrate!`, etc.) - remains open for the product crate. The PoC's ~50-line custom - mechanism is intentional, to keep the audit surface explainable. - -## Verification log - -See `VERIFICATION.md` in this directory. diff --git a/poc/sqlite-storage-spike/VERIFICATION.md b/poc/sqlite-storage-spike/VERIFICATION.md deleted file mode 100644 index 64aede8..0000000 --- a/poc/sqlite-storage-spike/VERIFICATION.md +++ /dev/null @@ -1,94 +0,0 @@ -# Verification record — `sqlite-storage-spike` - -## Result - -PASS. The PoC exit criterion (from -`docs/architecture/proof-of-concept-plan.md` §2: "Schema, migration, -and repository pattern are demonstrated") is met. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | -| Rust toolchain | stable 1.95.0 | -| `rusqlite` | 0.32.1 (bundled SQLite) | -| `thiserror` | 2 | - -## Reproduction - -```bash -cargo test -cargo run --bin sqlite-storage-cli -``` - -## Test run - -``` -running 11 tests -test migrator_rejects_out_of_order_versions ... ok -test migrator_rejects_db_newer_than_known_migrations ... ok -test failed_migration_rolls_back_atomically ... ok -test bookmark_repo_crud_round_trip ... ok -test bookmark_repo_unique_host_identity_is_enforced ... ok -test settings_repo_upsert_and_delete ... ok -test bookmark_repo_list_orders_recent_first ... ok -test migrator_applies_only_pending_versions ... ok -test migrator_is_idempotent ... ok -test open_creates_file_and_persists_across_reopen ... ok -test migrator_brings_fresh_db_to_latest_version ... ok - -test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; - 0 filtered out; finished in 0.01s -``` - -## CLI run - -``` -INFO spike: opening /tmp/.tmp.../chanora.sqlite -INFO spike: applying migration v1 "initial bookmarks + settings" -INFO spike: applying migration v2 "add bookmarks.nickname" -INFO spike: applying migration v3 "add bookmarks.last_connected_at" -INFO spike: schema version = 3 -INFO spike: inserted bookmark ids: 1, 2 - -Bookmarks (most-recently-connected first): - [1] Vigorous Pro @ cn.teamspeak.app (identity_ref=identity.primary, - last_connected_at=Some(1715000000)) - [2] Local Lab @ ts.example.invalid (identity_ref=identity.lab, - last_connected_at=None) - -Settings.audio.aec = Some("true") - -OK — schema v3 loaded, repository round-trip verified. -``` - -## Coverage matrix - -| Concern | Test(s) | Result | -|---|---|---| -| Schema present (SDD-077) | All tests open the schema | PASS | -| Migration mechanism exists (SRS-089) | `migrator_*` (6 tests) | PASS | -| Idempotency | `migrator_is_idempotent`, `open_creates_file_and_persists_across_reopen` | PASS | -| Partial-upgrade catch-up | `migrator_applies_only_pending_versions` | PASS | -| Out-of-order migrations rejected | `migrator_rejects_out_of_order_versions` | PASS | -| Downgrade-guard | `migrator_rejects_db_newer_than_known_migrations` | PASS | -| Atomic rollback on failure | `failed_migration_rolls_back_atomically` | PASS | -| Repository pattern (CRUD) | `bookmark_repo_crud_round_trip`, `settings_repo_upsert_and_delete` | PASS | -| Ordering invariants | `bookmark_repo_list_orders_recent_first` | PASS | -| Constraint enforcement | `bookmark_repo_unique_host_identity_is_enforced` | PASS | -| On-disk persistence | `open_creates_file_and_persists_across_reopen` | PASS | -| SAD-067 separation (no secrets in DB) | DTO design (`identity_ref` is a *name*); cross-checked in `secure-storage-spike` SS-AUD-001/002 | PASS by construction | - -## What this spike does NOT validate - -- Concurrent access from multiple processes or threads. -- WAL checkpoint behaviour under sustained writes. -- Database growth / vacuum policy. -- Encrypted-at-rest variants (SQLCipher / SEE). -- Behaviour with corrupted or partially-written DB files. -- Mobile-specific storage paths (Android scoped storage, iOS app - group containers). -- The eventual production choice of migration engine. -- Performance characteristics under realistic dataset sizes. diff --git a/poc/sqlite-storage-spike/src/lib.rs b/poc/sqlite-storage-spike/src/lib.rs deleted file mode 100644 index 7628bbb..0000000 --- a/poc/sqlite-storage-spike/src/lib.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Chanora PoC — SQLite storage spike. -//! -//! Authority: -//! * `docs/architecture/proof-of-concept-plan.md` §2 — SQLite storage spike. -//! Exit criterion: "Schema, migration, and repository pattern are demonstrated." -//! * SRS-089 — "The software shall use SQLite or an equivalent embedded -//! data store for non-secret local data." Acceptance criteria: -//! "Storage implementation uses an embedded local data store and -//! migration mechanism." -//! * SDD-077 — `LocalDatabaseRepository` persists non-secret local state -//! through SQLite or equivalent. -//! * SAD-067 — separation of non-secret local persistence (DB repository -//! layer) from secret persistence (platform secure storage). -//! -//! The PoC models the production shape: -//! -//! ┌───────────────────────────┐ ┌──────────────────────────┐ -//! │ BookmarkRepository (trait)│ │ SettingsRepository (trait)│ -//! └─────────────┬─────────────┘ └─────────────┬────────────┘ -//! │ │ -//! ▼ ▼ -//! ┌────────────────────────────────────────┐ -//! │ LocalDatabaseRepository (SQLite) │ -//! │ ────────────────────────────────── │ -//! │ forward-only schema migrations │ -//! │ tracked via PRAGMA user_version │ -//! └────────────────────────────────────────┘ -//! -//! Out of scope here: server passwords, identity secrets, any secret -//! material whatsoever (those belong to `secure-storage-spike` and to -//! the production `SecretStorageRepository`). - -pub mod migrate; -pub mod repo; - -pub use migrate::{Migrator, MigrationError}; -pub use repo::{ - Bookmark, BookmarkRepository, LocalDatabaseRepository, RepoError, Setting, - SettingsRepository, -}; diff --git a/poc/sqlite-storage-spike/src/main.rs b/poc/sqlite-storage-spike/src/main.rs deleted file mode 100644 index 2720582..0000000 --- a/poc/sqlite-storage-spike/src/main.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! CLI driver — exercises the full repository surface against an -//! on-disk SQLite file in a temporary directory. - -use std::process::ExitCode; - -use sqlite_storage_spike::{ - repo::NewBookmark, BookmarkRepository, LocalDatabaseRepository, SettingsRepository, -}; -use tracing::info; - -fn main() -> ExitCode { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .init(); - - if let Err(e) = run() { - eprintln!("error: {e}"); - return ExitCode::from(1); - } - ExitCode::SUCCESS -} - -fn run() -> Result<(), Box> { - let tmp = tempfile::tempdir()?; - let db_path = tmp.path().join("chanora.sqlite"); - info!(target: "spike", "opening {}", db_path.display()); - - let repo = LocalDatabaseRepository::open(&db_path)?; - info!(target: "spike", "schema version = {}", repo.schema_version()?); - - let id_a = repo.insert(NewBookmark { - server_name: "Vigorous Pro", - server_host: "cn.teamspeak.app", - identity_ref: "identity.primary", - nickname: Some("ChanoraPoC"), - })?; - let id_b = repo.insert(NewBookmark { - server_name: "Local Lab", - server_host: "ts.example.invalid", - identity_ref: "identity.lab", - nickname: None, - })?; - info!(target: "spike", "inserted bookmark ids: {id_a}, {id_b}"); - - repo.touch_connected(id_a, 1_715_000_000)?; - SettingsRepository::put(&repo, "audio.aec", "true")?; - SettingsRepository::put(&repo, "audio.ns", "true")?; - - let list = BookmarkRepository::list_ordered(&repo)?; - println!("\nBookmarks (most-recently-connected first):"); - for b in &list { - println!( - " [{}] {} @ {} (identity_ref={}, last_connected_at={:?})", - b.id, b.server_name, b.server_host, b.identity_ref, b.last_connected_at - ); - } - - let aec = SettingsRepository::get(&repo, "audio.aec")?; - println!("\nSettings.audio.aec = {aec:?}"); - - println!("\nOK — schema v{} loaded, repository round-trip verified.", repo.schema_version()?); - Ok(()) -} diff --git a/poc/sqlite-storage-spike/src/migrate.rs b/poc/sqlite-storage-spike/src/migrate.rs deleted file mode 100644 index ee56543..0000000 --- a/poc/sqlite-storage-spike/src/migrate.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Forward-only schema migration mechanism. -//! -//! Approach: each migration is a `&'static str` SQL script applied in -//! order. The current schema version is tracked in SQLite's -//! `PRAGMA user_version`. Running the migrator is idempotent — already- -//! applied versions are skipped. -//! -//! Why not an external migration framework? At this stage we want the -//! mechanism in the audit surface to be explainable in ~50 lines and -//! to have no transitive crate dependencies. Production code may -//! promote this to `refinery`, `sqlx::migrate!`, or similar if the -//! complexity warrants. - -use rusqlite::{Connection, TransactionBehavior}; -use thiserror::Error; -use tracing::info; - -/// A single migration step. `version` is monotonically increasing -/// starting at 1. `sql` is applied inside a transaction. -#[derive(Debug, Clone, Copy)] -pub struct Migration { - pub version: u32, - pub name: &'static str, - pub sql: &'static str, -} - -#[derive(Debug, Error)] -pub enum MigrationError { - #[error("sqlite error during migration: {0}")] - Sqlite(#[from] rusqlite::Error), - #[error("migrations must be ordered by ascending version; got {got} after {prev}")] - OutOfOrder { prev: u32, got: u32 }, - #[error("database is at version {db}, which is newer than the highest known migration {max}")] - FromTheFuture { db: u32, max: u32 }, -} - -/// Forward-only migrator over a fixed list of `Migration` steps. -pub struct Migrator { - migrations: Vec, -} - -impl Migrator { - pub fn new(migrations: Vec) -> Result { - let mut prev = 0u32; - for m in &migrations { - if m.version <= prev { - return Err(MigrationError::OutOfOrder { prev, got: m.version }); - } - prev = m.version; - } - Ok(Self { migrations }) - } - - pub fn highest_version(&self) -> u32 { - self.migrations.last().map(|m| m.version).unwrap_or(0) - } - - /// Apply every pending migration to `conn`. Idempotent: if the DB - /// is already at the highest version, this is a no-op. Each - /// migration is wrapped in an IMMEDIATE transaction; on failure - /// the transaction is rolled back and the DB version is unchanged. - pub fn run(&self, conn: &mut Connection) -> Result { - let current: u32 = - conn.query_row("PRAGMA user_version", [], |r| r.get::<_, i64>(0))? as u32; - - if current > self.highest_version() { - return Err(MigrationError::FromTheFuture { - db: current, - max: self.highest_version(), - }); - } - - let pending: Vec<&Migration> = self - .migrations - .iter() - .filter(|m| m.version > current) - .collect(); - - if pending.is_empty() { - info!(target: "spike", "no migrations to apply (already at v{current})"); - return Ok(current); - } - - for m in pending { - info!(target: "spike", "applying migration v{} {:?}", m.version, m.name); - let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; - tx.execute_batch(m.sql)?; - // PRAGMA user_version cannot be parameterised; the version - // is from our own static migration list and is therefore - // safe to interpolate. - tx.execute_batch(&format!("PRAGMA user_version = {}", m.version))?; - tx.commit()?; - } - - let final_v: u32 = - conn.query_row("PRAGMA user_version", [], |r| r.get::<_, i64>(0))? as u32; - Ok(final_v) - } -} - -/// Canonical Chanora PoC migration list. -/// -/// Each version is intentionally small and self-contained. Migrations -/// in the production crate will live in `migrations/` as separate -/// files; the PoC keeps them inline for visibility. -pub fn canonical_migrations() -> Vec { - vec![ - Migration { - version: 1, - name: "initial bookmarks + settings", - sql: r#" - CREATE TABLE bookmarks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - server_name TEXT NOT NULL, - server_host TEXT NOT NULL, - identity_ref TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')) - ); - CREATE UNIQUE INDEX ix_bookmarks_host_identity - ON bookmarks(server_host, identity_ref); - - CREATE TABLE settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - "#, - }, - Migration { - version: 2, - name: "add bookmarks.nickname", - sql: r#" - ALTER TABLE bookmarks ADD COLUMN nickname TEXT; - "#, - }, - Migration { - version: 3, - name: "add bookmarks.last_connected_at", - sql: r#" - ALTER TABLE bookmarks ADD COLUMN last_connected_at INTEGER; - CREATE INDEX ix_bookmarks_last_connected - ON bookmarks(last_connected_at DESC); - "#, - }, - ] -} diff --git a/poc/sqlite-storage-spike/src/repo.rs b/poc/sqlite-storage-spike/src/repo.rs deleted file mode 100644 index 4f10d24..0000000 --- a/poc/sqlite-storage-spike/src/repo.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Repository pattern over the SQLite schema. -//! -//! `LocalDatabaseRepository` is the single concrete type that owns the -//! `Connection`. Repository traits (`BookmarkRepository`, -//! `SettingsRepository`) describe the API surface the rest of the Rust -//! core depends on. This mirrors the production split where the -//! Chanora core consumes traits rather than the concrete SQLite type -//! (SAD-067, SDD-077). - -use std::path::Path; - -use rusqlite::{params, Connection, OptionalExtension}; -use thiserror::Error; - -use crate::migrate::{canonical_migrations, Migrator}; - -#[derive(Debug, Error)] -pub enum RepoError { - #[error("sqlite error: {0}")] - Sqlite(#[from] rusqlite::Error), - #[error("migration error: {0}")] - Migration(#[from] crate::migrate::MigrationError), - #[error("not found")] - NotFound, -} - -// ---------- DTOs ---------- - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Bookmark { - pub id: i64, - pub server_name: String, - pub server_host: String, - /// Stable reference to a secret in platform secure storage. - /// **Not** the secret itself (SAD-067, see secure-storage-spike). - pub identity_ref: String, - pub nickname: Option, - /// Unix epoch seconds; `None` if never connected. - pub last_connected_at: Option, - pub created_at: i64, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Setting { - pub key: String, - pub value: String, -} - -// ---------- Traits ---------- - -pub trait BookmarkRepository { - fn insert(&self, b: NewBookmark<'_>) -> Result; - fn get(&self, id: i64) -> Result; - fn list_ordered(&self) -> Result, RepoError>; - fn touch_connected(&self, id: i64, at_epoch_secs: i64) -> Result<(), RepoError>; - fn delete(&self, id: i64) -> Result<(), RepoError>; -} - -pub trait SettingsRepository { - fn put(&self, key: &str, value: &str) -> Result<(), RepoError>; - fn get(&self, key: &str) -> Result, RepoError>; - fn delete(&self, key: &str) -> Result; -} - -#[derive(Debug, Clone)] -pub struct NewBookmark<'a> { - pub server_name: &'a str, - pub server_host: &'a str, - pub identity_ref: &'a str, - pub nickname: Option<&'a str>, -} - -// ---------- Concrete repository ---------- - -pub struct LocalDatabaseRepository { - conn: Connection, -} - -impl LocalDatabaseRepository { - /// Open (or create) a SQLite database at `path` and bring the - /// schema up to date via the migrator. - pub fn open(path: &Path) -> Result { - let mut conn = Connection::open(path)?; - Self::pragmas(&conn)?; - let migrator = Migrator::new(canonical_migrations()).map_err(RepoError::Migration)?; - migrator.run(&mut conn).map_err(RepoError::Migration)?; - Ok(Self { conn }) - } - - /// Open an in-memory DB for tests. - pub fn open_in_memory() -> Result { - let mut conn = Connection::open_in_memory()?; - Self::pragmas(&conn)?; - let migrator = Migrator::new(canonical_migrations()).map_err(RepoError::Migration)?; - migrator.run(&mut conn).map_err(RepoError::Migration)?; - Ok(Self { conn }) - } - - fn pragmas(conn: &Connection) -> Result<(), RepoError> { - // Durability + concurrency defaults that match production - // expectations for a desktop/mobile embedded DB. - conn.execute_batch( - "PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - PRAGMA foreign_keys = ON;", - )?; - Ok(()) - } - - /// Current schema version (PRAGMA user_version). - pub fn schema_version(&self) -> Result { - let v: i64 = self - .conn - .query_row("PRAGMA user_version", [], |r| r.get(0))?; - Ok(v as u32) - } -} - -impl BookmarkRepository for LocalDatabaseRepository { - fn insert(&self, b: NewBookmark<'_>) -> Result { - self.conn.execute( - "INSERT INTO bookmarks (server_name, server_host, identity_ref, nickname) - VALUES (?1, ?2, ?3, ?4)", - params![b.server_name, b.server_host, b.identity_ref, b.nickname], - )?; - Ok(self.conn.last_insert_rowid()) - } - - fn get(&self, id: i64) -> Result { - self.conn - .query_row( - "SELECT id, server_name, server_host, identity_ref, nickname, - last_connected_at, created_at - FROM bookmarks WHERE id = ?1", - params![id], - row_to_bookmark, - ) - .optional()? - .ok_or(RepoError::NotFound) - } - - fn list_ordered(&self) -> Result, RepoError> { - let mut stmt = self.conn.prepare( - "SELECT id, server_name, server_host, identity_ref, nickname, - last_connected_at, created_at - FROM bookmarks - ORDER BY last_connected_at DESC NULLS LAST, server_name ASC", - )?; - let rows = stmt.query_map([], row_to_bookmark)?; - let mut out = Vec::new(); - for r in rows { - out.push(r?); - } - Ok(out) - } - - fn touch_connected(&self, id: i64, at_epoch_secs: i64) -> Result<(), RepoError> { - let n = self.conn.execute( - "UPDATE bookmarks SET last_connected_at = ?2 WHERE id = ?1", - params![id, at_epoch_secs], - )?; - if n == 0 { - return Err(RepoError::NotFound); - } - Ok(()) - } - - fn delete(&self, id: i64) -> Result<(), RepoError> { - let n = self - .conn - .execute("DELETE FROM bookmarks WHERE id = ?1", params![id])?; - if n == 0 { - return Err(RepoError::NotFound); - } - Ok(()) - } -} - -impl SettingsRepository for LocalDatabaseRepository { - fn put(&self, key: &str, value: &str) -> Result<(), RepoError> { - self.conn.execute( - "INSERT INTO settings (key, value) VALUES (?1, ?2) - ON CONFLICT(key) DO UPDATE SET value = excluded.value", - params![key, value], - )?; - Ok(()) - } - - fn get(&self, key: &str) -> Result, RepoError> { - Ok(self - .conn - .query_row( - "SELECT value FROM settings WHERE key = ?1", - params![key], - |r| r.get(0), - ) - .optional()?) - } - - fn delete(&self, key: &str) -> Result { - let n = self - .conn - .execute("DELETE FROM settings WHERE key = ?1", params![key])?; - Ok(n > 0) - } -} - -fn row_to_bookmark(row: &rusqlite::Row<'_>) -> rusqlite::Result { - Ok(Bookmark { - id: row.get(0)?, - server_name: row.get(1)?, - server_host: row.get(2)?, - identity_ref: row.get(3)?, - nickname: row.get(4)?, - last_connected_at: row.get(5)?, - created_at: row.get(6)?, - }) -} diff --git a/poc/sqlite-storage-spike/tests/storage.rs b/poc/sqlite-storage-spike/tests/storage.rs deleted file mode 100644 index 8f8b1bf..0000000 --- a/poc/sqlite-storage-spike/tests/storage.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! Verification tests mapped to SRS-089's acceptance criteria: -//! "Storage implementation uses an embedded local data store and -//! migration mechanism." -//! -//! And to the PoC plan §2 exit criterion: -//! "Schema, migration, and repository pattern are demonstrated." - -use rusqlite::Connection; -use sqlite_storage_spike::{ - migrate::{canonical_migrations, Migration, Migrator}, - repo::NewBookmark, - BookmarkRepository, LocalDatabaseRepository, SettingsRepository, -}; - -// ---------- Migration mechanism ---------- - -#[test] -fn migrator_brings_fresh_db_to_latest_version() { - let mut conn = Connection::open_in_memory().unwrap(); - let migrator = Migrator::new(canonical_migrations()).unwrap(); - - let final_v = migrator.run(&mut conn).unwrap(); - assert_eq!(final_v, migrator.highest_version()); -} - -#[test] -fn migrator_is_idempotent() { - let mut conn = Connection::open_in_memory().unwrap(); - let migrator = Migrator::new(canonical_migrations()).unwrap(); - let v1 = migrator.run(&mut conn).unwrap(); - let v2 = migrator.run(&mut conn).unwrap(); - let v3 = migrator.run(&mut conn).unwrap(); - assert_eq!(v1, v2); - assert_eq!(v2, v3); -} - -#[test] -fn migrator_applies_only_pending_versions() { - let mut conn = Connection::open_in_memory().unwrap(); - - // Apply only v1 first. - let first = Migrator::new(vec![canonical_migrations()[0]]).unwrap(); - assert_eq!(first.run(&mut conn).unwrap(), 1); - - // Now apply the full list — must catch up from v1 to v3 cleanly. - let full = Migrator::new(canonical_migrations()).unwrap(); - assert_eq!(full.run(&mut conn).unwrap(), 3); - - // Schema reflects v2 + v3 column additions. - let cols: Vec = conn - .prepare("SELECT name FROM pragma_table_info('bookmarks')") - .unwrap() - .query_map([], |r| r.get::<_, String>(0)) - .unwrap() - .collect::>() - .unwrap(); - assert!(cols.contains(&"nickname".to_string())); - assert!(cols.contains(&"last_connected_at".to_string())); -} - -#[test] -fn migrator_rejects_out_of_order_versions() { - let bogus = vec![ - Migration { version: 2, name: "a", sql: "" }, - Migration { version: 1, name: "b", sql: "" }, - ]; - assert!(Migrator::new(bogus).is_err()); -} - -#[test] -fn migrator_rejects_db_newer_than_known_migrations() { - let mut conn = Connection::open_in_memory().unwrap(); - conn.execute_batch("PRAGMA user_version = 99").unwrap(); - let migrator = Migrator::new(canonical_migrations()).unwrap(); - assert!(migrator.run(&mut conn).is_err()); -} - -#[test] -fn failed_migration_rolls_back_atomically() { - let mut conn = Connection::open_in_memory().unwrap(); - - // v1 ok; v2 deliberately broken. - let broken = vec![ - canonical_migrations()[0], - Migration { - version: 2, - name: "broken", - sql: "ALTER TABLE not_a_table ADD COLUMN x TEXT;", - }, - ]; - let m = Migrator::new(broken).unwrap(); - assert!(m.run(&mut conn).is_err()); - - // We should still be on v1, not partially upgraded. - let v: i64 = conn - .query_row("PRAGMA user_version", [], |r| r.get(0)) - .unwrap(); - assert_eq!(v, 1); -} - -// ---------- Repository pattern ---------- - -#[test] -fn bookmark_repo_crud_round_trip() { - let repo = LocalDatabaseRepository::open_in_memory().unwrap(); - let id = BookmarkRepository::insert( - &repo, - NewBookmark { - server_name: "Vigorous", - server_host: "cn.teamspeak.app", - identity_ref: "identity.primary", - nickname: Some("ChanoraPoC"), - }, - ) - .unwrap(); - - let got = BookmarkRepository::get(&repo, id).unwrap(); - assert_eq!(got.server_name, "Vigorous"); - assert_eq!(got.server_host, "cn.teamspeak.app"); - assert_eq!(got.identity_ref, "identity.primary"); - assert_eq!(got.nickname.as_deref(), Some("ChanoraPoC")); - assert!(got.last_connected_at.is_none()); - - BookmarkRepository::touch_connected(&repo, id, 1_700_000_000).unwrap(); - let again = BookmarkRepository::get(&repo, id).unwrap(); - assert_eq!(again.last_connected_at, Some(1_700_000_000)); - - BookmarkRepository::delete(&repo, id).unwrap(); - assert!(matches!( - BookmarkRepository::get(&repo, id), - Err(sqlite_storage_spike::RepoError::NotFound) - )); -} - -#[test] -fn bookmark_repo_list_orders_recent_first() { - let repo = LocalDatabaseRepository::open_in_memory().unwrap(); - let a = BookmarkRepository::insert( - &repo, - NewBookmark { server_name: "A", server_host: "a.example", identity_ref: "i.a", nickname: None }, - ) - .unwrap(); - let b = BookmarkRepository::insert( - &repo, - NewBookmark { server_name: "B", server_host: "b.example", identity_ref: "i.b", nickname: None }, - ) - .unwrap(); - let _c = BookmarkRepository::insert( - &repo, - NewBookmark { server_name: "C", server_host: "c.example", identity_ref: "i.c", nickname: None }, - ) - .unwrap(); - BookmarkRepository::touch_connected(&repo, b, 200).unwrap(); - BookmarkRepository::touch_connected(&repo, a, 100).unwrap(); - // c never connected. - - let list = BookmarkRepository::list_ordered(&repo).unwrap(); - let names: Vec<_> = list.iter().map(|x| x.server_name.as_str()).collect(); - // b (200), a (100), then c (NULL last) — c sorts after by name fallback. - assert_eq!(names, vec!["B", "A", "C"]); -} - -#[test] -fn bookmark_repo_unique_host_identity_is_enforced() { - let repo = LocalDatabaseRepository::open_in_memory().unwrap(); - BookmarkRepository::insert( - &repo, - NewBookmark { - server_name: "Vigorous", - server_host: "cn.teamspeak.app", - identity_ref: "identity.primary", - nickname: None, - }, - ) - .unwrap(); - let dup = BookmarkRepository::insert( - &repo, - NewBookmark { - server_name: "Vigorous Again", - server_host: "cn.teamspeak.app", - identity_ref: "identity.primary", - nickname: None, - }, - ); - assert!(dup.is_err(), "expected UNIQUE violation"); -} - -#[test] -fn settings_repo_upsert_and_delete() { - let repo = LocalDatabaseRepository::open_in_memory().unwrap(); - SettingsRepository::put(&repo, "audio.aec", "true").unwrap(); - assert_eq!( - SettingsRepository::get(&repo, "audio.aec").unwrap().as_deref(), - Some("true") - ); - - // Upsert (same key, new value). - SettingsRepository::put(&repo, "audio.aec", "false").unwrap(); - assert_eq!( - SettingsRepository::get(&repo, "audio.aec").unwrap().as_deref(), - Some("false") - ); - - assert!(SettingsRepository::delete(&repo, "audio.aec").unwrap()); - assert!(SettingsRepository::get(&repo, "audio.aec").unwrap().is_none()); - assert!(!SettingsRepository::delete(&repo, "audio.aec").unwrap()); -} - -// ---------- End-to-end on disk ---------- - -#[test] -fn open_creates_file_and_persists_across_reopen() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("chanora.sqlite"); - - { - let repo = LocalDatabaseRepository::open(&path).unwrap(); - assert_eq!(repo.schema_version().unwrap(), 3); - BookmarkRepository::insert( - &repo, - NewBookmark { - server_name: "Persist", - server_host: "persist.example", - identity_ref: "i.persist", - nickname: None, - }, - ) - .unwrap(); - } - - // Reopen and verify durability + migrator no-ops. - let repo2 = LocalDatabaseRepository::open(&path).unwrap(); - assert_eq!(repo2.schema_version().unwrap(), 3); - let list = BookmarkRepository::list_ordered(&repo2).unwrap(); - assert_eq!(list.len(), 1); - assert_eq!(list[0].server_name, "Persist"); -} diff --git a/poc/ts3-manual-research/README.md b/poc/ts3-manual-research/README.md deleted file mode 100644 index 88c8156..0000000 --- a/poc/ts3-manual-research/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# TS3 Manual Research Spike - -Chanora proof-of-concept. **Not product code.** - -## Purpose - -Prove that `docs/ts3_development_manual_en_v6.md` was read and reduced into the -specific protocol guidance needed for the TeamSpeak research spikes in this -repository. - -## What it covers - -1. The file-transfer bootstrap flow: `ftinitdownload` / `ftinitupload` plus the - second TCP connection for raw bytes. -2. The client-variable surfaces relevant to user inspection: stable UID, - DBID, nickname, version, platform, avatar, and related profile fields. -3. Identity import/export and security-level concepts needed to parse and reuse - TeamSpeak identity exports. -4. The mapping from those manual sections to the executable PoCs under `poc/`. - -## Manual sections used - -- Section 9: variable parameters / client fields -- Section 12 and later references to `client_unique_identifier` -- Section 30: file transfer protocol -- Section 32: identity and security level - -## Output - -The verification record documents the extracted conclusions and which runnable -PoC proves each one. - -## Verification - -See `VERIFICATION.md`. diff --git a/poc/ts3-manual-research/VERIFICATION.md b/poc/ts3-manual-research/VERIFICATION.md deleted file mode 100644 index 8bd9bd1..0000000 --- a/poc/ts3-manual-research/VERIFICATION.md +++ /dev/null @@ -1,60 +0,0 @@ -# Verification record — `ts3-manual-research` - -## Result - -PASS. The TeamSpeak development manual at -`docs/ts3_development_manual_en_v6.md` was read and the relevant protocol -guidance was extracted into concrete conclusions that were then exercised by the -executable TeamSpeak PoCs in `poc/`. - -## Source document - -- `docs/ts3_development_manual_en_v6.md` - -## Key extracted findings - -### 1. File download path - -The manual states that TeamSpeak file transfer is a two-phase flow: - -1. send `ftinitdownload` on the control channel; -2. receive transfer token, host, and port; -3. open a second TCP connection and exchange raw bytes. - -This directly informed `poc/tsclientlib-filetransfer-spike`. - -### 2. User-profile variable surfaces - -The manual identifies the client fields and lookup surfaces relevant to this -task: - -- `client_unique_identifier` -- `client_database_id` -- `client_nickname` -- `client_version` -- `client_platform` -- avatar-related fields - -This directly informed `poc/tsclientlib-query-spike`. - -### 3. Identity import/export compatibility - -The manual describes TeamSpeak identity material as keypair + counter/security -data and explicitly recommends keeping import/export compatible with the -official client format where possible. - -This directly informed `poc/tsidentity-import-spike`. - -## Cross-check against runnable PoCs - -| Manual finding | Runnable PoC | Verification status | -|---|---|---| -| `ftinitdownload` bootstraps a second TCP transfer | `poc/tsclientlib-filetransfer-spike` | PASS | -| Client UID / DBID / profile / connection data can be surfaced from client protocol state | `poc/tsclientlib-query-spike` | PASS | -| Official-style identity export can be parsed, round-tripped, and reused for connect | `poc/tsidentity-import-spike` | PASS | - -## Conclusion - -The manual-reading requirement is satisfied with a concrete repo artifact and -its extracted protocol guidance is reflected in the corresponding executable -PoCs. diff --git a/poc/tsclientlib-channel-query-spike/.gitignore b/poc/tsclientlib-channel-query-spike/.gitignore deleted file mode 100644 index b83d222..0000000 --- a/poc/tsclientlib-channel-query-spike/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/poc/tsclientlib-channel-query-spike/Cargo.lock b/poc/tsclientlib-channel-query-spike/Cargo.lock deleted file mode 100644 index 2ea8f72..0000000 --- a/poc/tsclientlib-channel-query-spike/Cargo.lock +++ /dev/null @@ -1,3453 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[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 = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[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 = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[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.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[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 = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest 0.10.7", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -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 = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[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 = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[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 = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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" -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", - "zeroize", -] - -[[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 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - -[[package]] -name = "git-testament" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74999c921479f919c87a9d2e6922a79a18683f18105344df8e067149232e51" -dependencies = [ - "git-testament-derive", -] - -[[package]] -name = "git-testament-derive" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeac967e71eb3dc1656742fc7521ec7cd3b6b88738face65bf1fddf702bc4c0" -dependencies = [ - "log", - "proc-macro2", - "quote", - "syn", - "time", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-net" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "futures-channel", - "futures-io", - "futures-util", - "hickory-proto", - "idna", - "ipnet", - "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" -dependencies = [ - "data-encoding", - "idna", - "ipnet", - "jni", - "once_cell", - "prefix-trie", - "rand 0.10.1", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-net", - "hickory-proto", - "ipconfig", - "ipnet", - "jni", - "moka", - "ndk-context", - "once_cell", - "parking_lot", - "rand 0.10.1", - "resolv-conf", - "smallvec", - "system-configuration", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[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 = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -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.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2", - "widestring", - "windows-registry", - "windows-result", - "windows-sys 0.61.2", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -dependencies = [ - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[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 = "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.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -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 = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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 = "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 = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "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 = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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", -] - -[[package]] -name = "omnom" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66562a92c66d5937e9d549597c2d3cafe3d2367dd08dc257da730587c2cfc218" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[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 = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[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 = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[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 = "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 = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - -[[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 = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[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 = "quicklz" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750eab265a26a0cd064f2a2b222bf172cc06d7a873e113e5f6d0118cc6e5fbb1" -dependencies = [ - "bit-vec", - "byteorder", - "thiserror 1.0.69", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "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 = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[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 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[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_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "reqwest" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "aws-lc-rs", - "once_cell", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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 = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "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 = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[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 = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[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" - -[[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 = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[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 = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[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 = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "t4rust-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e411692d6e0c8ce07f65a8210e95dcfb56cc4600940456592a49a3579ef6396" -dependencies = [ - "nom", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[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", - "num-conv", - "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 = "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.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "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 = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ts-bookkeeping" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "heck", - "itertools", - "num-derive", - "num-traits", - "serde", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tracing", - "tsproto-packets", - "tsproto-structs", - "tsproto-types", -] - -[[package]] -name = "tsclientlib" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "futures", - "git-testament", - "hickory-net", - "hickory-resolver", - "itertools", - "num-traits", - "pin-utils", - "rand 0.10.1", - "reqwest", - "thiserror 2.0.18", - "time", - "tokio", - "tokio-stream", - "tracing", - "ts-bookkeeping", - "tsproto", - "tsproto-packets", - "tsproto-types", - "url", -] - -[[package]] -name = "tsclientlib-channel-query-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "futures", - "time", - "tokio", - "tracing", - "tracing-subscriber", - "tsclientlib", - "tsproto-packets", -] - -[[package]] -name = "tsproto" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "aes", - "base64", - "curve25519-dalek-ng", - "eax", - "futures", - "generic-array", - "git-testament", - "num-bigint", - "num-derive", - "num-traits", - "omnom", - "pin-project-lite", - "quicklz", - "rand 0.10.1", - "serde", - "sha-1", - "sha2", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "tsproto-packets", - "tsproto-types", -] - -[[package]] -name = "tsproto-packets" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "num-derive", - "num-traits", - "omnom", - "self_cell", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "tsproto-structs" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "csv", - "heck", - "once_cell", - "serde", - "toml", -] - -[[package]] -name = "tsproto-types" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "curve25519-dalek-ng", - "elliptic-curve", - "generic-array", - "heck", - "num-bigint", - "num-derive", - "num-traits", - "p256", - "rand 0.8.6", - "ref-cast", - "serde", - "sha-1", - "simple_asn1", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tsproto-structs", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.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 = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[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-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[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.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.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.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.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.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.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.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.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.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 = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -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/poc/tsclientlib-channel-query-spike/Cargo.toml b/poc/tsclientlib-channel-query-spike/Cargo.toml deleted file mode 100644 index 6d8eb4b..0000000 --- a/poc/tsclientlib-channel-query-spike/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "tsclientlib-channel-query-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: inspect live TeamSpeak channel metadata over the full client protocol." - -[workspace] - -[dependencies] -anyhow = "1" -clap = { version = "4", features = ["derive"] } -futures = "0.3" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -time = "0.3" -tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["default-tls"] } -tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", package = "tsproto-packets" } diff --git a/poc/tsclientlib-channel-query-spike/README.md b/poc/tsclientlib-channel-query-spike/README.md deleted file mode 100644 index 3fb6eef..0000000 --- a/poc/tsclientlib-channel-query-spike/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# tsclientlib Channel Query Spike - -Chanora proof-of-concept. **Not product code.** - -## Purpose - -Prove what channel metadata can be observed from a normal TeamSpeak client -connection to `kr.teamspeak.app` without ServerQuery credentials. - -## What it does - -1. Connects to a TeamSpeak server with `tsclientlib`. -2. Subscribes to the server tree and pumps events so the live channel snapshot - is populated. -3. Prints the visible channel table. -4. Resolves a target channel by ID or exact name, or falls back to the current - channel. -5. Requests the channel description with `channelgetdescription`. -6. Prints channel metadata, flags, codec settings, capacity settings, child - channels, and current occupants from the live connection state. - -## Run - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --nickname ChanoraPoC-ChannelInspector \ - --target-channel-id 1 -``` - -Useful flags: - -- `--target-channel-id ` -- `--target-channel-name ` -- `--identity ` - -## Scope boundaries - -- No ServerQuery login or administrative credentials. -- No attempt to bypass permission-gated data. -- No persistence of the generated identity. - -## Verification - -See `VERIFICATION.md`. diff --git a/poc/tsclientlib-channel-query-spike/VERIFICATION.md b/poc/tsclientlib-channel-query-spike/VERIFICATION.md deleted file mode 100644 index 0ccab35..0000000 --- a/poc/tsclientlib-channel-query-spike/VERIFICATION.md +++ /dev/null @@ -1,60 +0,0 @@ -# Verification record — `tsclientlib-channel-query-spike` - -## Result - -PASS. On 2026-05-25, the spike connected to `kr.teamspeak.app`, listed the -visible channels, resolved channel `1`, requested its description, and printed -live channel metadata plus current occupants from a normal TeamSpeak client -session. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-25 | -| Host OS | Linux (x86_64) | -| Build profile | dev | -| Target server | `kr.teamspeak.app` | - -## Command - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --nickname ChanoraPoC-ChannelInspector \ - --target-channel-id 1 -``` - -## What was observed - -- The visible channel table populated successfully from the live client state. -- Channel `1` resolved as `Default Channel`. -- `channelgetdescription` succeeded and populated the channel description in - `tsclientlib` state. -- Printed channel data for `Default Channel`: - - `cid=1` - - parent: `Root (0)` - - topic: `Default Channel` - - type: `Permanent` - - default channel: `Yes` - - password-protected: `No` - - codec: `OpusMusic` - - codec quality: `6` - - unencrypted: `No` - - max clients: `Unlimited` - - max family clients: `Unlimited` - - needed talk power: `-1` - - GUID: `76789d5c-8aac-4740-9d03-e66f09317153` - - icon id: `399174223` -- Channel description was visible and included bilingual contact text pointing - users to `me@edison.network`. -- Current occupancy for channel `1` during the run: - - `ChanoraPoC-ChannelInspector` - - `EdisonJwa` - - `observer` - -## Conclusion - -This spike proves that a normal TeamSpeak client connection can inspect visible -channel metadata, request channel descriptions, and enumerate current occupants -without ServerQuery credentials. diff --git a/poc/tsclientlib-channel-query-spike/src/main.rs b/poc/tsclientlib-channel-query-spike/src/main.rs deleted file mode 100644 index 995f878..0000000 --- a/poc/tsclientlib-channel-query-spike/src/main.rs +++ /dev/null @@ -1,413 +0,0 @@ -use anyhow::{anyhow, bail, Context, Result}; -use clap::Parser; -use futures::prelude::*; -use time::Duration; -use tokio::time as tokio_time; -use tracing::{info, warn}; -use tracing_subscriber::EnvFilter; - -use tsclientlib::data; -use tsclientlib::{ChannelId, Connection, DisconnectOptions, Identity, MaxClients, OutCommandExt, StreamItem}; -use tsproto_packets::packets::{Direction, Flags, OutCommand, PacketType}; - -#[derive(Parser, Debug)] -#[command( - name = "tsclientlib-channel-query-spike", - about = "Chanora PoC: inspect TeamSpeak channel information over the full client protocol." -)] -struct Args { - /// Server address (hostname[:port] or TSDNS name). - #[arg(short, long, default_value = "kr.teamspeak.app")] - address: String, - - /// Nickname used on the server. - #[arg(short, long, default_value = "ChanoraPoC-ChannelInspector")] - nickname: String, - - /// Optional TeamSpeak identity string. If omitted, a fresh identity is generated. - #[arg(long)] - identity: Option, - - /// Server password if required. - #[arg(long)] - password: Option, - - /// Inspect a specific channel ID. - #[arg(long)] - target_channel_id: Option, - - /// Inspect a specific channel name. - #[arg(long)] - target_channel_name: Option, - - /// Print the visible channel table before detailed inspection. - #[arg(long, default_value_t = true)] - show_channels: bool, -} - -#[derive(Clone, Debug)] -struct TargetChannel { - id: ChannelId, - name: String, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn")); - tracing_subscriber::fmt().with_env_filter(filter).init(); - - info!(target: "channel-query-spike", address = %args.address, nickname = %args.nickname, "starting channel query spike"); - - let mut builder = Connection::build(args.address.clone()).name(args.nickname.clone()); - let identity = match &args.identity { - Some(raw) => Identity::new_from_str(raw).context("parsing --identity")?, - None => Identity::create(), - }; - builder = builder.identity(identity); - if let Some(password) = &args.password { - builder = builder.password(password.clone()); - } - - let mut con = builder.connect().context("dialling server")?; - wait_for_initial_state(&mut con).await?; - if let Ok(state) = con.get_state() { - let _ = state.server.set_subscribed(true).send(&mut con); - } - pump_events(&mut con, std::time::Duration::from_secs(1)).await?; - - let target = { - let state = con.get_state().context("reading connection state")?; - if args.show_channels { - print_channels(state); - } - resolve_target(&args, state)? - }; - - println!(); - println!("=== Detailed Target ==="); - println!("Target channel id : {}", target.id.0); - println!("Target channel name : {}", target.name); - - if let Err(error) = request_channel_description(&mut con, target.id).await { - warn!(target: "channel-query-spike", %error, channel_id = target.id.0, "channelgetdescription failed"); - } - - let state = con.get_state().context("reading updated channel state")?; - let channel = state - .channels - .get(&target.id) - .ok_or_else(|| anyhow!("target channel disappeared from state"))?; - - print_channel_summary(state, channel)?; - - con.disconnect(DisconnectOptions::new()).ok(); - con.events().for_each(|_| futures::future::ready(())).await; - - Ok(()) -} - -async fn wait_for_initial_state(con: &mut Connection) -> Result<()> { - loop { - let event = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before initial state"))??; - if matches!(event, StreamItem::BookEvents(_)) { - return Ok(()); - } - } -} - -async fn pump_events(con: &mut Connection, budget: std::time::Duration) -> Result<()> { - let deadline = tokio_time::Instant::now() + budget; - loop { - let now = tokio_time::Instant::now(); - if now >= deadline { - return Ok(()); - } - let remaining = deadline - now; - match tokio_time::timeout(remaining, con.events().next()).await { - Ok(Some(Ok(_))) => {} - Ok(Some(Err(error))) => return Err(error).context("pumping connection events"), - Ok(None) => bail!("event stream ended while pumping events"), - Err(_) => return Ok(()), - } - } -} - -fn build_command(name: &str, args: &[(&str, String)]) -> OutCommand { - let mut command = OutCommand::new(Direction::C2S, Flags::empty(), PacketType::Command, name); - for (key, value) in args { - command.write_arg(key, value); - } - command -} - -async fn request_messages(con: &mut Connection, command: OutCommand) -> Result<()> { - let handle = command.send_with_result(con).context("sending command")?; - loop { - let item = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before command response"))??; - if let StreamItem::MessageResult(reply, status) = item { - if reply == handle { - status.map_err(|error| anyhow!("command failed: {error}"))?; - return Ok(()); - } - } - } -} - -async fn request_channel_description(con: &mut Connection, channel_id: ChannelId) -> Result<()> { - request_messages( - con, - build_command("channelgetdescription", &[("cid", channel_id.0.to_string())]), - ) - .await?; - pump_events(con, std::time::Duration::from_millis(350)).await?; - Ok(()) -} - -fn resolve_target(args: &Args, state: &data::Connection) -> Result { - if let Some(channel_id) = args.target_channel_id { - let id = ChannelId(channel_id); - let channel = state - .channels - .get(&id) - .ok_or_else(|| anyhow!("channel id {} not found in visible state", channel_id))?; - return Ok(TargetChannel { - id, - name: channel.name.clone(), - }); - } - - if let Some(name) = &args.target_channel_name { - let channel = state - .channels - .values() - .find(|channel| channel.name == *name) - .ok_or_else(|| anyhow!("channel name `{name}` not found in visible state"))?; - return Ok(TargetChannel { - id: channel.id, - name: channel.name.clone(), - }); - } - - let own_client = state - .clients - .get(&state.own_client) - .ok_or_else(|| anyhow!("own client missing from state"))?; - let channel = state - .channels - .get(&own_client.channel) - .ok_or_else(|| anyhow!("own channel missing from state"))?; - Ok(TargetChannel { - id: channel.id, - name: channel.name.clone(), - }) -} - -fn print_channels(state: &data::Connection) { - println!("=== Visible Channels ==="); - let mut channels = state.channels.values().collect::>(); - channels.sort_by_key(|channel| channel.id.0); - for channel in channels { - let direct_clients = state - .clients - .values() - .filter(|client| client.channel == channel.id) - .count(); - println!( - "- {} | cid={} parent={} type={} clients={} subscribed={} password={}", - channel.name, - channel.id.0, - channel.parent.0, - format_channel_type(channel), - direct_clients, - yes_no(channel.subscribed), - option_bool(channel.has_password), - ); - } -} - -fn print_channel_summary(state: &data::Connection, channel: &data::Channel) -> Result<()> { - let parent_name = if channel.parent.0 == 0 { - "Root".to_string() - } else { - state - .channels - .get(&channel.parent) - .map(|parent| parent.name.clone()) - .unwrap_or_else(|| format!("Unknown ({})", channel.parent.0)) - }; - - let mut direct_clients = state - .clients - .values() - .filter(|client| client.channel == channel.id) - .map(|client| format!("{} ({})", client.name, client.id.0)) - .collect::>(); - direct_clients.sort(); - - let mut child_channels = state - .channels - .values() - .filter(|candidate| candidate.parent == channel.id) - .map(|candidate| format!("{} ({})", candidate.name, candidate.id.0)) - .collect::>(); - child_channels.sort(); - - println!(); - println!("=== Channel Profile ==="); - println!("Channel ID : {}", channel.id.0); - println!("Name : {}", channel.name); - println!("Parent : {} ({})", parent_name, channel.parent.0); - println!("Topic : {}", channel.topic.as_deref().unwrap_or("")); - println!( - "Description : {}", - channel - .optional_data - .as_ref() - .map(|data| data.description.as_str()) - .unwrap_or("") - ); - println!("Type : {}", format_channel_type(channel)); - println!("Order : {}", channel.order.0); - println!("Is Default : {}", option_bool(channel.is_default)); - println!("Has Password : {}", option_bool(channel.has_password)); - println!("Is Private : {}", option_bool(channel.is_private)); - println!("Subscribed : {}", yes_no(channel.subscribed)); - println!("Codec : {:?}", channel.codec); - println!( - "Codec Quality : {}", - channel - .codec_quality - .map(|value| value.to_string()) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Latency Factor : {}", - channel - .codec_latency_factor - .map(|value| value.to_string()) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!("Unencrypted : {}", option_bool(channel.is_unencrypted)); - println!("Max Clients : {}", format_max_clients(channel.max_clients)); - println!( - "Max Family Clients : {}", - format_max_clients(channel.max_family_clients) - ); - println!( - "Delete Delay : {}", - channel - .delete_delay - .map(format_duration) - .unwrap_or_else(|| "None".to_string()) - ); - println!( - "Needed Talk Power : {}", - channel - .needed_talk_power - .map(|value| value.to_string()) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!("Forced Silence : {}", yes_no(channel.forced_silence)); - println!( - "Phonetic Name : {}", - channel.phonetic_name.as_deref().unwrap_or("") - ); - println!( - "GUID : {}", - channel.guid.as_deref().unwrap_or("Unknown") - ); - println!( - "Storage Quota : {}", - channel - .storage_quota - .map(|value| value.to_string()) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Icon ID : {}", - channel - .icon - .map(|value| value.0.to_string()) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!(); - println!("=== Occupancy ==="); - println!("Direct clients : {}", direct_clients.len()); - println!( - "Client list : {}", - if direct_clients.is_empty() { - "None".to_string() - } else { - direct_clients.join(", ") - } - ); - println!("Child channels : {}", child_channels.len()); - println!( - "Children list : {}", - if child_channels.is_empty() { - "None".to_string() - } else { - child_channels.join(", ") - } - ); - - Ok(()) -} - -fn format_channel_type(channel: &data::Channel) -> &'static str { - match channel.channel_type { - tsclientlib::ChannelType::Temporary => "Temporary", - tsclientlib::ChannelType::SemiPermanent => "SemiPermanent", - tsclientlib::ChannelType::Permanent => "Permanent", - } -} - -fn format_max_clients(value: Option) -> String { - match value { - Some(MaxClients::Unlimited) => "Unlimited".to_string(), - Some(MaxClients::Inherited) => "Inherited".to_string(), - Some(MaxClients::Limited(limit)) => limit.to_string(), - None => "Unknown".to_string(), - } -} - -fn option_bool(value: Option) -> &'static str { - match value { - Some(true) => "Yes", - Some(false) => "No", - None => "Unknown", - } -} - -fn yes_no(value: bool) -> &'static str { - if value { "Yes" } else { "No" } -} - -fn format_duration(duration: Duration) -> String { - let total_ms = duration.whole_milliseconds(); - if total_ms < 1000 { - return format!("{total_ms} ms"); - } - let total_seconds = duration.whole_seconds(); - let seconds = total_seconds % 60; - let minutes = (total_seconds / 60) % 60; - let hours = total_seconds / 3600; - if hours > 0 { - format!("{hours}h {minutes}m {seconds}s") - } else if minutes > 0 { - format!("{minutes}m {seconds}s") - } else { - format!("{seconds}s") - } -} diff --git a/poc/tsclientlib-connect-spike/Cargo.lock b/poc/tsclientlib-connect-spike/Cargo.lock deleted file mode 100644 index 02c0017..0000000 --- a/poc/tsclientlib-connect-spike/Cargo.lock +++ /dev/null @@ -1,3472 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[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 = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[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 = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[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.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[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 = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest 0.10.7", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -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 = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[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 = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[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 = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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" -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", - "zeroize", -] - -[[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 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - -[[package]] -name = "git-testament" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74999c921479f919c87a9d2e6922a79a18683f18105344df8e067149232e51" -dependencies = [ - "git-testament-derive", -] - -[[package]] -name = "git-testament-derive" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeac967e71eb3dc1656742fc7521ec7cd3b6b88738face65bf1fddf702bc4c0" -dependencies = [ - "log", - "proc-macro2", - "quote", - "syn", - "time", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-net" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "futures-channel", - "futures-io", - "futures-util", - "hickory-proto", - "idna", - "ipnet", - "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" -dependencies = [ - "data-encoding", - "idna", - "ipnet", - "jni", - "once_cell", - "prefix-trie", - "rand 0.10.1", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-net", - "hickory-proto", - "ipconfig", - "ipnet", - "jni", - "moka", - "ndk-context", - "once_cell", - "parking_lot", - "rand 0.10.1", - "resolv-conf", - "smallvec", - "system-configuration", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[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 = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -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.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2", - "widestring", - "windows-registry", - "windows-result", - "windows-sys 0.61.2", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -dependencies = [ - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[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 = "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.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" -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 = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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 = "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 = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "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 = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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", -] - -[[package]] -name = "omnom" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66562a92c66d5937e9d549597c2d3cafe3d2367dd08dc257da730587c2cfc218" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[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 = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[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 = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[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 = "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 = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - -[[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 = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[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 = "quicklz" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750eab265a26a0cd064f2a2b222bf172cc06d7a873e113e5f6d0118cc6e5fbb1" -dependencies = [ - "bit-vec", - "byteorder", - "thiserror 1.0.69", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "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 = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[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 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[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_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "reqwest" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "aws-lc-rs", - "once_cell", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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 = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "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 = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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 = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[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" - -[[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 = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[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 = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[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 = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "t4rust-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e411692d6e0c8ce07f65a8210e95dcfb56cc4600940456592a49a3579ef6396" -dependencies = [ - "nom", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[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", - "num-conv", - "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 = "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.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "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 = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ts-bookkeeping" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "heck", - "itertools", - "num-derive", - "num-traits", - "serde", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tracing", - "tsproto-packets", - "tsproto-structs", - "tsproto-types", -] - -[[package]] -name = "tsclientlib" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "futures", - "git-testament", - "hickory-net", - "hickory-resolver", - "itertools", - "num-traits", - "pin-utils", - "rand 0.10.1", - "reqwest", - "thiserror 2.0.18", - "time", - "tokio", - "tokio-stream", - "tracing", - "ts-bookkeeping", - "tsproto", - "tsproto-packets", - "tsproto-types", - "url", -] - -[[package]] -name = "tsclientlib-connect-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "futures", - "tokio", - "tracing", - "tracing-subscriber", - "tsclientlib", -] - -[[package]] -name = "tsproto" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "aes", - "base64", - "curve25519-dalek-ng", - "eax", - "futures", - "generic-array", - "git-testament", - "num-bigint", - "num-derive", - "num-traits", - "omnom", - "pin-project-lite", - "quicklz", - "rand 0.10.1", - "serde", - "sha-1", - "sha2", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "tsproto-packets", - "tsproto-types", -] - -[[package]] -name = "tsproto-packets" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "num-derive", - "num-traits", - "omnom", - "self_cell", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "tsproto-structs" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "csv", - "heck", - "once_cell", - "serde", - "toml", -] - -[[package]] -name = "tsproto-types" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "curve25519-dalek-ng", - "elliptic-curve", - "generic-array", - "heck", - "num-bigint", - "num-derive", - "num-traits", - "p256", - "rand 0.8.6", - "ref-cast", - "serde", - "sha-1", - "simple_asn1", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tsproto-structs", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.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 = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[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-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[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.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.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.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.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.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.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.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.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.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 = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -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/poc/tsclientlib-connect-spike/Cargo.toml b/poc/tsclientlib-connect-spike/Cargo.toml deleted file mode 100644 index 0305d4e..0000000 --- a/poc/tsclientlib-connect-spike/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "tsclientlib-connect-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: prove tsclientlib protocol feasibility by connecting to a TS3-compatible server and printing the channel tree." - -# This is PoC code, not product code. See docs/architecture/proof-of-concept-plan.md §4. - -[dependencies] -# tsclientlib is git-only (not on crates.io). -# Audio feature is disabled because this spike only proves the connect/state path; -# audio behaviour is covered by a separate PoC (audio-capture-playback-spike). -tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", default-features = false, features = ["default-tls"] } - -anyhow = "1" -clap = { version = "4", features = ["derive"] } -futures = "0.3" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "signal"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/poc/tsclientlib-connect-spike/README.md b/poc/tsclientlib-connect-spike/README.md deleted file mode 100644 index 36260d2..0000000 --- a/poc/tsclientlib-connect-spike/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# tsclientlib Connect Spike - -Chanora proof-of-concept. **Not product code.** - -| Field | Value | -|---|---| -| PoC name | `tsclientlib-connect-spike` | -| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | -| Purpose | Prove protocol feasibility via `tsclientlib` | -| Exit criterion | "Rust can connect to a compatible server/test double" | - -## What it does - -1. Generates an ephemeral identity (or accepts one via `--identity`). -2. Connects to a TeamSpeak-3-compatible server. -3. Waits for the initial state snapshot (`StreamItem::BookEvents`). -4. Subscribes to the server's channel tree. -5. Prints server metadata + the channel tree with client names. -6. Disconnects cleanly. - -## Run - -Requires Rust stable (developed against 1.95). Network access required so -that `cargo` can fetch `tsclientlib` from GitHub on first build. - -```bash -cargo run --release -- --address cn.teamspeak.app -``` - -Useful flags: - -| Flag | Meaning | -|---|---| -| `-a, --address ` | Server (default `cn.teamspeak.app`) | -| `-n, --nickname ` | Nickname (default `ChanoraPoC`) | -| `--password ` | Server password if required | -| `--identity ` | Reuse an existing identity instead of generating one | -| `--hold-secs ` | Seconds to hold the connection before disconnect (default 3) | -| `-v` / `-vv` / `-vvv` | Increase tsclientlib log verbosity | - -## Scope boundaries - -This spike is intentionally narrow: - -- No audio capture or playback (covered by `audio-capture-playback-spike`). -- No FFI surface (covered by `flutter-rust-bridge-hello`). -- No persistent identity (covered by `secure-storage-spike`). -- No diagnostic redaction policy (covered by `diagnostics-redaction-spike`). -- No Chanora architecture layering — does **not** live inside - `chanora_protocol` and does not expose typed DTOs. The intent of the - spike is to validate the upstream `tsclientlib` API surface before the - product crate is written, per the PoC plan. - -## Verification log - -See `VERIFICATION.md` in this directory for the most recent run captured -against `cn.teamspeak.app`. diff --git a/poc/tsclientlib-connect-spike/VERIFICATION.md b/poc/tsclientlib-connect-spike/VERIFICATION.md deleted file mode 100644 index f11d08e..0000000 --- a/poc/tsclientlib-connect-spike/VERIFICATION.md +++ /dev/null @@ -1,68 +0,0 @@ -# Verification record — `tsclientlib-connect-spike` - -## Result - -PASS. The PoC exit criterion (from -`docs/architecture/proof-of-concept-plan.md` §2: "Rust can connect to a -compatible server/test double") is met. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-13 | -| Host OS | Linux (x86_64) | -| Rust toolchain | stable 1.95.0 (59807616e 2026-04-14) | -| Build profile | release | -| Target server | `cn.teamspeak.app` (resolved to 175.178.125.23) | -| Network | direct outbound UDP/TCP from build host | - -## Command - -```bash -cargo build --release -./target/release/tsclientlib-connect-spike \ - --address cn.teamspeak.app \ - --hold-secs 4 \ - --nickname ChanoraPoC -``` - -Process exit code: `0`. - -## What was observed - -- Identity generated locally (ephemeral, level-0). -- UDP connection negotiated with the server. -- `StreamItem::BookEvents` snapshot received → initial state ready. -- `server.set_subscribed(true)` accepted → full channel tree streamed. -- Server identity surfaced: - - name: `Vigorous Pro` - - platform: `Linux` - - version: `3.13.7 [Build: 1655727713]` (TeamSpeak 3 server) - - clients online: 5 - - channels: 36 -- Channel tree printed with multi-byte (CJK) channel and client names - intact, validating UTF-8 end-to-end (cross-references SAD ADR-008 - "UTF-8 at all boundaries"). -- The PoC client appeared in `Default Channel` as `ChanoraPoC`. -- Clean disconnect via `DisconnectOptions::new()`; no panics or hangs. - -## Known non-blocking observations - -- `tsproto::packet_codec` emits `Out of order command packet` warnings - during the initial state burst. These are upstream-level and informational; - the snapshot still resolves correctly. Worth tracking when the - `chanora_protocol` crate is written, but not a spike blocker. -- The welcome banner uses TeamSpeak's `[URL...]`/`[cspacer0]` markup — - this is server-provided text and per DEC-015 / ADR-008 must be - preserved, not translated. The spike does not parse it. - -## Scope honestly NOT validated by this spike - -- Audio capture/playback (separate PoC). -- Persisted identity via secure storage (separate PoC). -- Reconnect / network-loss recovery. -- FFI to Flutter (separate PoC). -- Behaviour against a non-TS3 / TS5 server. -- Behaviour behind restrictive NAT or under packet loss. -- Performance characteristics under sustained load. diff --git a/poc/tsclientlib-connect-spike/src/main.rs b/poc/tsclientlib-connect-spike/src/main.rs deleted file mode 100644 index 1b387d4..0000000 --- a/poc/tsclientlib-connect-spike/src/main.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Chanora PoC: tsclientlib connect spike. -//! -//! Purpose (per `docs/architecture/proof-of-concept-plan.md` §2): -//! Prove that a Rust process can connect to a TeamSpeak-3-compatible -//! server through `tsclientlib` and observe the server state. -//! -//! Exit criterion: "Rust can connect to a compatible server/test double." -//! -//! This is PoC code. It deliberately: -//! * does not capture or play audio; -//! * does not expose any FFI surface to Flutter; -//! * does not use the Chanora architecture layers (chanora_protocol etc.); -//! * generates a fresh identity per run unless one is supplied. -//! -//! It is not promoted to product code unless explicitly migrated per -//! `docs/architecture/proof-of-concept-plan.md` §4. - -use std::time::Duration; - -use anyhow::{Context, Result}; -use clap::Parser; -use futures::prelude::*; -use tokio::time; -use tracing::{info, warn}; -use tracing_subscriber::EnvFilter; - -use tsclientlib::data::{self, Channel, Client}; -use tsclientlib::{ChannelId, Connection, DisconnectOptions, Identity, OutCommandExt, StreamItem}; - -#[derive(Parser, Debug)] -#[command( - name = "tsclientlib-connect-spike", - about = "Chanora PoC: connect to a TS3-compatible server and dump channel tree." -)] -struct Args { - /// Server address (hostname[:port] or TSDNS name). - #[arg(short, long, default_value = "cn.teamspeak.app")] - address: String, - - /// Nickname used on the server. - #[arg(short, long, default_value = "ChanoraPoC")] - nickname: String, - - /// Seconds to remain connected before disconnecting cleanly. - #[arg(long, default_value_t = 3)] - hold_secs: u64, - - /// Optional base64 identity string. If omitted, a fresh identity is generated. - #[arg(long)] - identity: Option, - - /// Server password, if the server requires one. - #[arg(long)] - password: Option, - - /// Increase log verbosity (-v, -vv, -vvv). - #[arg(short, long, action = clap::ArgAction::Count)] - verbose: u8, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - // Default to info-level logs; respect RUST_LOG if set. - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn")); - tracing_subscriber::fmt().with_env_filter(filter).init(); - - info!(target: "spike", address = %args.address, nickname = %args.nickname, "starting connect spike"); - - // Build connection config. - let mut builder = Connection::build(args.address.clone()) - .name(args.nickname.clone()) - .log_commands(args.verbose >= 1) - .log_packets(args.verbose >= 2) - .log_udp_packets(args.verbose >= 3); - - // Identity: provided or freshly generated. - let identity = match &args.identity { - Some(s) => Identity::new_from_str(s).context("parsing --identity")?, - None => { - // Generate a level-0 identity. For a real product we would persist this - // via SecureStorage (see docs/security/secure-storage-audit-report.md). - // PoC: ephemeral per run. - Identity::create() - } - }; - builder = builder.identity(identity); - - if let Some(pw) = &args.password { - builder = builder.password(pw.clone()); - } - - // Connect. - let mut con = builder.connect().context("dialling server")?; - - // Wait for the initial book/state snapshot (BookEvents == state ready). - let first = con - .events() - .try_filter(|e| future::ready(matches!(e, StreamItem::BookEvents(_)))) - .next() - .await; - match first { - Some(Ok(_)) => info!(target: "spike", "received initial state snapshot"), - Some(Err(e)) => { - anyhow::bail!("connection failed before state ready: {e}"); - } - None => { - anyhow::bail!("event stream ended before state ready"); - } - } - - // Subscribe to the server so we get the full channel tree, not just our own. - if let Ok(state) = con.get_state() { - if let Err(e) = state.server.set_subscribed(true).send(&mut con) { - warn!(target: "spike", error = %e, "could not subscribe to server tree"); - } - } - - // Give the server a moment to ship the tree, but bail out early if the - // connection disconnects in the meantime. - let mut bg = con.events().try_filter(|_| future::ready(false)); - tokio::select! { - _ = time::sleep(Duration::from_secs(args.hold_secs.min(60))) => {} - ev = bg.next() => { - if let Some(Err(e)) = ev { - anyhow::bail!("disconnected during hold window: {e}"); - } - } - } - drop(bg); - - // Print snapshot. - print_snapshot(&*con.get_state().context("reading server state")?); - - // Clean disconnect. - con.disconnect(DisconnectOptions::new()).ok(); - con.events().for_each(|_| future::ready(())).await; - - info!(target: "spike", "spike completed successfully"); - Ok(()) -} - -fn print_snapshot(con: &data::Connection) { - println!(); - println!("=== Chanora tsclientlib connect spike — server snapshot ==="); - println!("Server name : {}", sanitize(&con.server.name)); - println!("Welcome msg : {}", sanitize(&con.server.welcome_message)); - println!("Platform : {}", sanitize(&con.server.platform)); - println!("Version : {}", sanitize(&con.server.version)); - println!("Clients online : {}", con.clients.len()); - println!("Channels : {}", con.channels.len()); - println!(); - - let mut channels: Vec<&Channel> = con.channels.values().collect(); - channels.sort_by_key(|c| c.order.0); - let clients: Vec<&Client> = con.clients.values().collect(); - - println!("Channel tree:"); - print_children(&clients, &channels, ChannelId(0), 0); - println!(); -} - -fn print_children(clients: &[&Client], channels: &[&Channel], parent: ChannelId, depth: usize) { - let indent = " ".repeat(depth); - for ch in channels { - if ch.parent == parent { - println!("{}- [{}] {}", indent, ch.id.0, sanitize(&ch.name)); - for client in clients { - if client.channel == ch.id { - println!("{} * {}", indent, sanitize(&client.name)); - } - } - print_children(clients, channels, ch.id, depth + 1); - } - } -} - -/// Restrict to a safe printable set; mirrors the upstream `simple.rs` example. -/// This is *not* the Chanora redaction policy — see -/// `docs/security/diagnostic-redaction-audit-report.md`. -fn sanitize(s: &str) -> String { - s.chars() - .filter(|c| { - c.is_alphanumeric() - || [ - ' ', '\t', '.', ':', '-', '_', '"', '\'', '/', '(', ')', '[', ']', '{', '}', - '!', '?', ',', '#', '+', '*', - ] - .contains(c) - }) - .collect() -} diff --git a/poc/tsclientlib-filetransfer-spike/.gitignore b/poc/tsclientlib-filetransfer-spike/.gitignore deleted file mode 100644 index 6f02f34..0000000 --- a/poc/tsclientlib-filetransfer-spike/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/downloads/ -/target/ diff --git a/poc/tsclientlib-filetransfer-spike/Cargo.lock b/poc/tsclientlib-filetransfer-spike/Cargo.lock deleted file mode 100644 index b1fabf6..0000000 --- a/poc/tsclientlib-filetransfer-spike/Cargo.lock +++ /dev/null @@ -1,3453 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[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 = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[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 = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[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.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[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 = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest 0.10.7", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -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 = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[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 = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[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 = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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" -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", - "zeroize", -] - -[[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 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - -[[package]] -name = "git-testament" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74999c921479f919c87a9d2e6922a79a18683f18105344df8e067149232e51" -dependencies = [ - "git-testament-derive", -] - -[[package]] -name = "git-testament-derive" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeac967e71eb3dc1656742fc7521ec7cd3b6b88738face65bf1fddf702bc4c0" -dependencies = [ - "log", - "proc-macro2", - "quote", - "syn", - "time", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-net" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "futures-channel", - "futures-io", - "futures-util", - "hickory-proto", - "idna", - "ipnet", - "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" -dependencies = [ - "data-encoding", - "idna", - "ipnet", - "jni", - "once_cell", - "prefix-trie", - "rand 0.10.1", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-net", - "hickory-proto", - "ipconfig", - "ipnet", - "jni", - "moka", - "ndk-context", - "once_cell", - "parking_lot", - "rand 0.10.1", - "resolv-conf", - "smallvec", - "system-configuration", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[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 = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -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.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2", - "widestring", - "windows-registry", - "windows-result", - "windows-sys 0.61.2", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -dependencies = [ - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[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 = "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.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -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 = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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 = "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 = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "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 = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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", -] - -[[package]] -name = "omnom" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66562a92c66d5937e9d549597c2d3cafe3d2367dd08dc257da730587c2cfc218" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[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 = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[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 = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[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 = "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 = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - -[[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 = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[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 = "quicklz" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750eab265a26a0cd064f2a2b222bf172cc06d7a873e113e5f6d0118cc6e5fbb1" -dependencies = [ - "bit-vec", - "byteorder", - "thiserror 1.0.69", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "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 = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[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 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[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_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "reqwest" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "aws-lc-rs", - "once_cell", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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 = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "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 = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[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 = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[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" - -[[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 = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[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 = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[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 = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "t4rust-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e411692d6e0c8ce07f65a8210e95dcfb56cc4600940456592a49a3579ef6396" -dependencies = [ - "nom", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[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", - "num-conv", - "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 = "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.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "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 = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ts-bookkeeping" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "heck", - "itertools", - "num-derive", - "num-traits", - "serde", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tracing", - "tsproto-packets", - "tsproto-structs", - "tsproto-types", -] - -[[package]] -name = "tsclientlib" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "futures", - "git-testament", - "hickory-net", - "hickory-resolver", - "itertools", - "num-traits", - "pin-utils", - "rand 0.10.1", - "reqwest", - "thiserror 2.0.18", - "time", - "tokio", - "tokio-stream", - "tracing", - "ts-bookkeeping", - "tsproto", - "tsproto-packets", - "tsproto-types", - "url", -] - -[[package]] -name = "tsclientlib-filetransfer-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64", - "clap", - "futures", - "tokio", - "tracing", - "tracing-subscriber", - "tsclientlib", - "tsproto-packets", -] - -[[package]] -name = "tsproto" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "aes", - "base64", - "curve25519-dalek-ng", - "eax", - "futures", - "generic-array", - "git-testament", - "num-bigint", - "num-derive", - "num-traits", - "omnom", - "pin-project-lite", - "quicklz", - "rand 0.10.1", - "serde", - "sha-1", - "sha2", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "tsproto-packets", - "tsproto-types", -] - -[[package]] -name = "tsproto-packets" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "num-derive", - "num-traits", - "omnom", - "self_cell", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "tsproto-structs" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "csv", - "heck", - "once_cell", - "serde", - "toml", -] - -[[package]] -name = "tsproto-types" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "curve25519-dalek-ng", - "elliptic-curve", - "generic-array", - "heck", - "num-bigint", - "num-derive", - "num-traits", - "p256", - "rand 0.8.6", - "ref-cast", - "serde", - "sha-1", - "simple_asn1", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tsproto-structs", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.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 = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[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-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[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.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.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.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.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.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.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.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.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.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 = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -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/poc/tsclientlib-filetransfer-spike/Cargo.toml b/poc/tsclientlib-filetransfer-spike/Cargo.toml deleted file mode 100644 index ed1cef0..0000000 --- a/poc/tsclientlib-filetransfer-spike/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "tsclientlib-filetransfer-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: discover and download a TeamSpeak file over the file-transfer port." - -[workspace] - -[dependencies] -anyhow = "1" -base64 = "0.22" -clap = { version = "4", features = ["derive"] } -futures = "0.3" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "fs", "io-util"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["default-tls"] } -tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", package = "tsproto-packets" } diff --git a/poc/tsclientlib-filetransfer-spike/README.md b/poc/tsclientlib-filetransfer-spike/README.md deleted file mode 100644 index e013f8e..0000000 --- a/poc/tsclientlib-filetransfer-spike/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# tsclientlib Filetransfer Spike - -Chanora proof-of-concept. **Not product code.** - -## Purpose - -Prove that a normal TeamSpeak client session can download a file from the -TeamSpeak fileserver by: - -1. discovering a candidate file path, and -2. completing the `ftinitdownload` + second TCP connection flow. - -## What it does - -1. Connects to `kr.teamspeak.app`. -2. Attempts `ftgetfilelist` on visible channels. -3. Falls back to server icon or client avatar paths if channel-file listing is - unavailable. -4. Uses `Connection::download_file` to perform the actual fileserver download. -5. Saves the file under `downloads/`. - -## Run - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --nickname ChanoraPoC-Downloader -``` - -Optional explicit target: - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --channel-id 0 \ - --path /avatar_ebmjofhfgojajpappiikgponphiippbficbmfemf -``` - -## Verification - -See `VERIFICATION.md`. diff --git a/poc/tsclientlib-filetransfer-spike/VERIFICATION.md b/poc/tsclientlib-filetransfer-spike/VERIFICATION.md deleted file mode 100644 index de7cd3c..0000000 --- a/poc/tsclientlib-filetransfer-spike/VERIFICATION.md +++ /dev/null @@ -1,45 +0,0 @@ -# Verification record — `tsclientlib-filetransfer-spike` - -## Result - -PASS. The spike successfully downloaded a real file from -`kr.teamspeak.app` over the TeamSpeak fileserver path. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-25 | -| Host OS | Linux (x86_64) | -| Build profile | dev | -| Target server | `kr.teamspeak.app` | - -## Command - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --nickname ChanoraPoC-Downloader -``` - -## What was observed - -- `ftgetfilelist` against visible channel IDs did not return typed file rows on - this server; each scan attempt failed with `ParameterNotFound`. -- The fallback logic selected `EdisonJwa`'s avatar path: - - channel id: `0` - - remote path: `/avatar_ebmjofhfgojajpappiikgponphiippbficbmfemf` -- `download_file` completed successfully. -- Reported file size: `129453` -- Copied bytes: `129453` -- Saved artifact: - - `downloads/avatar_ebmjofhfgojajpappiikgponphiippbficbmfemf` - -## Conclusion - -The practical TeamSpeak fileserver download path is proven: - -1. determine a valid TeamSpeak remote path, -2. request the transfer bootstrap, -3. open the returned file-transfer TCP stream, -4. receive raw bytes and persist them locally. diff --git a/poc/tsclientlib-filetransfer-spike/src/main.rs b/poc/tsclientlib-filetransfer-spike/src/main.rs deleted file mode 100644 index cff2769..0000000 --- a/poc/tsclientlib-filetransfer-spike/src/main.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::{anyhow, bail, Context, Result}; -use clap::Parser; -use futures::prelude::*; -use tokio::io; -use tokio::time as tokio_time; -use tracing::{info, warn}; -use tracing_subscriber::EnvFilter; - -use tsclientlib::messages::s2c::{InFileListPart, InMessage}; -use tsclientlib::{ - ChannelId, Connection, DisconnectOptions, Identity, OutCommandExt, StreamItem, -}; -use tsproto_packets::packets::{Direction, Flags, OutCommand, PacketType}; - -#[derive(Parser, Debug)] -#[command( - name = "tsclientlib-filetransfer-spike", - about = "Chanora PoC: list files and download one via the TeamSpeak fileserver." -)] -struct Args { - /// Server address (hostname[:port] or TSDNS name). - #[arg(short, long, default_value = "kr.teamspeak.app")] - address: String, - - /// Nickname used on the server. - #[arg(short, long, default_value = "ChanoraPoC-Downloader")] - nickname: String, - - /// Optional TeamSpeak identity string. If omitted, a fresh identity is generated. - #[arg(long)] - identity: Option, - - /// Server password if required. - #[arg(long)] - password: Option, - - /// Explicit channel id for the download target. - #[arg(long)] - channel_id: Option, - - /// Explicit remote TeamSpeak path for the download target. - #[arg(long)] - path: Option, - - /// Output directory for downloaded files. - #[arg(long, default_value = "downloads")] - output_dir: PathBuf, -} - -#[derive(Clone, Debug)] -struct DownloadCandidate { - channel_id: ChannelId, - remote_path: String, - reason: String, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn")); - tracing_subscriber::fmt().with_env_filter(filter).init(); - - info!(target: "filetransfer-spike", address = %args.address, nickname = %args.nickname, "starting filetransfer spike"); - - let mut builder = Connection::build(args.address.clone()).name(args.nickname.clone()); - let identity = match &args.identity { - Some(raw) => Identity::new_from_str(raw).context("parsing --identity")?, - None => Identity::create(), - }; - builder = builder.identity(identity); - if let Some(password) = &args.password { - builder = builder.password(password.clone()); - } - - let mut con = builder.connect().context("dialling server")?; - wait_for_initial_state(&mut con).await?; - if let Ok(state) = con.get_state() { - let _ = state.server.set_subscribed(true).send(&mut con); - } - pump_events(&mut con, std::time::Duration::from_secs(1)).await?; - - let candidate = if let (Some(channel_id), Some(path)) = (args.channel_id, args.path.clone()) { - DownloadCandidate { - channel_id: ChannelId(channel_id), - remote_path: path, - reason: "explicit CLI target".to_string(), - } - } else { - discover_candidate(&mut con).await? - }; - - println!("=== Download Target ==="); - println!("Reason : {}", candidate.reason); - println!("Channel ID : {}", candidate.channel_id.0); - println!("Remote path : {}", candidate.remote_path); - - tokio::fs::create_dir_all(&args.output_dir) - .await - .with_context(|| format!("creating {}", args.output_dir.display()))?; - let output_path = args.output_dir.join(file_name_from_remote_path(&candidate.remote_path)); - - let download = con - .download_file(candidate.channel_id, &candidate.remote_path, None, None) - .context("requesting file download")?; - let mut result = await_download_result(&mut con, download).await?; - let mut output = tokio::fs::File::create(&output_path) - .await - .with_context(|| format!("creating {}", output_path.display()))?; - let copied = io::copy(&mut result.stream, &mut output) - .await - .with_context(|| format!("writing {}", output_path.display()))?; - - println!(); - println!("=== Download Result ==="); - println!("Expected size : {}", result.size); - println!("Copied bytes : {}", copied); - println!("Saved to : {}", output_path.display()); - - con.disconnect(DisconnectOptions::new()).ok(); - con.events().for_each(|_| futures::future::ready(())).await; - Ok(()) -} - -async fn discover_candidate(con: &mut Connection) -> Result { - let channel_ids = { - let state = con.get_state().context("reading connection state")?; - let mut ids = state.channels.keys().copied().collect::>(); - ids.sort_by_key(|id| id.0); - ids - }; - - for channel_id in channel_ids { - match request_file_list(con, channel_id, "/").await { - Ok(files) => { - if let Some(file) = files.into_iter().find(|entry| entry.is_file) { - return Ok(DownloadCandidate { - channel_id: file.channel_id, - remote_path: format_path(&file.path, &file.name), - reason: format!("first file discovered via ftgetfilelist in channel {}", file.channel_id.0), - }); - } - } - Err(error) => { - warn!(target: "filetransfer-spike", %error, channel_id = channel_id.0, "ftgetfilelist failed"); - } - } - } - - let state = con.get_state().context("reading connection state after file scan")?; - if state.server.icon.0 != 0 { - return Ok(DownloadCandidate { - channel_id: ChannelId(0), - remote_path: format!("/icon_{}", state.server.icon.0), - reason: "server icon fallback".to_string(), - }); - } - for client in state.clients.values() { - if !client.avatar_hash.is_empty() { - if let Some(uid) = &client.uid { - return Ok(DownloadCandidate { - channel_id: ChannelId(0), - remote_path: format!("/avatar_{}", uid.as_ref().as_avatar()), - reason: format!("avatar fallback for client {}", client.name), - }); - } - } - } - - bail!("no downloadable candidate discovered from channel files, server icon, or client avatars"); -} - -fn build_command(name: &str, args: &[(&str, String)], flags: &[&str]) -> OutCommand { - let mut command = OutCommand::new(Direction::C2S, Flags::empty(), PacketType::Command, name); - for flag in flags { - command.write_arg(flag, &""); - } - for (key, value) in args { - command.write_arg(key, value); - } - command -} - -async fn request_messages(con: &mut Connection, command: OutCommand) -> Result> { - let handle = command.send_with_result(con).context("sending command")?; - let mut messages = Vec::new(); - loop { - let item = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before command response"))??; - match item { - StreamItem::MessageEvent(message) => messages.push(message), - StreamItem::MessageResult(reply, status) if reply == handle => { - status.map_err(|error| anyhow!("command failed: {error}"))?; - return Ok(messages); - } - _ => {} - } - } -} - -async fn request_file_list(con: &mut Connection, channel_id: ChannelId, path: &str) -> Result> { - let command = build_command( - "ftgetfilelist", - &[("cid", channel_id.0.to_string()), ("path", path.to_string())], - &[], - ); - let messages = request_messages(con, command).await?; - let mut rows = Vec::new(); - for message in messages { - if let InMessage::FileList(list) = message { - rows.extend(list.iter().cloned()); - } - } - Ok(rows) -} - -async fn await_download_result( - con: &mut Connection, - handle: tsclientlib::FiletransferHandle, -) -> Result { - loop { - let item = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before filetransfer result"))??; - match item { - StreamItem::FileDownload(reply, result) if reply == handle => return Ok(result), - StreamItem::FiletransferFailed(reply, error) if reply == handle => { - return Err(anyhow!("filetransfer failed: {error}")); - } - _ => {} - } - } -} - -async fn wait_for_initial_state(con: &mut Connection) -> Result<()> { - loop { - let event = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before initial state"))??; - if matches!(event, StreamItem::BookEvents(_)) { - return Ok(()); - } - } -} - -async fn pump_events(con: &mut Connection, budget: std::time::Duration) -> Result<()> { - let deadline = tokio_time::Instant::now() + budget; - loop { - let now = tokio_time::Instant::now(); - if now >= deadline { - return Ok(()); - } - let remaining = deadline - now; - match tokio_time::timeout(remaining, con.events().next()).await { - Ok(Some(Ok(_))) => {} - Ok(Some(Err(error))) => return Err(error).context("pumping connection events"), - Ok(None) => bail!("event stream ended while pumping events"), - Err(_) => return Ok(()), - } - } -} - -fn format_path(path: &str, name: &str) -> String { - if path == "/" { - format!("/{name}") - } else if path.ends_with('/') { - format!("{path}{name}") - } else { - format!("{path}/{name}") - } -} - -fn file_name_from_remote_path(remote_path: &str) -> String { - let name = Path::new(remote_path) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("download.bin"); - let sanitized = name - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() || ch == '.' || ch == '-' || ch == '_' { ch } else { '_' }) - .collect::(); - if sanitized.is_empty() { - "download.bin".to_string() - } else { - sanitized - } -} diff --git a/poc/tsclientlib-query-spike/.gitignore b/poc/tsclientlib-query-spike/.gitignore deleted file mode 100644 index b83d222..0000000 --- a/poc/tsclientlib-query-spike/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/poc/tsclientlib-query-spike/Cargo.lock b/poc/tsclientlib-query-spike/Cargo.lock deleted file mode 100644 index fdc5fae..0000000 --- a/poc/tsclientlib-query-spike/Cargo.lock +++ /dev/null @@ -1,3454 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[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 = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[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 = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[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.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[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 = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest 0.10.7", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -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 = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[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 = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[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 = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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" -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", - "zeroize", -] - -[[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 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - -[[package]] -name = "git-testament" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74999c921479f919c87a9d2e6922a79a18683f18105344df8e067149232e51" -dependencies = [ - "git-testament-derive", -] - -[[package]] -name = "git-testament-derive" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeac967e71eb3dc1656742fc7521ec7cd3b6b88738face65bf1fddf702bc4c0" -dependencies = [ - "log", - "proc-macro2", - "quote", - "syn", - "time", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-net" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "futures-channel", - "futures-io", - "futures-util", - "hickory-proto", - "idna", - "ipnet", - "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" -dependencies = [ - "data-encoding", - "idna", - "ipnet", - "jni", - "once_cell", - "prefix-trie", - "rand 0.10.1", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-net", - "hickory-proto", - "ipconfig", - "ipnet", - "jni", - "moka", - "ndk-context", - "once_cell", - "parking_lot", - "rand 0.10.1", - "resolv-conf", - "smallvec", - "system-configuration", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[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 = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -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.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2", - "widestring", - "windows-registry", - "windows-result", - "windows-sys 0.61.2", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -dependencies = [ - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[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 = "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.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -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 = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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 = "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 = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "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 = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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", -] - -[[package]] -name = "omnom" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66562a92c66d5937e9d549597c2d3cafe3d2367dd08dc257da730587c2cfc218" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[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 = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[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 = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[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 = "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 = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - -[[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 = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[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 = "quicklz" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750eab265a26a0cd064f2a2b222bf172cc06d7a873e113e5f6d0118cc6e5fbb1" -dependencies = [ - "bit-vec", - "byteorder", - "thiserror 1.0.69", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "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 = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[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 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[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_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "reqwest" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "aws-lc-rs", - "once_cell", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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 = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "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 = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[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 = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[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" - -[[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 = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[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 = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[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 = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "t4rust-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e411692d6e0c8ce07f65a8210e95dcfb56cc4600940456592a49a3579ef6396" -dependencies = [ - "nom", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[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", - "num-conv", - "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 = "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.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "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 = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ts-bookkeeping" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "heck", - "itertools", - "num-derive", - "num-traits", - "serde", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tracing", - "tsproto-packets", - "tsproto-structs", - "tsproto-types", -] - -[[package]] -name = "tsclientlib" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "futures", - "git-testament", - "hickory-net", - "hickory-resolver", - "itertools", - "num-traits", - "pin-utils", - "rand 0.10.1", - "reqwest", - "thiserror 2.0.18", - "time", - "tokio", - "tokio-stream", - "tracing", - "ts-bookkeeping", - "tsproto", - "tsproto-packets", - "tsproto-types", - "url", -] - -[[package]] -name = "tsclientlib-query-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64", - "clap", - "futures", - "time", - "tokio", - "tracing", - "tracing-subscriber", - "tsclientlib", - "tsproto-packets", -] - -[[package]] -name = "tsproto" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "aes", - "base64", - "curve25519-dalek-ng", - "eax", - "futures", - "generic-array", - "git-testament", - "num-bigint", - "num-derive", - "num-traits", - "omnom", - "pin-project-lite", - "quicklz", - "rand 0.10.1", - "serde", - "sha-1", - "sha2", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "tsproto-packets", - "tsproto-types", -] - -[[package]] -name = "tsproto-packets" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "num-derive", - "num-traits", - "omnom", - "self_cell", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "tsproto-structs" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "csv", - "heck", - "once_cell", - "serde", - "toml", -] - -[[package]] -name = "tsproto-types" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "curve25519-dalek-ng", - "elliptic-curve", - "generic-array", - "heck", - "num-bigint", - "num-derive", - "num-traits", - "p256", - "rand 0.8.6", - "ref-cast", - "serde", - "sha-1", - "simple_asn1", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tsproto-structs", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.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 = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[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-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[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.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.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.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.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.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.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.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.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.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 = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -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/poc/tsclientlib-query-spike/Cargo.toml b/poc/tsclientlib-query-spike/Cargo.toml deleted file mode 100644 index 21a0d46..0000000 --- a/poc/tsclientlib-query-spike/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "tsclientlib-query-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: query live TeamSpeak client profile and connection data over the full client protocol." - -[workspace] - -[dependencies] -anyhow = "1" -base64 = "0.22" -clap = { version = "4", features = ["derive"] } -futures = "0.3" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -time = { version = "0.3", features = ["formatting"] } -tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["default-tls"] } -tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", package = "tsproto-packets" } diff --git a/poc/tsclientlib-query-spike/README.md b/poc/tsclientlib-query-spike/README.md deleted file mode 100644 index 15a4971..0000000 --- a/poc/tsclientlib-query-spike/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# tsclientlib Query Spike - -Chanora proof-of-concept. **Not product code.** - -## Purpose - -Prove what user/profile data can be observed from a normal TeamSpeak client -connection to `kr.teamspeak.app` without ServerQuery credentials. - -## What it does - -1. Connects to a TeamSpeak server with `tsclientlib`. -2. Subscribes to the server tree and pumps events so the live client/channel - snapshot is populated. -3. Prints the online client table. -4. Resolves a target client by nickname, UID, DBID, or CLID. -5. Requests richer client data with `clientgetvariables` and connection stats - with `getconnectioninfo`, then reads the populated values back out of the - live `tsclientlib` connection state. -6. Falls back gracefully when the server denies permission-gated lookups such - as `clientdbinfo`. - -## Run - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --nickname ChanoraPoC-Inspector \ - --target-nickname EdisonJwa -``` - -Useful flags: - -- `--target-nickname ` -- `--target-uid ` -- `--target-dbid ` -- `--target-clid ` -- `--identity ` - -## Scope boundaries - -- No ServerQuery login or administrative credentials. -- No attempt to bypass permission-gated data. -- No persistence of the generated identity. - -## Verification - -See `VERIFICATION.md`. diff --git a/poc/tsclientlib-query-spike/VERIFICATION.md b/poc/tsclientlib-query-spike/VERIFICATION.md deleted file mode 100644 index 75d7578..0000000 --- a/poc/tsclientlib-query-spike/VERIFICATION.md +++ /dev/null @@ -1,82 +0,0 @@ -# Verification record — `tsclientlib-query-spike` - -## Result - -PASS. On 2026-05-25, the spike connected to `kr.teamspeak.app`, resolved the -live `EdisonJwa` session, and recovered the requested profile, group, country, -avatar, quota, and connection-stat fields from a normal TeamSpeak client -session. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-25 | -| Host OS | Linux (x86_64) | -| Build profile | dev | -| Target server | `kr.teamspeak.app` | - -## Command - -```bash -cargo run --offline -- \ - --address kr.teamspeak.app \ - --nickname ChanoraPoC-Inspector \ - --target-nickname EdisonJwa -``` - -## Observed output - -- Live roster populated after subscription + event pumping. -- `EdisonJwa` was online and resolved as: - - `clid=18481` - - `dbid=2` - - `uid=QcnldW6Qnw/4im/t94j/FYIcVMU=` - - `country=KR` - - description: `test` - - avatar path: `/avatar_ebmjofhfgojajpappiikgponphiippbficbmfemf` -- `clientgetvariables` populated richer profile data: - - version: `3.6.2 [Build: 1695203293]` - - platform: `Windows` - - first connected: `2018-09-07T09:39:00Z` - - last connected: `2026-05-25T03:34:44Z` - - total connections: `3831` - - downloaded total: `102735429` - - uploaded total: `51249731` -- Group names resolved from live state after `servergrouplist` / - `channelgrouplist`: - - server groups: `Server Admin`, `Talkaholic` - - channel group: `Channel Admin` -- `getconnectioninfo` populated connection stats: - - connection time: `27m 29s` - - idle time: `182 ms` - - ping: `42 ms` - - client address: hidden by server permissions - - packet loss: `0.0000` in both directions - - packets transferred: populated - - bytes transferred: populated - - filetransfer bandwidth: `sent=0 recv=0` -- `clientdbinfo cldbid=2` still failed with - `PermissionsClientInsufficient`, which confirms that the richer results above - came from the full client session path rather than elevated query - permissions. - -## Conclusion - -This spike proves the following from a normal client connection: - -- online roster discovery -- target user resolution by visible session data -- nickname / UID / DBID / country capture -- description, version, platform, first-connected, last-connected, and total - connections retrieval -- server-group and channel-group name resolution -- avatar path derivation -- live connection-time, idle-time, ping, loss, packet, byte, and quota - retrieval - -## Remaining limits - -- `clientdbinfo` remains permission-gated for this target on this server. -- `Client Address` is correctly reported as hidden because the live session did - not have permission to see it. diff --git a/poc/tsclientlib-query-spike/src/main.rs b/poc/tsclientlib-query-spike/src/main.rs deleted file mode 100644 index a91a5d2..0000000 --- a/poc/tsclientlib-query-spike/src/main.rs +++ /dev/null @@ -1,776 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{anyhow, bail, Context, Result}; -use base64::prelude::*; -use clap::Parser; -use futures::prelude::*; -use time::format_description::well_known::Rfc3339; -use time::{Duration, OffsetDateTime}; -use tokio::time as tokio_time; -use tracing::{info, warn}; -use tracing_subscriber::EnvFilter; - -use tsclientlib::data; -use tsclientlib::messages::s2c::{InClientDbIdFromUidPart, InClientDbInfoPart, InMessage}; -use tsclientlib::{ - ChannelGroupId, ClientDbId, ClientId, Connection, DisconnectOptions, Identity, OutCommandExt, - ServerGroupId, StreamItem, Uid, -}; -use tsproto_packets::packets::{Direction, Flags, OutCommand, PacketType}; - -#[derive(Parser, Debug)] -#[command( - name = "tsclientlib-query-spike", - about = "Chanora PoC: inspect TeamSpeak client profile + connection info over the full client protocol." -)] -struct Args { - /// Server address (hostname[:port] or TSDNS name). - #[arg(short, long, default_value = "kr.teamspeak.app")] - address: String, - - /// Nickname used on the server. - #[arg(short, long, default_value = "ChanoraPoC-Inspector")] - nickname: String, - - /// Optional TeamSpeak identity string. If omitted, a fresh identity is generated. - #[arg(long)] - identity: Option, - - /// Server password if required. - #[arg(long)] - password: Option, - - /// Inspect a specific online/offline nickname. - #[arg(long)] - target_nickname: Option, - - /// Inspect a specific TeamSpeak UID. - #[arg(long)] - target_uid: Option, - - /// Inspect a specific TeamSpeak database id. - #[arg(long)] - target_dbid: Option, - - /// Inspect a specific online client id. - #[arg(long)] - target_clid: Option, - - /// Print the online client table before detailed inspection. - #[arg(long, default_value_t = true)] - show_online: bool, -} - -#[derive(Clone, Debug)] -struct Target { - clid: Option, - dbid: Option, - uid_b64: Option, - nickname: Option, -} - -#[derive(Clone, Debug)] -struct OnlineTargetSnapshot { - description: String, - country_code: String, - server_groups: Vec, - channel_group: ChannelGroupId, - avatar_hash: String, - uid_b64: Option, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn")); - tracing_subscriber::fmt().with_env_filter(filter).init(); - - info!(target: "query-spike", address = %args.address, nickname = %args.nickname, "starting query spike"); - - let mut builder = Connection::build(args.address.clone()).name(args.nickname.clone()); - let identity = match &args.identity { - Some(raw) => Identity::new_from_str(raw).context("parsing --identity")?, - None => Identity::create(), - }; - builder = builder.identity(identity); - if let Some(password) = &args.password { - builder = builder.password(password.clone()); - } - - let mut con = builder.connect().context("dialling server")?; - wait_for_initial_state(&mut con).await?; - if let Ok(state) = con.get_state() { - let _ = state.server.set_subscribed(true).send(&mut con); - } - pump_events(&mut con, std::time::Duration::from_secs(1)).await?; - - let (mut target, online_snapshot) = { - let state = con.get_state().context("reading connection state")?; - if args.show_online { - print_online_clients(state); - } - let target = resolve_target(&args, state)?; - let snapshot = target - .clid - .and_then(|clid| state.clients.get(&clid)) - .map(|client| OnlineTargetSnapshot { - description: client.description.clone(), - country_code: client.country_code.clone(), - server_groups: client.server_groups.iter().copied().collect(), - channel_group: client.channel_group, - avatar_hash: client.avatar_hash.clone(), - uid_b64: client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())), - }); - (target, snapshot) - }; - - let _ = request_server_group_list(&mut con).await; - let _ = request_channel_group_list(&mut con).await; - if target.dbid.is_none() { - if let Some(uid) = target.uid_b64.as_deref() { - if let Ok(lookup) = request_client_dbid_from_uid(&mut con, uid).await { - target.dbid = Some(lookup.client_db_id); - } - } - } - println!(); - println!("=== Detailed Target ==="); - println!("Target nickname : {}", target.nickname.as_deref().unwrap_or("Unknown")); - println!("Target UID : {}", target.uid_b64.as_deref().unwrap_or("Unknown")); - println!( - "Target DBID : {}", - target - .dbid - .map(|id| id.0.to_string()) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Target CLID : {}", - target - .clid - .map(|id| id.0.to_string()) - .unwrap_or_else(|| "Offline/Unknown".to_string()) - ); - - if let Some(clid) = target.clid { - if let Err(error) = request_client_variables(&mut con, clid).await { - warn!(target: "query-spike", %error, clid = clid.0, "clientgetvariables failed"); - } - if let Err(error) = request_client_connection_info(&mut con, clid).await { - warn!(target: "query-spike", %error, clid = clid.0, "getconnectioninfo failed"); - } - } - let db_info = match target.dbid { - Some(dbid) => match request_client_db_info(&mut con, dbid).await { - Ok(info) => Some(info), - Err(error) => { - warn!(target: "query-spike", %error, dbid = dbid.0, "clientdbinfo failed"); - None - } - }, - None => None, - }; - - let (optional_data, connection_data, server_group_names, channel_group_names) = { - let state = con.get_state().context("reading updated connection state")?; - let server_group_names = state - .server_groups - .iter() - .map(|(id, group)| (*id, group.name.clone())) - .collect::>(); - let channel_group_names = state - .channel_groups - .iter() - .map(|(id, group)| (*id, group.name.clone())) - .collect::>(); - match target.clid.and_then(|clid| state.clients.get(&clid)) { - Some(client) => ( - client.optional_data.clone(), - client.connection_data.clone(), - server_group_names, - channel_group_names, - ), - None => (None, None, server_group_names, channel_group_names), - } - }; - - print_detailed_summary( - target, - optional_data.as_ref(), - connection_data.as_ref(), - db_info.as_ref(), - online_snapshot.as_ref(), - &server_group_names, - &channel_group_names, - )?; - - con.disconnect(DisconnectOptions::new()).ok(); - con.events().for_each(|_| futures::future::ready(())).await; - - Ok(()) -} - -async fn wait_for_initial_state(con: &mut Connection) -> Result<()> { - loop { - let event = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before initial state"))??; - if matches!(event, StreamItem::BookEvents(_)) { - return Ok(()); - } - } -} - -async fn pump_events(con: &mut Connection, budget: std::time::Duration) -> Result<()> { - let deadline = tokio_time::Instant::now() + budget; - loop { - let now = tokio_time::Instant::now(); - if now >= deadline { - return Ok(()); - } - let remaining = deadline - now; - match tokio_time::timeout(remaining, con.events().next()).await { - Ok(Some(Ok(_))) => {} - Ok(Some(Err(error))) => return Err(error).context("pumping connection events"), - Ok(None) => bail!("event stream ended while pumping events"), - Err(_) => return Ok(()), - } - } -} - -fn build_command(name: &str, args: &[(&str, String)], flags: &[&str]) -> OutCommand { - let mut command = OutCommand::new(Direction::C2S, Flags::empty(), PacketType::Command, name); - for flag in flags { - command.write_arg(flag, &""); - } - for (key, value) in args { - command.write_arg(key, value); - } - command -} - -async fn request_messages(con: &mut Connection, command: OutCommand) -> Result> { - let handle = command.send_with_result(con).context("sending command")?; - let mut messages = Vec::new(); - loop { - let item = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before command response"))??; - match item { - StreamItem::MessageEvent(message) => messages.push(message), - StreamItem::MessageResult(reply, status) if reply == handle => { - status.map_err(|error| anyhow!("command failed: {error}"))?; - return Ok(messages); - } - _ => {} - } - } -} - -async fn request_messages_and_quiet( - con: &mut Connection, - command: OutCommand, - quiet_period: std::time::Duration, -) -> Result> { - let messages = request_messages(con, command).await?; - pump_events(con, quiet_period).await?; - Ok(messages) -} - -async fn request_client_variables(con: &mut Connection, clid: ClientId) -> Result<()> { - let command = build_command("clientgetvariables", &[("clid", clid.0.to_string())], &[]); - request_messages_and_quiet(con, command, std::time::Duration::from_millis(350)).await?; - Ok(()) -} - -async fn request_client_connection_info(con: &mut Connection, clid: ClientId) -> Result<()> { - let command = build_command("getconnectioninfo", &[("clid", clid.0.to_string())], &[]); - request_messages_and_quiet(con, command, std::time::Duration::from_millis(350)).await?; - Ok(()) -} - -async fn request_client_db_info(con: &mut Connection, dbid: ClientDbId) -> Result { - let command = build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]); - let messages = request_messages(con, command).await?; - for message in messages { - if let InMessage::ClientDbInfo(info) = message { - if let Some(row) = info.iter().next() { - return Ok(row.clone()); - } - } - } - bail!("clientdbinfo returned no row"); -} - -async fn request_client_dbid_from_uid(con: &mut Connection, uid: &str) -> Result { - let command = build_command("clientgetdbidfromuid", &[("cluid", uid.to_string())], &[]); - let messages = request_messages(con, command).await?; - for message in messages { - if let InMessage::ClientDbIdFromUid(info) = message { - if let Some(row) = info.iter().next() { - return Ok(row.clone()); - } - } - } - bail!("clientgetdbidfromuid returned no row"); -} - -async fn request_server_group_list(con: &mut Connection) -> Result<()> { - request_messages_and_quiet( - con, - build_command("servergrouplist", &[], &[]), - std::time::Duration::from_millis(200), - ) - .await?; - Ok(()) -} - -async fn request_channel_group_list(con: &mut Connection) -> Result<()> { - request_messages_and_quiet( - con, - build_command("channelgrouplist", &[], &[]), - std::time::Duration::from_millis(200), - ) - .await?; - Ok(()) -} - -fn resolve_target(args: &Args, state: &data::Connection) -> Result { - if let Some(clid) = args.target_clid { - if let Some(row) = state.clients.get(&ClientId(clid)) { - return Ok(Target { - clid: Some(row.id), - dbid: Some(row.database_id), - uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())), - nickname: Some(row.name.clone()), - }); - } - return Ok(Target { - clid: Some(ClientId(clid)), - dbid: None, - uid_b64: None, - nickname: None, - }); - } - - if let Some(uid) = &args.target_uid { - if let Some(row) = state - .clients - .values() - .find(|row| row.uid.as_ref().is_some_and(|value| uid_to_b64(value.as_ref()) == *uid)) - { - return Ok(Target { - clid: Some(row.id), - dbid: Some(row.database_id), - uid_b64: Some(uid.clone()), - nickname: Some(row.name.clone()), - }); - } - return Ok(Target { - clid: None, - dbid: args.target_dbid.map(ClientDbId), - uid_b64: Some(uid.clone()), - nickname: args.target_nickname.clone(), - }); - } - - if let Some(nickname) = &args.target_nickname { - if let Some(row) = state.clients.values().find(|row| row.name == *nickname) { - return Ok(Target { - clid: Some(row.id), - dbid: Some(row.database_id), - uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())), - nickname: Some(row.name.clone()), - }); - } - return Ok(Target { - clid: None, - dbid: args.target_dbid.map(ClientDbId), - uid_b64: None, - nickname: Some(nickname.clone()), - }); - } - - if let Some(dbid) = args.target_dbid { - if let Some(row) = state.clients.values().find(|row| row.database_id.0 == dbid) { - return Ok(Target { - clid: Some(row.id), - dbid: Some(row.database_id), - uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())), - nickname: Some(row.name.clone()), - }); - } - return Ok(Target { - clid: None, - dbid: Some(ClientDbId(dbid)), - uid_b64: args.target_uid.clone(), - nickname: args.target_nickname.clone(), - }); - } - - let own_client_id = state.own_client; - let own_client = state - .clients - .get(&own_client_id) - .ok_or_else(|| anyhow!("own client missing from state"))?; - Ok(Target { - clid: Some(own_client_id), - dbid: Some(own_client.database_id), - uid_b64: own_client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())), - nickname: Some(own_client.name.clone()), - }) -} - -fn print_online_clients(state: &data::Connection) { - println!("=== Online Clients ==="); - if !state.server.ips.is_empty() { - let ips = state - .server - .ips - .iter() - .map(std::string::ToString::to_string) - .collect::>() - .join(", "); - println!("Server IPs: {ips}"); - } - for row in state.clients.values() { - let uid = row - .uid - .as_ref() - .map(|value| uid_to_b64(value.as_ref())) - .unwrap_or_else(|| "Hidden".to_string()); - let country = if row.country_code.is_empty() { - "Unknown".to_string() - } else { - row.country_code.clone() - }; - println!( - "- {} | clid={} dbid={} cid={} uid={} country={}", - row.name, row.id.0, row.database_id.0, row.channel.0, uid, country - ); - } -} - -fn print_detailed_summary( - target: Target, - client_info: Option<&data::OptionalClientData>, - connection_info: Option<&data::ConnectionClientData>, - db_info: Option<&InClientDbInfoPart>, - online_snapshot: Option<&OnlineTargetSnapshot>, - server_group_names: &HashMap, - channel_group_names: &HashMap, -) -> Result<()> { - let nickname = db_info - .map(|info| info.name.clone()) - .or(target.nickname) - .unwrap_or_else(|| "Unknown".to_string()); - let uid = db_info - .map(|info| uid_to_b64(info.uid.as_ref())) - .or(target.uid_b64) - .unwrap_or_else(|| "Unknown".to_string()); - let dbid = db_info - .map(|info| info.database_id.0) - .or(target.dbid.map(|value| value.0)) - .unwrap_or_default(); - - println!(); - println!("=== Client Profile ==="); - println!("Nickname : {} ({})", nickname, dbid); - println!("Unique ID : {}", uid); - println!("Database ID : {}", dbid); - println!( - "Description : {}", - db_info - .map(|info| info.description.as_str()) - .or_else(|| online_snapshot.map(|info| info.description.as_str())) - .unwrap_or("") - ); - println!( - "Version : {}", - client_info.map(|info| info.version.as_str()).unwrap_or("Unknown") - ); - println!( - "Connections : {}", - client_info - .map(|info| info.connections_total) - .or_else(|| db_info.map(|info| info.connections_total)) - .unwrap_or_default() - ); - println!( - "First Connected : {}", - client_info - .map(|info| format_timestamp(info.created)) - .or_else(|| db_info.map(|info| format_timestamp(info.created))) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Last Connected : {}", - client_info - .map(|info| format_timestamp(info.last_connected)) - .or_else(|| db_info.map(|info| format_timestamp(info.last_connected))) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Online Since : {}", - connection_info - .and_then(|info| info.connected_time.map(format_duration)) - .unwrap_or_else(|| "Offline".to_string()) - ); - println!( - "Country : {}", - online_snapshot - .map(|info| info.country_code.as_str()) - .filter(|value| !value.is_empty()) - .unwrap_or("Unknown") - ); - if let Some(info) = client_info { - println!("Platform : {}", info.platform); - println!( - "Server Groups : {}", - online_snapshot - .map(|snapshot| format_server_groups(&snapshot.server_groups, &server_group_names)) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Channel Group : {}", - online_snapshot - .map(|snapshot| { - channel_group_names - .get(&snapshot.channel_group) - .cloned() - .unwrap_or_else(|| format!("Unknown ({})", snapshot.channel_group.0)) - }) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Avatar Path : {}", - online_snapshot - .map(|snapshot| { - if snapshot.avatar_hash.is_empty() { - "None".to_string() - } else { - snapshot - .uid_b64 - .as_ref() - .map(|uid| format!("/avatar_{}", uid_to_avatar_path(uid))) - .unwrap_or_else(|| "Hidden".to_string()) - } - }) - .unwrap_or_else(|| "Unknown".to_string()) - ); - } else if let Some(info) = online_snapshot { - println!( - "Server Groups : {}", - format_server_groups(&info.server_groups, &server_group_names) - ); - println!( - "Channel Group : {}", - channel_group_names - .get(&info.channel_group) - .cloned() - .unwrap_or_else(|| format!("Unknown ({})", info.channel_group.0)) - ); - println!( - "Avatar Path : {}", - if info.avatar_hash.is_empty() { - "None".to_string() - } else { - info.uid_b64 - .as_ref() - .map(|uid| format!("/avatar_{}", uid_to_avatar_path(uid))) - .unwrap_or_else(|| "Hidden".to_string()) - } - ); - } else { - println!("Server Groups : Unknown"); - println!("Channel Group : Unknown"); - println!("Avatar Path : Unknown"); - } - - println!(); - println!("=== Connection Info ==="); - if let Some(connection) = connection_info { - println!( - "Connection Time : {}", - connection - .connected_time - .map(format_duration) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!("Idle Time : {}", format_duration(connection.idle_time)); - println!( - "Ping : {}", - connection - .ping - .map(format_duration) - .unwrap_or_else(|| "Unknown".to_string()) - ); - println!( - "Client Address : {}", - connection - .client_address - .map(|address| address.to_string()) - .unwrap_or_else(|| "Hidden".to_string()) - ); - println!( - "Packet Loss C->S : total={} speech={} keepalive={} control={}", - format_loss(connection.client_to_server_packetloss_total), - format_loss(connection.client_to_server_packetloss_speech), - format_loss(connection.client_to_server_packetloss_keepalive), - format_loss(connection.client_to_server_packetloss_control), - ); - println!( - "Packet Loss S->C : total={} speech={} keepalive={} control={}", - connection - .server_to_client_packetloss_total - .map(format_loss) - .unwrap_or_else(|| "Hidden".to_string()), - connection - .server_to_client_packetloss_speech - .map(format_loss) - .unwrap_or_else(|| "Hidden".to_string()), - connection - .server_to_client_packetloss_keepalive - .map(format_loss) - .unwrap_or_else(|| "Hidden".to_string()), - connection - .server_to_client_packetloss_control - .map(format_loss) - .unwrap_or_else(|| "Hidden".to_string()), - ); - println!( - "Bandwidth Sent : 1s[speech={} keepalive={} control={}] 1m[speech={} keepalive={} control={}]", - option_u64(connection.bandwidth_sent_last_second_speech), - option_u64(connection.bandwidth_sent_last_second_keepalive), - option_u64(connection.bandwidth_sent_last_second_control), - option_u64(connection.bandwidth_sent_last_minute_speech), - option_u64(connection.bandwidth_sent_last_minute_keepalive), - option_u64(connection.bandwidth_sent_last_minute_control), - ); - println!( - "Bandwidth Received : 1s[speech={} keepalive={} control={}] 1m[speech={} keepalive={} control={}]", - option_u64(connection.bandwidth_received_last_second_speech), - option_u64(connection.bandwidth_received_last_second_keepalive), - option_u64(connection.bandwidth_received_last_second_control), - option_u64(connection.bandwidth_received_last_minute_speech), - option_u64(connection.bandwidth_received_last_minute_keepalive), - option_u64(connection.bandwidth_received_last_minute_control), - ); - println!( - "Filetransfer BW : sent={} recv={}", - option_u64(connection.filetransfer_bandwidth_sent), - option_u64(connection.filetransfer_bandwidth_received), - ); - println!( - "Packets Sent : speech={} keepalive={} control={}", - option_u64(connection.packets_sent_speech), - option_u64(connection.packets_sent_keepalive), - option_u64(connection.packets_sent_control), - ); - println!( - "Packets Received : speech={} keepalive={} control={}", - option_u64(connection.packets_received_speech), - option_u64(connection.packets_received_keepalive), - option_u64(connection.packets_received_control), - ); - println!( - "Bytes Sent : speech={} keepalive={} control={}", - option_u64(connection.bytes_sent_speech), - option_u64(connection.bytes_sent_keepalive), - option_u64(connection.bytes_sent_control), - ); - println!( - "Bytes Received : speech={} keepalive={} control={}", - option_u64(connection.bytes_received_speech), - option_u64(connection.bytes_received_keepalive), - option_u64(connection.bytes_received_control), - ); - } else { - println!("Connection details unavailable (offline or permission-gated)."); - } - - println!(); - println!("=== Transfer Quota ==="); - if let Some(info) = client_info { - println!("Downloaded this month : {}", info.bytes_downloaded_month); - println!("Uploaded this month : {}", info.bytes_uploaded_month); - println!("Downloaded total : {}", info.bytes_downloaded_total); - println!("Uploaded total : {}", info.bytes_uploaded_total); - } else if let Some(info) = db_info { - println!("Downloaded this month : {}", info.bytes_downloaded_month); - println!("Uploaded this month : {}", info.bytes_uploaded_month); - println!("Downloaded total : {}", info.bytes_downloaded_total); - println!("Uploaded total : {}", info.bytes_uploaded_total); - } else { - println!("Quota details unavailable."); - } - - Ok(()) -} - -fn format_server_groups(groups: &[ServerGroupId], names: &HashMap) -> String { - let mut rendered = Vec::new(); - for group in groups { - rendered.push( - names - .get(group) - .cloned() - .unwrap_or_else(|| format!("Unknown ({})", group.0)), - ); - } - rendered.join(", ") -} - -fn format_timestamp(value: OffsetDateTime) -> String { - value - .format(&Rfc3339) - .unwrap_or_else(|_| value.unix_timestamp().to_string()) -} - -fn format_duration(duration: Duration) -> String { - let total_ms = duration.whole_milliseconds(); - if total_ms < 1000 { - return format!("{total_ms} ms"); - } - let total_seconds = duration.whole_seconds(); - let seconds = total_seconds % 60; - let minutes = (total_seconds / 60) % 60; - let hours = total_seconds / 3600; - if hours > 0 { - format!("{hours}h {minutes}m {seconds}s") - } else if minutes > 0 { - format!("{minutes}m {seconds}s") - } else { - format!("{seconds}s") - } -} - -fn format_loss(value: f32) -> String { - format!("{value:.4}") -} - -fn option_u64(value: Option) -> String { - value - .map(|number| number.to_string()) - .unwrap_or_else(|| "Hidden".to_string()) -} - -fn uid_to_b64(uid: &Uid) -> String { - BASE64_STANDARD.encode(&uid.0) -} - -fn uid_to_avatar_path(uid_b64: &str) -> String { - let decoded = BASE64_STANDARD.decode(uid_b64).unwrap_or_default(); - let mut rendered = String::with_capacity(decoded.len() * 2); - for byte in decoded { - rendered.push((b'a' + (byte >> 4)) as char); - rendered.push((b'a' + (byte & 0x0f)) as char); - } - rendered -} diff --git a/poc/tsidentity-import-spike/.gitignore b/poc/tsidentity-import-spike/.gitignore deleted file mode 100644 index 3a68598..0000000 --- a/poc/tsidentity-import-spike/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/sample_identity.ini -/target/ diff --git a/poc/tsidentity-import-spike/Cargo.lock b/poc/tsidentity-import-spike/Cargo.lock deleted file mode 100644 index bc9b327..0000000 --- a/poc/tsidentity-import-spike/Cargo.lock +++ /dev/null @@ -1,3452 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[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 = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[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 = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[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.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[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 = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest 0.10.7", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -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 = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[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 = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[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 = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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" -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", - "zeroize", -] - -[[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 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - -[[package]] -name = "git-testament" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74999c921479f919c87a9d2e6922a79a18683f18105344df8e067149232e51" -dependencies = [ - "git-testament-derive", -] - -[[package]] -name = "git-testament-derive" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeac967e71eb3dc1656742fc7521ec7cd3b6b88738face65bf1fddf702bc4c0" -dependencies = [ - "log", - "proc-macro2", - "quote", - "syn", - "time", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-net" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "futures-channel", - "futures-io", - "futures-util", - "hickory-proto", - "idna", - "ipnet", - "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" -dependencies = [ - "data-encoding", - "idna", - "ipnet", - "jni", - "once_cell", - "prefix-trie", - "rand 0.10.1", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-net", - "hickory-proto", - "ipconfig", - "ipnet", - "jni", - "moka", - "ndk-context", - "once_cell", - "parking_lot", - "rand 0.10.1", - "resolv-conf", - "smallvec", - "system-configuration", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[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 = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -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.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2", - "widestring", - "windows-registry", - "windows-result", - "windows-sys 0.61.2", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -dependencies = [ - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[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 = "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.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -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 = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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 = "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 = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "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 = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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", -] - -[[package]] -name = "omnom" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66562a92c66d5937e9d549597c2d3cafe3d2367dd08dc257da730587c2cfc218" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[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 = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[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 = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[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 = "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 = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - -[[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 = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[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 = "quicklz" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750eab265a26a0cd064f2a2b222bf172cc06d7a873e113e5f6d0118cc6e5fbb1" -dependencies = [ - "bit-vec", - "byteorder", - "thiserror 1.0.69", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "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 = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[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 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[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_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "reqwest" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "aws-lc-rs", - "once_cell", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -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.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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 = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "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 = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[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 = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[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" - -[[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 = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[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 = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[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 = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "t4rust-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e411692d6e0c8ce07f65a8210e95dcfb56cc4600940456592a49a3579ef6396" -dependencies = [ - "nom", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[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", - "num-conv", - "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 = "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.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "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 = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "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", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ts-bookkeeping" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "heck", - "itertools", - "num-derive", - "num-traits", - "serde", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tracing", - "tsproto-packets", - "tsproto-structs", - "tsproto-types", -] - -[[package]] -name = "tsclientlib" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "futures", - "git-testament", - "hickory-net", - "hickory-resolver", - "itertools", - "num-traits", - "pin-utils", - "rand 0.10.1", - "reqwest", - "thiserror 2.0.18", - "time", - "tokio", - "tokio-stream", - "tracing", - "ts-bookkeeping", - "tsproto", - "tsproto-packets", - "tsproto-types", - "url", -] - -[[package]] -name = "tsidentity-import-spike" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64", - "clap", - "futures", - "tokio", - "tracing", - "tracing-subscriber", - "tsclientlib", -] - -[[package]] -name = "tsproto" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "aes", - "base64", - "curve25519-dalek-ng", - "eax", - "futures", - "generic-array", - "git-testament", - "num-bigint", - "num-derive", - "num-traits", - "omnom", - "pin-project-lite", - "quicklz", - "rand 0.10.1", - "serde", - "sha-1", - "sha2", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "tsproto-packets", - "tsproto-types", -] - -[[package]] -name = "tsproto-packets" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "num-derive", - "num-traits", - "omnom", - "self_cell", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "tsproto-structs" -version = "0.2.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "csv", - "heck", - "once_cell", - "serde", - "toml", -] - -[[package]] -name = "tsproto-types" -version = "0.1.0" -source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" -dependencies = [ - "base64", - "bitflags", - "curve25519-dalek-ng", - "elliptic-curve", - "generic-array", - "heck", - "num-bigint", - "num-derive", - "num-traits", - "p256", - "rand 0.8.6", - "ref-cast", - "serde", - "sha-1", - "simple_asn1", - "t4rust-derive", - "thiserror 2.0.18", - "time", - "tsproto-structs", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.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 = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[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-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[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.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.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.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.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.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.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.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.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.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 = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -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/poc/tsidentity-import-spike/Cargo.toml b/poc/tsidentity-import-spike/Cargo.toml deleted file mode 100644 index 14e1813..0000000 --- a/poc/tsidentity-import-spike/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "tsidentity-import-spike" -version = "0.1.0" -edition = "2021" -publish = false -description = "Chanora PoC: import/export official TeamSpeak identity files and connect using the imported identity." - -[workspace] - -[dependencies] -anyhow = "1" -base64 = "0.22" -clap = { version = "4", features = ["derive"] } -futures = "0.3" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["default-tls"] } diff --git a/poc/tsidentity-import-spike/README.md b/poc/tsidentity-import-spike/README.md deleted file mode 100644 index 7e9dcb1..0000000 --- a/poc/tsidentity-import-spike/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# tsidentity Import Spike - -Chanora proof-of-concept. **Not product code.** - -## Purpose - -Prove TeamSpeak identity import/export compatibility with the official INI-style -format and verify that an imported identity can be used for a live connection. - -## What it does - -1. Parses an exported TeamSpeak identity file. -2. Computes the imported identity UID and security level. -3. Re-exports the identity in TeamSpeak's `counter + V + obfuscated-key` form. -4. Connects to `kr.teamspeak.app` using the imported identity and nickname. - -## Run - -```bash -cargo test --offline - -cargo run --offline -- \ - --identity-file /path/to/exported_identity.ini \ - --address kr.teamspeak.app -``` - -## Important finding - -The validation identity used during this spike round-tripped correctly, but its -UID `eaRkG62hRaLs+R9sOuOWK7xbnUY=` did **not** match the live `EdisonJwa` UID -`QcnldW6Qnw/4im/t94j/FYIcVMU=` observed on `kr.teamspeak.app`. - -## Verification - -See `VERIFICATION.md`. diff --git a/poc/tsidentity-import-spike/VERIFICATION.md b/poc/tsidentity-import-spike/VERIFICATION.md deleted file mode 100644 index fa77fed..0000000 --- a/poc/tsidentity-import-spike/VERIFICATION.md +++ /dev/null @@ -1,59 +0,0 @@ -# Verification record — `tsidentity-import-spike` - -## Result - -PASS. The sample TeamSpeak identity file imported successfully, exported back to -the exact same identity string, and was usable for a live connection to -`kr.teamspeak.app`. - -## Environment - -| Field | Value | -|---|---| -| Date | 2026-05-25 | -| Host OS | Linux (x86_64) | -| Build profile | test + dev | -| Target server | `kr.teamspeak.app` | - -## Commands - -```bash -cargo test --offline - -cargo run --offline -- \ - --identity-file /path/to/exported_identity.ini \ - --address kr.teamspeak.app -``` - -## What was observed - -- Unit test passed: - - exported identity INI parsed successfully - - exported identity string matched the original exactly -- Imported sample identity properties: - - nickname: `Edison.Jwa` - - UID: `eaRkG62hRaLs+R9sOuOWK7xbnUY=` - - security level: `8` - - counter: `40` -- Live connect using the imported identity succeeded: - - connected nickname: `Edison.Jwa` - - connected UID: `eaRkG62hRaLs+R9sOuOWK7xbnUY=` - - connected DBID: `17225` - - connected CLID: `18506` - - connected CID: `1` - - country: `KR` - - server groups: `{ServerGroupId(8)}` - -## Important finding - -The provided sample identity is **not** the live `EdisonJwa` identity currently -online on `kr.teamspeak.app`. - -- Sample imported identity UID: `eaRkG62hRaLs+R9sOuOWK7xbnUY=` -- Live `EdisonJwa` UID from the query spike: `QcnldW6Qnw/4im/t94j/FYIcVMU=` - -That means: - -- the import/export implementation is working, and -- the sample INI belongs to a different TeamSpeak identity than the live - `EdisonJwa` account used in the example profile. diff --git a/poc/tsidentity-import-spike/src/main.rs b/poc/tsidentity-import-spike/src/main.rs deleted file mode 100644 index d316ba7..0000000 --- a/poc/tsidentity-import-spike/src/main.rs +++ /dev/null @@ -1,249 +0,0 @@ -use std::fs; -use std::path::PathBuf; - -use anyhow::{anyhow, bail, Context, Result}; -use clap::Parser; -use futures::prelude::*; -use tokio::time as tokio_time; -use tracing::info; -use tracing_subscriber::EnvFilter; - -use tsclientlib::{Connection, DisconnectOptions, Identity, OutCommandExt, StreamItem}; - -#[derive(Parser, Debug)] -#[command( - name = "tsidentity-import-spike", - about = "Chanora PoC: parse/export TeamSpeak identity files and connect with the imported identity." -)] -struct Args { - /// Path to an exported TeamSpeak identity INI file. - #[arg(long)] - identity_file: PathBuf, - - /// Server address for the live connect verification. - #[arg(short, long, default_value = "kr.teamspeak.app")] - address: String, - - /// Override the nickname from the identity file for the live connect verification. - #[arg(long)] - nickname: Option, - - /// Server password if required. - #[arg(long)] - password: Option, - - /// Skip the live connection step and only validate the import/export round trip. - #[arg(long)] - no_connect: bool, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct IdentityRecord { - id: String, - identity: String, - nickname: String, - phonetic_nickname: String, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn")); - tracing_subscriber::fmt().with_env_filter(filter).init(); - - let raw = fs::read_to_string(&args.identity_file) - .with_context(|| format!("reading {}", args.identity_file.display()))?; - let record = IdentityRecord::parse(&raw)?; - let identity = Identity::new_from_str(&record.identity).context("parsing TeamSpeak identity")?; - - let computed_uid = identity.key().to_pub().get_uid(); - let roundtrip_identity = export_identity_string(&identity); - let roundtrip_record = IdentityRecord { - id: record.id.clone(), - identity: roundtrip_identity.clone(), - nickname: record.nickname.clone(), - phonetic_nickname: record.phonetic_nickname.clone(), - }; - - println!("=== Identity Import ==="); - println!("Identity file : {}", args.identity_file.display()); - println!("Profile id : {}", record.id); - println!("Nickname : {}", record.nickname); - println!("UID : {}", computed_uid); - println!("Security level : {}", identity.level()); - println!("Counter : {}", identity.counter()); - println!("Roundtrip match : {}", if roundtrip_identity == record.identity { "yes" } else { "no" }); - println!(); - println!("=== Export Preview ==="); - println!("{}", roundtrip_record.render()); - - if args.no_connect { - return Ok(()); - } - - let connect_nickname = args.nickname.clone().unwrap_or_else(|| record.nickname.clone()); - info!( - target: "identity-spike", - address = %args.address, - nickname = %connect_nickname, - uid = %computed_uid, - "connecting with imported identity" - ); - - let mut builder = Connection::build(args.address.clone()) - .name(connect_nickname.clone()) - .identity(identity); - if let Some(password) = &args.password { - builder = builder.password(password.clone()); - } - - let mut con = builder.connect().context("dialling server with imported identity")?; - wait_for_initial_state(&mut con).await?; - if let Ok(state) = con.get_state() { - let _ = state.server.set_subscribed(true).send(&mut con); - } - pump_events(&mut con, std::time::Duration::from_secs(1)).await?; - - let state = con.get_state().context("reading post-connect state")?; - let own = state - .clients - .get(&state.own_client) - .ok_or_else(|| anyhow!("own client missing after imported-identity connect"))?; - let own_uid = own - .uid - .as_ref() - .map(|uid| tsclientlib_uid_to_b64(uid.as_ref())) - .unwrap_or_else(|| "".to_string()); - - println!(); - println!("=== Live Connect Result ==="); - println!("Connected nickname : {}", own.name); - println!("Connected UID : {}", own_uid); - println!("Connected DBID : {}", own.database_id.0); - println!("Connected CLID : {}", own.id.0); - println!("Connected CID : {}", own.channel.0); - println!( - "Country : {}", - if own.country_code.is_empty() { - "Unknown".to_string() - } else { - own.country_code.clone() - } - ); - println!("Server groups : {:?}", own.server_groups); - println!("Avatar hash : {}", own.avatar_hash); - - con.disconnect(DisconnectOptions::new()).ok(); - con.events().for_each(|_| futures::future::ready(())).await; - Ok(()) -} - -impl IdentityRecord { - fn parse(raw: &str) -> Result { - let mut section = None::; - let mut map = std::collections::HashMap::new(); - for line in raw.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - if trimmed.starts_with('[') && trimmed.ends_with(']') { - section = Some(trimmed[1..trimmed.len() - 1].to_string()); - continue; - } - if section.as_deref() != Some("Identity") { - continue; - } - let (key, value) = trimmed - .split_once('=') - .ok_or_else(|| anyhow!("invalid identity line: {trimmed}"))?; - map.insert(key.to_string(), unquote(value)); - } - - Ok(Self { - id: required(&map, "id")?, - identity: required(&map, "identity")?, - nickname: required(&map, "nickname")?, - phonetic_nickname: map.get("phonetic_nickname").cloned().unwrap_or_default(), - }) - } - - fn render(&self) -> String { - format!( - "[Identity]\nid={}\nidentity=\"{}\"\nnickname={}\nphonetic_nickname={}\n", - self.id, self.identity, self.nickname, self.phonetic_nickname - ) - } -} - -fn required(map: &std::collections::HashMap, key: &str) -> Result { - map.get(key) - .cloned() - .ok_or_else(|| anyhow!("missing required identity key `{key}`")) -} - -fn unquote(value: &str) -> String { - let trimmed = value.trim(); - if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { - trimmed[1..trimmed.len() - 1].to_string() - } else { - trimmed.to_string() - } -} - -fn export_identity_string(identity: &Identity) -> String { - format!("{}V{}", identity.counter(), identity.key().to_ts_obfuscated()) -} - -async fn wait_for_initial_state(con: &mut Connection) -> Result<()> { - loop { - let event = con - .events() - .next() - .await - .ok_or_else(|| anyhow!("event stream ended before initial state"))??; - if matches!(event, StreamItem::BookEvents(_)) { - return Ok(()); - } - } -} - -async fn pump_events(con: &mut Connection, budget: std::time::Duration) -> Result<()> { - let deadline = tokio_time::Instant::now() + budget; - loop { - let now = tokio_time::Instant::now(); - if now >= deadline { - return Ok(()); - } - let remaining = deadline - now; - match tokio_time::timeout(remaining, con.events().next()).await { - Ok(Some(Ok(_))) => {} - Ok(Some(Err(error))) => return Err(error).context("pumping connection events"), - Ok(None) => bail!("event stream ended while pumping events"), - Err(_) => return Ok(()), - } - } -} - -fn tsclientlib_uid_to_b64(uid: &tsclientlib::Uid) -> String { - use base64::prelude::*; - BASE64_STANDARD.encode(&uid.0) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_and_roundtrips_sample_identity() { - let raw = include_str!("../sample_identity.ini"); - let record = IdentityRecord::parse(raw).expect("sample identity must parse"); - let identity = Identity::new_from_str(&record.identity).expect("identity must parse"); - let uid = identity.key().to_pub().get_uid(); - assert_eq!(uid, "eaRkG62hRaLs+R9sOuOWK7xbnUY="); - assert_eq!(export_identity_string(&identity), record.identity); - assert_eq!(record.nickname, "Edison.Jwa"); - } -} diff --git a/tools/build-android-aab.sh b/tools/build-android-aab.sh deleted file mode 100755 index 0ad2009..0000000 --- a/tools/build-android-aab.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -APP_DIR="$REPO_ROOT/apps/chanora_flutter" -PUBSPEC="$APP_DIR/pubspec.yaml" -OBOE_DIR="$(cd "$REPO_ROOT/.." && pwd)/oboe-rs" -DIST_DIR="$REPO_ROOT/dist/android" -TMP_DIR="" - -cleanup() { - if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then - rm -rf "$TMP_DIR" - fi -} -trap cleanup EXIT - -require_cmd() { - local cmd=$1 - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "ERROR: required command '$cmd' is not on PATH." >&2 - exit 1 - fi -} - -require_file() { - local path=$1 - local label=$2 - if [[ ! -f "$path" ]]; then - echo "ERROR: missing $label at $path" >&2 - exit 1 - fi -} - -ensure_release_signing() { - if [[ -n "${CHANORA_RELEASE_STORE_FILE:-}" ]]; then - local missing=() - [[ -n "${CHANORA_RELEASE_STORE_PASSWORD:-}" ]] || missing+=("CHANORA_RELEASE_STORE_PASSWORD") - [[ -n "${CHANORA_RELEASE_KEY_ALIAS:-}" ]] || missing+=("CHANORA_RELEASE_KEY_ALIAS") - [[ -n "${CHANORA_RELEASE_KEY_PASSWORD:-}" ]] || missing+=("CHANORA_RELEASE_KEY_PASSWORD") - if (( ${#missing[@]} > 0 )); then - printf 'ERROR: release signing is partially configured; missing %s\n' "${missing[*]}" >&2 - exit 1 - fi - require_file "$CHANORA_RELEASE_STORE_FILE" "release keystore" - return - fi - - require_cmd keytool - TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/chanora-android-aab.XXXXXX")" - - export CHANORA_RELEASE_STORE_FILE="$TMP_DIR/chanora-release.keystore" - export CHANORA_RELEASE_STORE_PASSWORD="${CHANORA_RELEASE_STORE_PASSWORD:-chanora-ci}" - export CHANORA_RELEASE_KEY_ALIAS="${CHANORA_RELEASE_KEY_ALIAS:-chanora-ci}" - export CHANORA_RELEASE_KEY_PASSWORD="${CHANORA_RELEASE_KEY_PASSWORD:-$CHANORA_RELEASE_STORE_PASSWORD}" - - keytool -genkeypair \ - -storetype PKCS12 \ - -keystore "$CHANORA_RELEASE_STORE_FILE" \ - -storepass "$CHANORA_RELEASE_STORE_PASSWORD" \ - -keypass "$CHANORA_RELEASE_KEY_PASSWORD" \ - -alias "$CHANORA_RELEASE_KEY_ALIAS" \ - -keyalg RSA \ - -keysize 2048 \ - -validity 3650 \ - -dname "CN=Chanora CI,O=Chanora,OU=Engineering,L=Seoul,S=Seoul,C=KR" \ - -noprompt >/dev/null 2>&1 - - printf 'Generated ephemeral Android release keystore: %s\n' "$CHANORA_RELEASE_STORE_FILE" -} - -main() { - require_cmd flutter - require_file "$PUBSPEC" "Flutter pubspec" - - if [[ ! -f "$OBOE_DIR/Cargo.toml" ]]; then - cat >&2 <&2 - exit 1 - fi - - ( - cd "$APP_DIR" - flutter pub get - flutter build appbundle --release - ) - - local source_aab="$APP_DIR/build/app/outputs/bundle/release/app-release.aab" - require_file "$source_aab" "Android release app bundle" - - mkdir -p "$DIST_DIR" - local output="$DIST_DIR/chanora-${app_version}-release.aab" - cp "$source_aab" "$output" - - printf 'Built Android release bundle: %s\n' "$output" -} - -main "$@" diff --git a/tools/build-linux-deb.sh b/tools/build-linux-deb.sh deleted file mode 100755 index 5755aee..0000000 --- a/tools/build-linux-deb.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -APP_DIR="$REPO_ROOT/apps/chanora_flutter" -PUBSPEC="$APP_DIR/pubspec.yaml" -OBOE_DIR="$(cd "$REPO_ROOT/.." && pwd)/oboe-rs" -DIST_DIR="$REPO_ROOT/dist/linux" -STAGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/chanora-linux-deb.XXXXXX")" -PKG_ROOT="$STAGE_DIR/package" -INSTALL_ROOT="$PKG_ROOT/opt/chanora" -ICON_SRC="$APP_DIR/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" - -cleanup() { - rm -rf "$STAGE_DIR" -} -trap cleanup EXIT - -require_cmd() { - local cmd=$1 - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "ERROR: required command '$cmd' is not on PATH." >&2 - exit 1 - fi -} - -require_file() { - local path=$1 - local label=$2 - if [[ ! -f "$path" ]]; then - echo "ERROR: missing $label at $path" >&2 - exit 1 - fi -} - -resolve_arch() { - if command -v dpkg >/dev/null 2>&1; then - dpkg --print-architecture - return - fi - - case "$(uname -m)" in - x86_64) echo "amd64" ;; - aarch64 | arm64) echo "arm64" ;; - *) - echo "ERROR: unsupported Linux architecture '$(uname -m)'" >&2 - exit 1 - ;; - esac -} - -flutter_linux_arch() { - case "$1" in - amd64) echo "x64" ;; - arm64) echo "arm64" ;; - *) - echo "ERROR: unsupported Flutter Linux architecture '$1'" >&2 - exit 1 - ;; - esac -} - -main() { - require_cmd flutter - require_cmd dpkg-deb - require_file "$PUBSPEC" "Flutter pubspec" - require_file "$ICON_SRC" "launcher icon" - - if [[ ! -f "$OBOE_DIR/Cargo.toml" ]]; then - cat >&2 <&2 - exit 1 - fi - - local deb_version - deb_version="${app_version/-/~}" - - local arch - arch="$(resolve_arch)" - local flutter_arch - flutter_arch="$(flutter_linux_arch "$arch")" - - ( - cd "$APP_DIR" - flutter pub get - flutter build linux --release - ) - - local bundle_dir="$APP_DIR/build/linux/$flutter_arch/release/bundle" - local app_binary="$bundle_dir/chanora_flutter" - require_file "$app_binary" "Linux release bundle binary" - require_file "$bundle_dir/lib/libonnxruntime.so" "bundled Linux ONNX Runtime shared library" - - mkdir -p \ - "$PKG_ROOT/DEBIAN" \ - "$INSTALL_ROOT" \ - "$PKG_ROOT/usr/bin" \ - "$PKG_ROOT/usr/share/applications" \ - "$PKG_ROOT/usr/share/icons/hicolor/256x256/apps" - - cp -R "$bundle_dir/." "$INSTALL_ROOT/" - - cat >"$PKG_ROOT/DEBIAN/control" <"$PKG_ROOT/usr/bin/chanora" <<'EOF' -#!/bin/sh -exec /opt/chanora/chanora_flutter "$@" -EOF - chmod 0755 "$PKG_ROOT/usr/bin/chanora" - - cat >"$PKG_ROOT/usr/share/applications/chanora.desktop" <<'EOF' -[Desktop Entry] -Type=Application -Name=Chanora -GenericName=Voice Client -Comment=Voice client for TeamSpeak-compatible servers -Exec=/usr/bin/chanora -Icon=chanora -Terminal=false -Categories=Network;Chat;AudioVideo; -StartupNotify=true -EOF - - cp "$ICON_SRC" "$PKG_ROOT/usr/share/icons/hicolor/256x256/apps/chanora.png" - - mkdir -p "$DIST_DIR" - local output="$DIST_DIR/chanora_${deb_version}_${arch}.deb" - dpkg-deb --build --root-owner-group "$PKG_ROOT" "$output" >/dev/null - - printf 'Built Linux package: %s\n' "$output" -} - -main "$@" diff --git a/tools/build-linux.sh b/tools/build-linux.sh index 99a5977..25b92e5 100755 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -6,9 +6,7 @@ # - Rust stable with x86_64-unknown-linux-gnu target # - Flutter 3.41.9+ stable on PATH # - CMake 3.13+, pkg-config, GTK3 dev headers, clang -# - System libopus development package (libopus-dev / opus-devel) -# - Optional: CHANORA_ONNXRUNTIME_SHARED_LIB=/abs/path/libonnxruntime.so -# to override the ONNX Runtime shared library bundled under bundle/lib/ +# - System libopus dev package (libopus-dev / opus-devel) # - flutter_rust_bridge_codegen 2.12.0 (only if regenerating bindings) # # Usage (from the repo root ~/chanora): @@ -28,8 +26,6 @@ VERSION="v0.2.0-beta.1" SKIP_RUST=0 REGEN=0 TARGET="x86_64-unknown-linux-gnu" -ONNXRUNTIME_SHARED_LIB="${CHANORA_ONNXRUNTIME_SHARED_LIB:-}" -ONNXRUNTIME_VERSION="${CHANORA_ONNXRUNTIME_VERSION:-1.26.0}" while [[ $# -gt 0 ]]; do case "$1" in @@ -52,65 +48,11 @@ echo "Repo root : $REPO_ROOT" echo "Version : $VERSION" echo "Target : $TARGET" echo "Build Rust: $([ "$SKIP_RUST" = 1 ] && echo skip || echo yes)" -echo "Bundle ORT: ${ONNXRUNTIME_SHARED_LIB:-auto ($ONNXRUNTIME_VERSION)}" bar -resolve_onnxruntime_shared_lib() { - if [[ -n "$ONNXRUNTIME_SHARED_LIB" ]]; then - if [[ ! -f "$ONNXRUNTIME_SHARED_LIB" ]]; then - echo " ERROR: CHANORA_ONNXRUNTIME_SHARED_LIB does not exist:" >&2 - echo " $ONNXRUNTIME_SHARED_LIB" >&2 - exit 1 - fi - return - fi - - local ort_arch - case "$TARGET" in - x86_64-unknown-linux-gnu) ort_arch="x64" ;; - aarch64-unknown-linux-gnu) ort_arch="aarch64" ;; - *) - echo " ERROR: automatic ONNX Runtime bundling does not support target $TARGET." >&2 - echo " Set CHANORA_ONNXRUNTIME_SHARED_LIB=/absolute/path/to/libonnxruntime.so." >&2 - exit 1 - ;; - esac - - local archive_name="onnxruntime-linux-${ort_arch}-${ONNXRUNTIME_VERSION}.tgz" - local cache_dir="$REPO_ROOT/.cache/onnxruntime/$ONNXRUNTIME_VERSION/$ort_arch" - local archive_path="$cache_dir/$archive_name" - local extract_dir="$cache_dir/extracted" - local extracted_lib="$extract_dir/onnxruntime-linux-${ort_arch}-${ONNXRUNTIME_VERSION}/lib/libonnxruntime.so" - local url="https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/${archive_name}" - - if [[ ! -f "$extracted_lib" ]]; then - mkdir -p "$cache_dir" "$extract_dir" - if [[ ! -f "$archive_path" ]]; then - if ! command -v curl >/dev/null; then - echo " ERROR: curl is required to download ONNX Runtime automatically." >&2 - echo " Install curl or set CHANORA_ONNXRUNTIME_SHARED_LIB." >&2 - exit 1 - fi - echo " Downloading ONNX Runtime $ONNXRUNTIME_VERSION for linux-$ort_arch" - curl --fail --location --show-error --output "$archive_path" "$url" - fi - rm -rf "$extract_dir" - mkdir -p "$extract_dir" - tar -xzf "$archive_path" -C "$extract_dir" - fi - - if [[ ! -f "$extracted_lib" ]]; then - echo " ERROR: ONNX Runtime archive did not contain expected library:" >&2 - echo " $extracted_lib" >&2 - exit 1 - fi - - ONNXRUNTIME_SHARED_LIB="$extracted_lib" -} - # ---------- 1. Verify toolchain ---------- echo "[1/6] Verify toolchain" -for cmd in cargo rustc flutter cmake pkg-config clang gcc tar; do +for cmd in cargo rustc flutter cmake pkg-config clang gcc; do if ! command -v "$cmd" >/dev/null; then echo " ERROR: '$cmd' not on PATH. See docs/release/linux-build.md." >&2 exit 1 @@ -124,52 +66,11 @@ if ! pkg-config --exists gtk+-3.0 2>/dev/null; then exit 1 fi -# Verify libopus development files. tsclientlib's audio feature pulls in -# audiopus, which links libopus for Linux builds/tests. +# Verify libopus dev headers (needed by audiopus_sys to link) if ! pkg-config --exists opus 2>/dev/null; then - echo " ERROR: opus pkg-config metadata not found." >&2 - echo " Install the Opus development package (libopus-dev on" >&2 - echo " Debian/Ubuntu or opus-devel on Fedora)." >&2 + echo " ERROR: opus dev headers not found. Install libopus-dev (Debian/Ubuntu) or opus-devel (Fedora)." >&2 exit 1 fi -printf " %-12s : %s\n" "opus" "$(pkg-config --modversion opus)" - -# Verify the exact PipeWire pkg-config modules the primary Linux voice backend uses. -# `pipewire-0.3` is not the module name exposed by distro packages; the crates -# resolve `libpipewire-0.3` and `libspa-0.2`. -if ! pkg-config --exists libpipewire-0.3 2>/dev/null; then - echo " ERROR: libpipewire-0.3 pkg-config metadata not found." >&2 - echo " Install PipeWire development headers (for example pipewire-devel" >&2 - echo " on Fedora or libpipewire-0.3-dev on Debian/Ubuntu)." >&2 - exit 1 -fi -if ! pkg-config --exists libspa-0.2 2>/dev/null; then - echo " ERROR: libspa-0.2 pkg-config metadata not found." >&2 - echo " Install the PipeWire SPA development headers packaged with" >&2 - echo " your distro's PipeWire development package." >&2 - exit 1 -fi -printf " %-12s : %s\n" "libpipewire" "$(pkg-config --modversion libpipewire-0.3)" -printf " %-12s : %s\n" "libspa" "$(pkg-config --modversion libspa-0.2)" - -# Verify PulseAudio development files used by the fallback Linux voice backend. -if ! pkg-config --exists libpulse 2>/dev/null; then - echo " ERROR: libpulse pkg-config metadata not found." >&2 - echo " Install PulseAudio development headers (for example libpulse-dev" >&2 - echo " on Debian/Ubuntu, pulseaudio-libs-devel on Fedora, or libpulse" >&2 - echo " on Arch Linux)." >&2 - exit 1 -fi -if ! pkg-config --exists libpulse-simple 2>/dev/null; then - echo " ERROR: libpulse-simple pkg-config metadata not found." >&2 - echo " Install PulseAudio simple API development headers." >&2 - exit 1 -fi -printf " %-12s : %s\n" "libpulse" "$(pkg-config --modversion libpulse)" -printf " %-12s : %s\n" "pulse-simple" "$(pkg-config --modversion libpulse-simple)" - -resolve_onnxruntime_shared_lib -echo " ORT source : $ONNXRUNTIME_SHARED_LIB" # Verify Rust target if ! rustup target list --installed | grep -q "^$TARGET$"; then @@ -223,8 +124,6 @@ fi # ---------- 5. flutter build linux --release ---------- echo "[5/6] flutter build linux --release" cd "$FLUTTER_APP" -export CHANORA_BRIDGE_SHARED_LIB="$BRIDGE_SO" -export CHANORA_ONNXRUNTIME_SHARED_LIB="$ONNXRUNTIME_SHARED_LIB" flutter pub get flutter build linux --release @@ -235,25 +134,12 @@ if [[ ! -f "$EXE" ]]; then exit 1 fi +# Copy the bridge .so into the bundle's lib/ directory so dart:ffi finds it. mkdir -p "$BUNDLE_DIR/lib" -rm -f "$BUNDLE_DIR/lib/libonnxruntime.so" -cp "$ONNXRUNTIME_SHARED_LIB" "$BUNDLE_DIR/lib/libonnxruntime.so" - +cp "$BRIDGE_SO" "$BUNDLE_DIR/lib/" echo " Bundle dir : $BUNDLE_DIR" echo " EXE : $EXE" echo " Bridge .so : $BUNDLE_DIR/lib/libchanora_bridge.so" -if [[ ! -f "$BUNDLE_DIR/lib/libchanora_bridge.so" ]]; then - echo " ERROR: bridge shared library missing from Linux bundle:" >&2 - echo " $BUNDLE_DIR/lib/libchanora_bridge.so" >&2 - exit 1 -fi -if [[ -f "$BUNDLE_DIR/lib/libonnxruntime.so" ]]; then - echo " ORT .so : $BUNDLE_DIR/lib/libonnxruntime.so" -else - echo " ERROR: ONNX Runtime bundle file missing after flutter build:" >&2 - echo " $BUNDLE_DIR/lib/libonnxruntime.so" >&2 - exit 1 -fi # ---------- 6. Package the bundle ---------- echo "[6/6] Package release tarball" @@ -276,4 +162,4 @@ echo " target/$TARGET/release/libchanora_bridge.so" echo " Bundle dir:" echo " $BUNDLE_DIR/" echo " Tarball:" -echo " $TAR_PATH" +echo " $TAR_PATH" \ No newline at end of file diff --git a/tools/dev-env.sh b/tools/dev-env.sh deleted file mode 100755 index 0760cfe..0000000 --- a/tools/dev-env.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# Source this file before local verification commands: -# source tools/dev-env.sh - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd "${script_dir}/.." && pwd)" - -if [[ -x "${repo_root}/.flutter-home/flutter/bin/flutter" ]]; then - export PATH="${repo_root}/.flutter-home/flutter/bin:${PATH}" -elif [[ -n "${FLUTTER_HOME:-}" && -x "${FLUTTER_HOME}/bin/flutter" ]]; then - export PATH="${FLUTTER_HOME}/bin:${PATH}" -elif [[ -x "${HOME}/sdks/flutter/bin/flutter" ]]; then - export PATH="${HOME}/sdks/flutter/bin:${PATH}" -fi - -if [[ -d "${HOME}/.cargo/bin" ]]; then - export PATH="${HOME}/.cargo/bin:${PATH}" -fi