diff --git a/Cargo.lock b/Cargo.lock index dddd097..93086bc 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 2.11.1", + "bitflags", "cfg-if", "libc", ] @@ -121,6 +121,16 @@ 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" @@ -260,18 +270,31 @@ 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" @@ -367,6 +390,25 @@ 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" @@ -427,13 +469,15 @@ 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", @@ -493,6 +537,7 @@ name = "chanora_protocol" version = "0.2.0-beta.1" dependencies = [ "async-trait", + "base64", "chanora_resolver", "futures", "reqwest 0.13.3", @@ -579,6 +624,17 @@ 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" @@ -658,6 +714,12 @@ 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" @@ -690,7 +752,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -1055,7 +1117,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags", "block2", "libc", "objc2", @@ -1514,6 +1576,12 @@ 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" @@ -2135,6 +2203,16 @@ 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" @@ -2145,6 +2223,81 @@ 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" @@ -2162,7 +2315,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", ] @@ -2345,7 +2498,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.1", + "bitflags", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2378,6 +2531,15 @@ 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" @@ -2488,7 +2650,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", "objc2", "objc2-core-audio", @@ -2526,7 +2688,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.11.1", + "bitflags", "objc2", ] @@ -2536,7 +2698,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags", "block2", "dispatch2", "libc", @@ -2555,7 +2717,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags", "block2", "libc", "objc2", @@ -2624,7 +2786,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -2677,7 +2839,7 @@ version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" dependencies = [ - "libloading", + "libloading 0.9.0", "ndarray", "ort-sys", "smallvec", @@ -2769,6 +2931,31 @@ 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" @@ -3116,7 +3303,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3283,7 +3470,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.11.1", + "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3338,7 +3525,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -3457,29 +3644,6 @@ 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" @@ -3500,7 +3664,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3513,7 +3677,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3868,7 +4032,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3883,13 +4047,26 @@ 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", + "nom 7.1.3", "proc-macro2", "quote", "syn", @@ -3901,6 +4078,12 @@ 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" @@ -4188,7 +4371,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags", "bytes", "futures-util", "http", @@ -4384,7 +4567,7 @@ version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" dependencies = [ "base64", - "bitflags 2.11.1", + "bitflags", "num-derive", "num-traits", "omnom", @@ -4412,7 +4595,7 @@ version = "0.1.0" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" dependencies = [ "base64", - "bitflags 2.11.1", + "bitflags", "curve25519-dalek-ng", "elliptic-curve", "generic-array", @@ -4455,6 +4638,12 @@ 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" @@ -4521,9 +4710,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -4657,7 +4846,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -4716,6 +4905,22 @@ 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" @@ -4725,6 +4930,12 @@ 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" @@ -5170,7 +5381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags", "indexmap", "log", "serde", diff --git a/apps/chanora_flutter/android/app/build.gradle.kts b/apps/chanora_flutter/android/app/build.gradle.kts index a105572..adcaa58 100644 --- a/apps/chanora_flutter/android/app/build.gradle.kts +++ b/apps/chanora_flutter/android/app/build.gradle.kts @@ -352,7 +352,22 @@ 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 rustupToolchain = System.getenv("RUSTUP_TOOLCHAIN") ?: "stable-aarch64-apple-darwin" + 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 // SDD-118 item 13 (corrected): per-ABI Exec sub-tasks. Each runs an // isolated `cargo ndk -t ... -- build ...` so ANDROID_ABI is set diff --git a/apps/chanora_flutter/android/gradle.properties b/apps/chanora_flutter/android/gradle.properties index fbee1d8..d5da727 100644 --- a/apps/chanora_flutter/android/gradle.properties +++ b/apps/chanora_flutter/android/gradle.properties @@ -1,2 +1,6 @@ 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/lib/design/platform_capabilities.dart b/apps/chanora_flutter/lib/design/platform_capabilities.dart index 5182578..adfe376 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: 'PulseAudio/ALSA via cpal.', + description: 'PipeWire voice I/O with PulseAudio fallback.', tier: CapabilityTier.supported, ), const PlatformCapability( diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 9c18b2b..92fb5a2 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -226,6 +226,9 @@ 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; @@ -314,6 +317,105 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { } 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) { @@ -1482,6 +1584,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { void _applySnapshot(rust.BridgeSnapshot snap) { _snapshot = snap; + unawaited(_applyClientPlaybackPreferencesToSnapshot(snap)); final own = ownClientSnapshotState(snap); if (own == null) return; _inputMuted = own.inputMuted; @@ -2013,6 +2116,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver { onJoinChannel: (ch) => _onJoinChannel(ch), onJoinChannelWithPassword: (ch) => _onJoinChannel(ch, askForPassword: true), + clientPlaybackPreferences: _clientPlaybackPrefs, + onClientPlaybackPreferenceChanged: + _setClientPlaybackPreference, onTs3ServerLink: _onTs3ServerLink, ); final ownClientState = _ownClientState; diff --git a/apps/chanora_flutter/lib/services/app_bootstrap.dart b/apps/chanora_flutter/lib/services/app_bootstrap.dart index ef14815..1c135b7 100644 --- a/apps/chanora_flutter/lib/services/app_bootstrap.dart +++ b/apps/chanora_flutter/lib/services/app_bootstrap.dart @@ -18,17 +18,19 @@ Future? _storageInitFuture; Future? _vadBootstrapFuture; StorageDirectoryProvider _storageDirectoryProvider = getApplicationSupportDirectory; +StorageDirectoryProvider _vadModelDirectoryProvider = + getApplicationSupportDirectory; StorageInitializer _storageInitializer = _defaultStorageInitializer; Future _defaultStorageInitializer(String dir) { return rust.initStorage(dir: dir); } -Future _copyBundledAssetToDocuments({ +Future _copyBundledAssetToManagedDirectory({ required String assetPath, required String fileName, }) async { - final dir = await getApplicationDocumentsDirectory(); + final dir = await _vadModelDirectoryProvider(); final file = File('${dir.path}/$fileName'); final data = await rootBundle.load(assetPath); final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); @@ -59,7 +61,7 @@ Future configureBundledVadModels() async { } Future _configureBundledVadModelsImpl() async { - final silero = await _copyBundledAssetToDocuments( + final silero = await _copyBundledAssetToManagedDirectory( assetPath: _sileroVadAsset, fileName: 'silero_vad.onnx', ); @@ -124,12 +126,15 @@ 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/startup_dependency_check.dart b/apps/chanora_flutter/lib/services/startup_dependency_check.dart index b361a17..9f6e740 100644 --- a/apps/chanora_flutter/lib/services/startup_dependency_check.dart +++ b/apps/chanora_flutter/lib/services/startup_dependency_check.dart @@ -117,8 +117,20 @@ Future checkStartupDependencies() async { final executableDir = File(_resolvedExecutableProvider()).parent.path; final issues = []; - if (!_hasAnyLoadableLibrary(const ['libSDL2.so', 'libSDL2-2.0.so.0'])) { - issues.add(_buildSdlIssue(distro)); + 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)) { @@ -272,33 +284,76 @@ _LinuxArch _detectLinuxArch() { }; } -StartupDependencyIssue _buildSdlIssue(_LinuxDistro distro) { +StartupDependencyIssue _buildPipeWireIssue(_LinuxDistro distro) { final List hints = switch (distro) { _LinuxDistro.debian => const [ StartupInstallHint( label: 'Debian / Ubuntu', - command: 'sudo apt install libsdl2-2.0-0', + command: 'sudo apt install libpipewire-0.3-0', ), ], _LinuxDistro.fedora => const [ - StartupInstallHint(label: 'Fedora', command: 'sudo dnf install SDL2'), + StartupInstallHint( + label: 'Fedora', + command: 'sudo dnf install pipewire-libs', + ), ], _LinuxDistro.arch => const [ - StartupInstallHint(label: 'Arch Linux', command: 'sudo pacman -S sdl2'), + StartupInstallHint( + label: 'Arch Linux', + command: 'sudo pacman -S pipewire', + ), ], _LinuxDistro.other => const [], }; return StartupDependencyIssue( - id: 'linux-sdl2-runtime', - title: 'SDL2 runtime is missing', + id: 'linux-pipewire-runtime', + title: 'PipeWire runtime is missing', summary: - 'Chanora uses SDL2 for Linux audio playback. Without it, voice output will not work.', + 'Chanora uses PipeWire as the primary Linux voice backend. Without it, Chanora will try the PulseAudio fallback.', details: const [ - 'Install the SDL2 runtime package for your distribution.', + 'Install the PipeWire runtime package for your distribution.', 'After installing it, restart Chanora and tap Recheck.', ], - severity: StartupDependencySeverity.required, + 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, ); } diff --git a/apps/chanora_flutter/lib/services/ui_preferences_service.dart b/apps/chanora_flutter/lib/services/ui_preferences_service.dart index b940b74..5834d96 100644 --- a/apps/chanora_flutter/lib/services/ui_preferences_service.dart +++ b/apps/chanora_flutter/lib/services/ui_preferences_service.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:shared_preferences/shared_preferences.dart'; class UiSettings { @@ -7,10 +9,45 @@ class UiSettings { 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); + } +} + class UiPreferencesService { static const _hostKey = 'ui.host'; static const _nicknameKey = 'ui.nickname'; static const _permissionsExplainedKey = 'perms_explained'; + static const _clientPlaybackPrefsKey = 'audio.client_playback_prefs'; const UiPreferencesService(); @@ -37,4 +74,80 @@ 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 8160f52..1f9776a 100644 --- a/apps/chanora_flutter/lib/src/rust/api.dart +++ b/apps/chanora_flutter/lib/src/rust/api.dart @@ -795,6 +795,9 @@ 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; @@ -821,6 +824,7 @@ class BridgeClient { const BridgeClient({ required this.id, + required this.uid, required this.channel, required this.name, required this.inputMuted, @@ -834,6 +838,7 @@ class BridgeClient { @override int get hashCode => id.hashCode ^ + uid.hashCode ^ channel.hashCode ^ name.hashCode ^ inputMuted.hashCode ^ @@ -849,6 +854,7 @@ class BridgeClient { other is BridgeClient && runtimeType == other.runtimeType && id == other.id && + uid == other.uid && channel == other.channel && name == other.name && inputMuted == other.inputMuted && diff --git a/apps/chanora_flutter/lib/src/rust/frb_generated.dart b/apps/chanora_flutter/lib/src/rust/frb_generated.dart index 894c8fd..755189b 100644 --- a/apps/chanora_flutter/lib/src/rust/frb_generated.dart +++ b/apps/chanora_flutter/lib/src/rust/frb_generated.dart @@ -1751,18 +1751,19 @@ 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 != 9) - throw Exception('unexpected arr length: expect 9 but see ${arr.length}'); + if (arr.length != 10) + throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); return BridgeClient( id: dco_decode_u_64(arr[0]), - 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]), + 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]), ); } @@ -2352,6 +2353,7 @@ 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); @@ -2362,6 +2364,7 @@ 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, diff --git a/apps/chanora_flutter/lib/widgets/snapshot_view.dart b/apps/chanora_flutter/lib/widgets/snapshot_view.dart index 60f443f..3e11276 100644 --- a/apps/chanora_flutter/lib/widgets/snapshot_view.dart +++ b/apps/chanora_flutter/lib/widgets/snapshot_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,10 +8,17 @@ 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. @@ -25,6 +34,8 @@ class SnapshotView extends StatefulWidget { required this.canJoinVoiceChannel, required this.onJoinChannel, required this.onJoinChannelWithPassword, + this.clientPlaybackPreferences = const {}, + this.onClientPlaybackPreferenceChanged, this.onTs3ServerLink, }); @@ -58,6 +69,12 @@ class SnapshotView extends StatefulWidget { /// Join a password-protected channel. final ValueChanged onJoinChannelWithPassword; + /// Persisted per-client playback preferences keyed by TeamSpeak UID. + final Map clientPlaybackPreferences; + + /// Apply an updated per-client playback preference. + final ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged; + /// Handle TeamSpeak server links embedded in server-provided text. final Ts3ServerLinkHandler? onTs3ServerLink; @@ -293,21 +310,41 @@ class _SnapshotViewState extends State { ], ) : null; + final canAdjustPlayback = _canAdjustClientPlayback(client); return Padding( padding: EdgeInsets.only( left: channelIndent + _userRowStartIndent, right: 8, ), - 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: GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: canAdjustPlayback + ? () => + unawaited(_showClientPlaybackSheet(client, withHaptic: true)) + : null, + onSecondaryTap: canAdjustPlayback + ? () => + unawaited(_showClientPlaybackSheet(client, withHaptic: true)) + : 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), + trailing: canAdjustPlayback + ? IconButton( + icon: const Icon(Icons.more_horiz), + tooltip: 'Playback options', + onPressed: () => + unawaited(_showClientPlaybackSheet(client)), + ) + : null, + ), ), ), ); @@ -352,6 +389,100 @@ 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 _showClientPlaybackSheet( + rust.BridgeClient client, { + 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; + + var preference = _clientPlaybackPreference(client); + await showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + void updatePreference(ClientPlaybackPreference next) { + setModalState(() => preference = next); + unawaited(onChanged(client, next)); + } + + final volumePercent = (preference.volume * 100).round(); + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + client.name, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + 'Per-user playback', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 12), + SwitchListTile.adaptive( + key: const Key('client-playback-mute-tile'), + contentPadding: EdgeInsets.zero, + title: const Text('Mute playback'), + subtitle: Text( + preference.muted + ? 'Audio from this user is muted on this server.' + : 'Audio from this user plays at $volumePercent%.', + ), + value: preference.muted, + onChanged: (muted) => + updatePreference(preference.copyWith(muted: muted)), + ), + const SizedBox(height: 8), + Text( + 'Volume $volumePercent%', + style: Theme.of(context).textTheme.labelLarge, + ), + Slider( + key: const Key('client-playback-volume-slider'), + min: 0.0, + max: 4.0, + divisions: 40, + label: '$volumePercent%', + value: preference.volume, + onChanged: (value) => + updatePreference(preference.copyWith(volume: value)), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + ({Widget icon, bool isSpeaking}) _clientVoiceStatusIcon( ThemeData theme, rust.BridgeClient client, diff --git a/apps/chanora_flutter/linux/CMakeLists.txt b/apps/chanora_flutter/linux/CMakeLists.txt index 48b596b..da17239 100644 --- a/apps/chanora_flutter/linux/CMakeLists.txt +++ b/apps/chanora_flutter/linux/CMakeLists.txt @@ -9,6 +9,21 @@ 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) @@ -53,6 +68,7 @@ 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") @@ -60,6 +76,27 @@ 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 @@ -100,6 +137,29 @@ 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 ec1f808..d8529f3 100644 --- a/apps/chanora_flutter/linux/runner/my_application.cc +++ b/apps/chanora_flutter/linux/runner/my_application.cc @@ -14,6 +14,27 @@ 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))); @@ -110,6 +131,7 @@ 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/snapshot_state_mapper_test.dart b/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart index af67869..1ff3901 100644 --- a/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart +++ b/apps/chanora_flutter/test/services/snapshot_state_mapper_test.dart @@ -18,6 +18,7 @@ void main() { rust.BridgeClient client({ BigInt? id, BigInt? channelId, + String uid = '', int talkPower = 0, bool talkPowerGranted = false, bool inputMuted = false, @@ -25,6 +26,7 @@ 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 index 8b4480a..eba8065 100644 --- a/apps/chanora_flutter/test/services/startup_dependency_check_test.dart +++ b/apps/chanora_flutter/test/services/startup_dependency_check_test.dart @@ -18,7 +18,7 @@ void main() { expect(result.issues, isEmpty); }); - test('missing SDL2 is reported as required on Debian-like systems', () async { + test('missing Linux audio runtimes are reported as recommended', () async { debugResetStartupDependencyCheck( platformIsLinux: () => true, libraryProbe: (candidate) => false, @@ -31,11 +31,19 @@ void main() { final result = await checkStartupDependencies(); expect(result.hasIssues, isTrue); - final sdl = result.issues.firstWhere( - (issue) => issue.id == 'linux-sdl2-runtime', + final pipewire = result.issues.firstWhere( + (issue) => issue.id == 'linux-pipewire-runtime', ); - expect(sdl.isRequired, isTrue); - expect(sdl.installHints.single.command, 'sudo apt install libsdl2-2.0-0'); + 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 { @@ -51,18 +59,22 @@ void main() { final result = await checkStartupDependencies(); expect(result.platformLabel, 'Fedora'); - final sdl = result.issues.singleWhere( - (issue) => issue.id == 'linux-sdl2-runtime', + final pipewire = result.issues.singleWhere( + (issue) => issue.id == 'linux-pipewire-runtime', + ); + expect( + pipewire.installHints.single.command, + 'sudo dnf install pipewire-libs', ); - expect(sdl.installHints.single.command, 'sudo dnf install SDL2'); }); test( - 'missing ONNX runtime is reported as recommended when SDL2 is present', + 'missing ONNX runtime is reported as recommended when Linux audio runtimes are present', () async { debugResetStartupDependencyCheck( platformIsLinux: () => true, - libraryProbe: (candidate) => candidate.contains('SDL2'), + libraryProbe: (candidate) => + candidate.contains('pipewire') || candidate.contains('pulse'), fileExists: (_) async => false, osReleaseProvider: () async => 'ID=fedora\n', resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter', @@ -106,7 +118,8 @@ void main() { test('bundle-local ONNX runtime clears the recommendation', () async { debugResetStartupDependencyCheck( platformIsLinux: () => true, - libraryProbe: (candidate) => candidate.contains('SDL2'), + libraryProbe: (candidate) => + candidate.contains('pipewire') || candidate.contains('pulse'), fileExists: (path) async => path == '/opt/chanora/lib/libonnxruntime.so', resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter', currentDirectoryProvider: () => '/tmp', @@ -130,11 +143,11 @@ void main() { platformLabel: 'Fedora', issues: [ StartupDependencyIssue( - id: 'linux-sdl2-runtime', - title: 'SDL2 runtime is missing', - summary: 'Audio playback needs SDL2.', - details: ['Install SDL2 and recheck.'], - severity: StartupDependencySeverity.required, + id: 'linux-pipewire-runtime', + title: 'PipeWire runtime is missing', + summary: 'Primary Linux audio needs PipeWire.', + details: ['Install PipeWire and recheck.'], + severity: StartupDependencySeverity.recommended, ), ], ); @@ -146,7 +159,9 @@ void main() { expect(text, contains('platform="Fedora"')); expect( text, - contains('issues=[linux-sdl2-runtime:required:SDL2 runtime is missing]'), + 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 6671dcf..78302a4 100644 --- a/apps/chanora_flutter/test/services/ui_preferences_service_test.dart +++ b/apps/chanora_flutter/test/services/ui_preferences_service_test.dart @@ -39,4 +39,79 @@ 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, + ); + + final examplePrefs = await service.loadClientPlaybackPreferencesForServer( + 'example.com', + ); + final otherPrefs = await service.loadClientPlaybackPreferencesForServer( + 'other.example', + ); + + 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); + }); + + 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, + ); + + final prefs = await service.loadClientPlaybackPreferencesForServer( + 'example.com', + ); + expect(prefs, isEmpty); + }, + ); + + 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); + }); } diff --git a/apps/chanora_flutter/test/widgets/chat_views_test.dart b/apps/chanora_flutter/test/widgets/chat_views_test.dart index ed4f141..f9692f4 100644 --- a/apps/chanora_flutter/test/widgets/chat_views_test.dart +++ b/apps/chanora_flutter/test/widgets/chat_views_test.dart @@ -36,10 +36,12 @@ 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, @@ -496,7 +498,15 @@ void main() { final privateTop = tester.getTopLeft(find.text('Alpha').first).dy; final serverTileSize = tester.getSize( find - .ancestor(of: find.text('Server').first, matching: find.byType(Ink)) + .ancestor( + of: find.byIcon(Icons.dns_outlined), + matching: find.byWidgetPredicate( + (widget) => + widget is SizedBox && + widget.width == 92 && + widget.height == 92, + ), + ) .first, ); @@ -506,6 +516,95 @@ void main() { expect(serverTileSize.height, 92); }); + testWidgets('chat sidebar uses MD3-style selected container colors', ( + 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'; + + await tester.pumpWidget( + MaterialApp( + home: ChatPage( + messages: [ + ChatEntry( + senderId: BigInt.from(2), + senderName: longName, + message: 'Private', + target: rust.BridgeMessageTarget.client(BigInt.from(2)), + ), + ], + snapshot: snapshot( + channels: const [], + clients: [ + client( + id: BigInt.from(2), + name: longName, + 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); + + expect(tester.getSize(selectedIndicator), const Size(76, 76)); + expect(privateLabel.maxLines, 2); + expect(privateLabel.overflow, TextOverflow.ellipsis); + expect(privateLabel.textAlign, TextAlign.center); + }); + testWidgets( 'plus opens user picker and close removes selected private chat', (tester) async { diff --git a/apps/chanora_flutter/test/widgets/snapshot_view_test.dart b/apps/chanora_flutter/test/widgets/snapshot_view_test.dart index 60da270..dc71d12 100644 --- a/apps/chanora_flutter/test/widgets/snapshot_view_test.dart +++ b/apps/chanora_flutter/test/widgets/snapshot_view_test.dart @@ -1,7 +1,9 @@ +import 'package:flutter/gestures.dart'; 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/services/ui_preferences_service.dart'; import 'package:chanora_flutter/src/rust/api.dart' as rust; import 'package:chanora_flutter/widgets/snapshot_view.dart'; @@ -28,12 +30,14 @@ 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, @@ -51,6 +55,8 @@ void main() { BigInt? ownClientId, BigInt? currentVoiceChannelId, rust.BridgeAudioStats? audioStats, + Map clientPlaybackPreferences = const {}, + ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged, }) { return MaterialApp( localizationsDelegates: AppL10n.localizationsDelegates, @@ -75,6 +81,8 @@ void main() { canJoinVoiceChannel: true, onJoinChannel: (_) {}, onJoinChannelWithPassword: (_) {}, + clientPlaybackPreferences: clientPlaybackPreferences, + onClientPlaybackPreferenceChanged: onClientPlaybackPreferenceChanged, ), ), ); @@ -262,6 +270,122 @@ void main() { }, ); + testWidgets('remote client playback options button opens playback sheet', ( + 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(); + await tester.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + + expect(find.text('Per-user playback'), findsOneWidget); + expect(find.text('Mute playback'), findsOneWidget); + expect(find.text('Volume 50%'), findsOneWidget); + expect( + find.byKey(const Key('client-playback-volume-slider')), + findsOneWidget, + ); + }); + + testWidgets('remote client long press opens playback sheet', (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('Per-user playback'), findsOneWidget); + expect(find.text('Mute playback'), findsOneWidget); + }); + + testWidgets('remote client secondary click opens playback sheet', ( + 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('Per-user playback'), findsOneWidget); + expect(find.text('Mute playback'), findsOneWidget); + }); + + testWidgets('client playback sheet 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.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Mute playback')); + await tester.pumpAndSettle(); + + expect(changes, isNotEmpty); + expect(changes.last.muted, isTrue); + + await tester.drag( + find.byKey(const Key('client-playback-volume-slider')), + const Offset(160, 0), + ); + await tester.pumpAndSettle(); + + expect(changes.last.volume, greaterThan(1.0)); + }); + 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 index dda960d..7e88b60 100644 --- a/apps/chanora_flutter/test/widgets/startup_dependency_screen_test.dart +++ b/apps/chanora_flutter/test/widgets/startup_dependency_screen_test.dart @@ -17,11 +17,11 @@ void main() { platformLabel: 'Fedora', issues: const [ StartupDependencyIssue( - id: 'linux-sdl2-runtime', - title: 'SDL2 runtime is missing', - summary: 'Audio playback needs SDL2.', - details: ['Install SDL2 and recheck.'], - severity: StartupDependencySeverity.required, + 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', @@ -29,7 +29,7 @@ void main() { ), StartupInstallHint( label: 'Fedora', - command: 'sudo dnf install SDL2', + command: 'sudo dnf install pipewire-libs', ), ], ), @@ -52,14 +52,14 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Finish Linux setup'), findsOneWidget); - expect(find.text('SDL2 runtime is missing'), findsOneWidget); - expect(find.text('Continue with limited mode'), 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 with limited mode')); - await tester.tap(find.text('Continue with limited mode')); + 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/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index ff3f73d..54f1530 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -2411,6 +2411,7 @@ 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/crates/chanora_audio/Cargo.toml b/crates/chanora_audio/Cargo.toml index 9bd2e93..01158c9 100644 --- a/crates/chanora_audio/Cargo.toml +++ b/crates/chanora_audio/Cargo.toml @@ -9,6 +9,10 @@ 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 @@ -29,10 +33,13 @@ 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 capture. -# Linux playback uses SDL2; Apple platforms use direct VoiceProcessingIO +# 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 # AudioUnits via `coreaudio-rs` for the voice path. cpal = "0.17.3" @@ -101,34 +108,33 @@ 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 @@ -148,22 +154,8 @@ 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" -# 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 } +# 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" diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index 376ee23..6a8aabd 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -5,28 +5,35 @@ //! 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") @@ -103,6 +110,7 @@ pub struct AudioDeviceInfo { } #[cfg(all( + not(target_os = "linux"), not(target_os = "ios"), not(target_os = "macos"), not(target_os = "android") @@ -112,6 +120,7 @@ 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") @@ -123,6 +132,7 @@ 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") @@ -178,29 +188,53 @@ pub fn list_audio_devices() -> AudioDeviceList { output_devices: Vec::new(), }; let host = cpal::default_host(); - 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); + #[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); + } } } - } - 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); + 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); + } } } } @@ -306,31 +340,23 @@ 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, - #[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") - ))] + // 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")] _input_stream: Mutex>, #[cfg(target_os = "linux")] - _output_stream: Mutex>, + _output_stream: Mutex>, #[cfg(all( not(target_os = "linux"), not(target_os = "ios"), @@ -457,6 +483,31 @@ 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( @@ -692,7 +743,7 @@ impl AudioEngine { Self::start_with_gate_cpal(cfg, voice_out_tx, voice_in_rx, transmit_gate) } - /// Non-Apple/non-Android implementation: cpal capture + (cpal | SDL2) output. + /// Non-Apple/non-Android implementation: native Linux voice I/O or cpal on Windows. /// 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 @@ -711,7 +762,16 @@ 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, @@ -735,29 +795,31 @@ 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)?; - - 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" ); + #[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)?; + // Log the cpal-reported default configs *before* trying to // open streams, so a downstream stream-build failure can // be cross-referenced against what the platform reported @@ -768,6 +830,29 @@ 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", @@ -782,6 +867,24 @@ 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", @@ -797,6 +900,7 @@ 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)); @@ -804,6 +908,7 @@ 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 @@ -811,24 +916,52 @@ 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, - ); - let (input_stream, capture_active) = match capture_result { - Ok(s) => (Some(s), true), + ) + .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), Err(e) => { warn!( target: "chanora_audio", error = %e, "capture stream unavailable; continuing with playback only" ); - (None, false) + (None, false, "".to_string()) } }; + #[cfg(target_os = "windows")] if let Some(s) = &input_stream { s.play() .map_err(|e| AudioError::Backend(format!("input play: {e}")))?; @@ -838,21 +971,18 @@ impl AudioEngine { let audio_handler: Arc>> = Arc::new(Mutex::new(AudioHandler::new())); - // 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. + // Linux uses native PipeWire/PulseAudio playback. PipeWire is primary; + // PulseAudio is fallback and can also be selected explicitly by id. #[cfg(target_os = "linux")] - let output_stream = crate::sdl_output::SdlOutput::start( + 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(), 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 = { @@ -927,10 +1057,20 @@ 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 { @@ -947,6 +1087,11 @@ 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); } } @@ -966,6 +1111,10 @@ 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), @@ -991,6 +1140,7 @@ 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())); @@ -1137,6 +1287,7 @@ 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 { @@ -1153,6 +1304,11 @@ 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); } } @@ -1172,6 +1328,7 @@ 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, @@ -1224,6 +1381,7 @@ 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())); @@ -1258,6 +1416,7 @@ 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 { @@ -1274,6 +1433,11 @@ 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); } } @@ -1293,6 +1457,7 @@ 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, @@ -1312,12 +1477,8 @@ 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 cpal input + cpal/SDL output. - #[cfg(all( - not(target_os = "ios"), - not(target_os = "macos"), - not(target_os = "android") - ))] + // platform has separate input + output stream owners. + #[cfg(any(target_os = "linux", target_os = "windows"))] { let _ = self._input_stream.lock().unwrap().take(); let _ = self._output_stream.lock().unwrap().take(); @@ -1669,6 +1830,25 @@ 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" + ); + } + } } } @@ -1680,7 +1860,7 @@ impl Drop for AudioEngine { // ---------- Capture pipeline ---------- -#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] +#[cfg(target_os = "windows")] fn try_open_capture( in_dev: &cpal::Device, voice_out_tx: mpsc::Sender, @@ -1699,9 +1879,8 @@ 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: 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. + // * Linux does not reach this cpal capture path in production; + // it uses native PipeWire/PulseAudio capture instead. let mut in_stream_cfg: cpal::StreamConfig = in_cfg.into(); #[cfg(target_os = "windows")] { @@ -1738,7 +1917,7 @@ fn try_open_capture( } #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] -struct CaptureState { +pub(crate) struct CaptureState { encoder: OpusEncoder, in_sample_rate: u32, in_channels: usize, @@ -1777,7 +1956,7 @@ struct CaptureState { #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl CaptureState { - fn new( + pub(crate) fn new( encoder: OpusEncoder, in_sample_rate: u32, in_channels: usize, @@ -1810,7 +1989,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). - fn ingest(&mut self, buf: &[T]) { + pub(crate) 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(); @@ -1959,7 +2138,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")))] -trait ToF32 { +pub(crate) trait ToF32 { fn to_f32_sample(self) -> f32; } #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] @@ -1981,7 +2160,7 @@ impl ToF32 for u16 { } } -#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] +#[cfg(target_os = "windows")] 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 b36769b..5212566 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 SDL2 for output (see `sdl_output.rs`) and cpal for capture; +//! Linux uses native PipeWire/PulseAudio voice I/O; //! 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 -//! cpal + SDL use) and never move it. The outer `AudioEngine` +//! the desktop backends 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 cpal - /// and SDL backends accept so engine.rs can swap backends with + /// Parameters mirror the capture + playback inputs the desktop + /// 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 cpal - /// and SDL output paths read on every callback so the master + /// * `output_gain` / `output_muted` — same atomics the desktop + /// 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 cpal + SDL output + // buffer + mix. Same primitive desktop 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,12 +748,10 @@ 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 - // SdlOutput::callback and the cpal output stream). + // Linux PipeWire/PulseAudio and the cpal output stream). // // Build the playback pipeline (direct fill_buffer in - // render callback; matches tsclientlib's reference SDL - // example at - // tsclientlib/examples/audio_utils/ts_to_audio.rs). + // render callback; matches the desktop voice backends). // // The earlier ring-buffer attempt (rc.8+73..+74) decoupled // AudioHandler from the render callback via a 50 Hz @@ -766,14 +764,12 @@ 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. Linux/SDL's same pattern works fine because - // SDL calls fill_buffer at exactly the device callback - // rate. + // arrived. Direct desktop callbacks follow the same pattern. // // 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 Linux/SDL, just stereo-f32 -> mono-i16 + // buffer. Same as desktop output, 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 f49c4e4..eed388e 100644 --- a/crates/chanora_audio/src/lib.rs +++ b/crates/chanora_audio/src/lib.rs @@ -47,7 +47,7 @@ pub mod voice_activity; pub(crate) mod voice_render; #[cfg(target_os = "linux")] -mod sdl_output; +mod linux_pipewire_input; #[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 new file mode 100644 index 0000000..bafa478 --- /dev/null +++ b/crates/chanora_audio/src/linux_pipewire_input.rs @@ -0,0 +1,961 @@ +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}") +} + +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}") +} + +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}") +} + +fn pulse_input_name_from_id(id: &str) -> Option<&str> { + id.strip_prefix(PULSE_INPUT_ID_PREFIX) +} + +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 { + let mut devices = Vec::new(); + match list_pipewire_nodes(direction) { + Ok(mut pipewire) => devices.append(&mut pipewire), + Err(error) => { + warn!(target: "chanora_audio", error = %error, "PipeWire device enumeration failed") + } + } + match list_pulse_devices(direction) { + Ok(mut pulse) => devices.append(&mut pulse), + Err(error) => { + warn!(target: "chanora_audio", error = %error, "PulseAudio device enumeration failed") + } + } + devices +} + +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/sdl_output.rs b/crates/chanora_audio/src/sdl_output.rs deleted file mode 100644 index 13fa98e..0000000 --- a/crates/chanora_audio/src/sdl_output.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! 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 7fe550b..31f01db 100644 --- a/crates/chanora_audio/src/vad/silero_onnx.rs +++ b/crates/chanora_audio/src/vad/silero_onnx.rs @@ -33,12 +33,16 @@ //! ## Fallback //! //! `try_new` returns `None` when the model file is missing, the ONNX -//! Runtime is unavailable, or the platform is not iOS/macOS. The caller +//! Runtime is unavailable, or the platform cannot load ONNX Runtime. 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). @@ -88,7 +92,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 does not support ONNX. + /// is unavailable, or the platform cannot initialize ONNX Runtime. pub fn try_new(model_path: &str) -> Option { Self::try_new_onnx(model_path) } @@ -105,6 +109,17 @@ 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)) }); @@ -301,6 +316,111 @@ 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. @@ -401,6 +521,8 @@ impl Drop for SileroOnnxVadWorker { #[cfg(test)] mod tests { use super::*; + #[cfg(target_os = "linux")] + use std::path::PathBuf; fn make_stub_vad() -> SileroOnnxVad { SileroOnnxVad { @@ -496,4 +618,17 @@ 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 611c6ab..3d38e1e 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -355,6 +355,8 @@ 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. @@ -418,6 +420,7 @@ 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, diff --git a/crates/chanora_bridge/src/frb_generated.rs b/crates/chanora_bridge/src/frb_generated.rs index 62c3299..1ac3001 100644 --- a/crates/chanora_bridge/src/frb_generated.rs +++ b/crates/chanora_bridge/src/frb_generated.rs @@ -1940,6 +1940,7 @@ 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); @@ -1950,6 +1951,7 @@ 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, @@ -2796,6 +2798,7 @@ 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(), @@ -3434,6 +3437,7 @@ 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); diff --git a/crates/chanora_protocol/Cargo.toml b/crates/chanora_protocol/Cargo.toml index c599943..98384e5 100644 --- a/crates/chanora_protocol/Cargo.toml +++ b/crates/chanora_protocol/Cargo.toml @@ -13,6 +13,7 @@ publish.workspace = true thiserror.workspace = true tracing.workspace = true serde.workspace = true +base64 = "0.22" chanora_resolver = { path = "../chanora_resolver" } # tsclientlib is git-only and not on crates.io. The "audio" feature @@ -31,12 +32,14 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync" futures = "0.3" async-trait = "0.1" -[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). +[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. reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] } -[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. +[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. 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 a7b4cb9..97408c0 100644 --- a/crates/chanora_protocol/src/adapter.rs +++ b/crates/chanora_protocol/src/adapter.rs @@ -20,6 +20,7 @@ use std::net::SocketAddr; use std::time::{Duration, Instant}; +use base64::prelude::*; use chanora_resolver::ChanoraResolver; use futures::prelude::*; use std::collections::HashMap; @@ -975,7 +976,9 @@ fn send_text_message( ) -> Result { use tsproto_types::TextMessageTargetMode; match target { - MessageTarget::Server => send_text_to_mode(con, message, TextMessageTargetMode::Server, "server"), + MessageTarget::Server => { + send_text_to_mode(con, message, TextMessageTargetMode::Server, "server") + } MessageTarget::Channel => { // Fix: previously channel messages were sent via // state.server.send_textmessage() which always uses @@ -1216,6 +1219,11 @@ 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, diff --git a/crates/chanora_protocol/src/dto.rs b/crates/chanora_protocol/src/dto.rs index 3283215..59d6d67 100644 --- a/crates/chanora_protocol/src/dto.rs +++ b/crates/chanora_protocol/src/dto.rs @@ -68,6 +68,8 @@ pub struct ServerActivity { 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. diff --git a/docs/release/linux-build.md b/docs/release/linux-build.md new file mode 100644 index 0000000..bdc3094 --- /dev/null +++ b/docs/release/linux-build.md @@ -0,0 +1,91 @@ +# 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. + +## Optional: bundle ONNX Runtime + +If you want the Linux bundle to include ONNX Runtime for Silero VAD, 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 +``` + +If `CHANORA_ONNXRUNTIME_SHARED_LIB` is set, the Debian helper now verifies that +`libonnxruntime.so` was actually bundled before packaging. diff --git a/docs/ts3_development_manual_en_v6.md b/docs/ts3_development_manual_en_v6.md new file mode 100644 index 0000000..3226008 --- /dev/null +++ b/docs/ts3_development_manual_en_v6.md @@ -0,0 +1,4616 @@ +# 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/poc/README.md b/poc/README.md index 5b73947..51b259b 100644 --- a/poc/README.md +++ b/poc/README.md @@ -20,6 +20,12 @@ Authority: [`docs/architecture/proof-of-concept-plan.md`](../docs/architecture/p | `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 diff --git a/poc/linux_voice_test/Cargo.lock b/poc/linux_voice_test/Cargo.lock new file mode 100644 index 0000000..43b20b7 --- /dev/null +++ b/poc/linux_voice_test/Cargo.lock @@ -0,0 +1,1293 @@ +# 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 new file mode 100644 index 0000000..7e1f2c4 --- /dev/null +++ b/poc/linux_voice_test/Cargo.toml @@ -0,0 +1,17 @@ +[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 new file mode 100644 index 0000000..1f6491f --- /dev/null +++ b/poc/linux_voice_test/README.md @@ -0,0 +1,37 @@ +# 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 new file mode 100644 index 0000000..0e80bd6 --- /dev/null +++ b/poc/linux_voice_test/VERIFICATION.md @@ -0,0 +1,59 @@ +# 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 new file mode 100644 index 0000000..e071ca2 --- /dev/null +++ b/poc/linux_voice_test/src/audio_buffer.rs @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..3521656 --- /dev/null +++ b/poc/linux_voice_test/src/linux_pipewire.rs @@ -0,0 +1,318 @@ +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 new file mode 100644 index 0000000..1b3bf54 --- /dev/null +++ b/poc/linux_voice_test/src/linux_pulseaudio.rs @@ -0,0 +1,309 @@ +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 new file mode 100644 index 0000000..5e0fdaa --- /dev/null +++ b/poc/linux_voice_test/src/linux_shared.rs @@ -0,0 +1,384 @@ +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 new file mode 100644 index 0000000..1b7eaeb --- /dev/null +++ b/poc/linux_voice_test/src/main.rs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..769dcb3 --- /dev/null +++ b/poc/linux_voice_test/src/windows_cpal.rs @@ -0,0 +1,168 @@ +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/ts3-manual-research/README.md b/poc/ts3-manual-research/README.md new file mode 100644 index 0000000..88c8156 --- /dev/null +++ b/poc/ts3-manual-research/README.md @@ -0,0 +1,35 @@ +# 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 new file mode 100644 index 0000000..8bd9bd1 --- /dev/null +++ b/poc/ts3-manual-research/VERIFICATION.md @@ -0,0 +1,60 @@ +# 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 new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/poc/tsclientlib-channel-query-spike/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/poc/tsclientlib-channel-query-spike/Cargo.lock b/poc/tsclientlib-channel-query-spike/Cargo.lock new file mode 100644 index 0000000..2ea8f72 --- /dev/null +++ b/poc/tsclientlib-channel-query-spike/Cargo.lock @@ -0,0 +1,3453 @@ +# 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 new file mode 100644 index 0000000..6d8eb4b --- /dev/null +++ b/poc/tsclientlib-channel-query-spike/Cargo.toml @@ -0,0 +1,19 @@ +[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 new file mode 100644 index 0000000..3fb6eef --- /dev/null +++ b/poc/tsclientlib-channel-query-spike/README.md @@ -0,0 +1,45 @@ +# 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 new file mode 100644 index 0000000..0ccab35 --- /dev/null +++ b/poc/tsclientlib-channel-query-spike/VERIFICATION.md @@ -0,0 +1,60 @@ +# 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 new file mode 100644 index 0000000..995f878 --- /dev/null +++ b/poc/tsclientlib-channel-query-spike/src/main.rs @@ -0,0 +1,413 @@ +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-filetransfer-spike/.gitignore b/poc/tsclientlib-filetransfer-spike/.gitignore new file mode 100644 index 0000000..6f02f34 --- /dev/null +++ b/poc/tsclientlib-filetransfer-spike/.gitignore @@ -0,0 +1,2 @@ +/downloads/ +/target/ diff --git a/poc/tsclientlib-filetransfer-spike/Cargo.lock b/poc/tsclientlib-filetransfer-spike/Cargo.lock new file mode 100644 index 0000000..b1fabf6 --- /dev/null +++ b/poc/tsclientlib-filetransfer-spike/Cargo.lock @@ -0,0 +1,3453 @@ +# 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 new file mode 100644 index 0000000..ed1cef0 --- /dev/null +++ b/poc/tsclientlib-filetransfer-spike/Cargo.toml @@ -0,0 +1,19 @@ +[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 new file mode 100644 index 0000000..e013f8e --- /dev/null +++ b/poc/tsclientlib-filetransfer-spike/README.md @@ -0,0 +1,41 @@ +# 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 new file mode 100644 index 0000000..de7cd3c --- /dev/null +++ b/poc/tsclientlib-filetransfer-spike/VERIFICATION.md @@ -0,0 +1,45 @@ +# 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 new file mode 100644 index 0000000..cff2769 --- /dev/null +++ b/poc/tsclientlib-filetransfer-spike/src/main.rs @@ -0,0 +1,297 @@ +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 new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/poc/tsclientlib-query-spike/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/poc/tsclientlib-query-spike/Cargo.lock b/poc/tsclientlib-query-spike/Cargo.lock new file mode 100644 index 0000000..fdc5fae --- /dev/null +++ b/poc/tsclientlib-query-spike/Cargo.lock @@ -0,0 +1,3454 @@ +# 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 new file mode 100644 index 0000000..21a0d46 --- /dev/null +++ b/poc/tsclientlib-query-spike/Cargo.toml @@ -0,0 +1,20 @@ +[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 new file mode 100644 index 0000000..15a4971 --- /dev/null +++ b/poc/tsclientlib-query-spike/README.md @@ -0,0 +1,48 @@ +# 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 new file mode 100644 index 0000000..75d7578 --- /dev/null +++ b/poc/tsclientlib-query-spike/VERIFICATION.md @@ -0,0 +1,82 @@ +# 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 new file mode 100644 index 0000000..a91a5d2 --- /dev/null +++ b/poc/tsclientlib-query-spike/src/main.rs @@ -0,0 +1,776 @@ +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 new file mode 100644 index 0000000..3a68598 --- /dev/null +++ b/poc/tsidentity-import-spike/.gitignore @@ -0,0 +1,2 @@ +/sample_identity.ini +/target/ diff --git a/poc/tsidentity-import-spike/Cargo.lock b/poc/tsidentity-import-spike/Cargo.lock new file mode 100644 index 0000000..bc9b327 --- /dev/null +++ b/poc/tsidentity-import-spike/Cargo.lock @@ -0,0 +1,3452 @@ +# 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 new file mode 100644 index 0000000..14e1813 --- /dev/null +++ b/poc/tsidentity-import-spike/Cargo.toml @@ -0,0 +1,18 @@ +[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 new file mode 100644 index 0000000..7e9dcb1 --- /dev/null +++ b/poc/tsidentity-import-spike/README.md @@ -0,0 +1,35 @@ +# 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 new file mode 100644 index 0000000..fa77fed --- /dev/null +++ b/poc/tsidentity-import-spike/VERIFICATION.md @@ -0,0 +1,59 @@ +# 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 new file mode 100644 index 0000000..d316ba7 --- /dev/null +++ b/poc/tsidentity-import-spike/src/main.rs @@ -0,0 +1,249 @@ +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-linux-deb.sh b/tools/build-linux-deb.sh index 3418991..17d0e18 100755 --- a/tools/build-linux-deb.sh +++ b/tools/build-linux-deb.sh @@ -12,6 +12,7 @@ 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" +ONNXRUNTIME_SHARED_LIB="${CHANORA_ONNXRUNTIME_SHARED_LIB:-}" cleanup() { rm -rf "$STAGE_DIR" @@ -67,6 +68,9 @@ main() { require_cmd dpkg-deb require_file "$PUBSPEC" "Flutter pubspec" require_file "$ICON_SRC" "launcher icon" + if [[ -n "$ONNXRUNTIME_SHARED_LIB" ]]; then + require_file "$ONNXRUNTIME_SHARED_LIB" "Linux ONNX Runtime shared library" + fi if [[ ! -f "$OBOE_DIR/Cargo.toml" ]]; then cat >&2 <}" bar # ---------- 1. Verify toolchain ---------- @@ -66,9 +70,54 @@ if ! pkg-config --exists gtk+-3.0 2>/dev/null; then exit 1 fi -# Verify libopus dev headers (needed by audiopus_sys to link) +# Verify libopus development files. tsclientlib's audio feature pulls in +# audiopus, which links libopus for Linux builds/tests. if ! pkg-config --exists opus 2>/dev/null; then - echo " ERROR: opus dev headers not found. Install libopus-dev (Debian/Ubuntu) or opus-devel (Fedora)." >&2 + 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 + 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)" + +# Verify the optional ONNX Runtime shared library when explicitly requested. +if [[ -n "$ONNXRUNTIME_SHARED_LIB" && ! -f "$ONNXRUNTIME_SHARED_LIB" ]]; then + echo " ERROR: CHANORA_ONNXRUNTIME_SHARED_LIB does not exist:" >&2 + echo " $ONNXRUNTIME_SHARED_LIB" >&2 exit 1 fi @@ -124,6 +173,7 @@ fi # ---------- 5. flutter build linux --release ---------- echo "[5/6] flutter build linux --release" cd "$FLUTTER_APP" +export CHANORA_BRIDGE_SHARED_LIB="$BRIDGE_SO" flutter pub get flutter build linux --release @@ -134,12 +184,25 @@ 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" -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 [[ -n "$ONNXRUNTIME_SHARED_LIB" ]]; then + 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 +else + echo " ORT .so : not bundled (set CHANORA_ONNXRUNTIME_SHARED_LIB to include it)" +fi # ---------- 6. Package the bundle ---------- echo "[6/6] Package release tarball" @@ -162,4 +225,4 @@ echo " target/$TARGET/release/libchanora_bridge.so" echo " Bundle dir:" echo " $BUNDLE_DIR/" echo " Tarball:" -echo " $TAR_PATH" \ No newline at end of file +echo " $TAR_PATH"