diff --git a/.gitignore b/.gitignore index 6e18fb5..095c70f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ local.properties # OS & temporary files .DS_Store Thumbs.db + +# Rust / Cargo +hermes-pair/target/ +**/target/ diff --git a/README.md b/README.md index fe4a8f8..bfc4c5a 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,13 @@ hermes serve --host 0.0.0.0 --port 9119 Run the full automated test suite: ```powershell -.\gradlew.bat testDebugUnitTest +.\gradlew.bat test ``` Run Android Lint: ```powershell -.\gradlew.bat lintDebug +.\gradlew.bat lint ``` Assemble Debug APK: @@ -116,3 +116,34 @@ Assemble Debug APK: ```powershell .\gradlew.bat assembleDebug ``` + +--- + +## 📱 Instant QR Onboarding — Hermes Pair (`hermes-pair/`) + +Inside the `hermes-pair/` directory is the cross-platform desktop companion application written in Rust. It runs on Windows and Linux to auto-discover your local IP and generate a secure QR code for instant onboarding with Hermes Android. + +### Windows (GUI or CLI): +```powershell +# GUI window +.\hermes-pair\dist\windows\HermesPair.exe + +# Terminal QR output +.\hermes-pair\dist\windows\HermesPair.exe qr --port 9119 +``` + +### Linux (GUI or Headless Server): +```bash +# GUI window +./hermes-pair/dist/linux/hermes-pair + +# Headless / Terminal QR +./hermes-pair/dist/linux/hermes-pair --terminal --port 9119 +``` + +### Building Hermes Pair from Source: +```bash +cd hermes-pair +cargo test +cargo build --release +``` diff --git a/hermes-pair/Cargo.lock b/hermes-pair/Cargo.lock new file mode 100644 index 0000000..2161de8 --- /dev/null +++ b/hermes-pair/Cargo.lock @@ -0,0 +1,4171 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.20", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[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 = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[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 = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[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 = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +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 = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.59.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +dependencies = [ + "ahash", + "bitflags 2.13.1", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui-wgpu" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 1.0.69", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" +dependencies = [ + "ahash", + "arboard", + "bytemuck", + "egui", + "log", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "epaint" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" + +[[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 = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[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", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.1", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[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 = "hermes-pair" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "clap", + "dirs", + "eframe", + "if-addrs", + "image", + "qrcode", + "rand 0.8.7", + "reqwest", + "serde", + "serde_json", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "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", + "webpki-roots", +] + +[[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 = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[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 = "if-addrs" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[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", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[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 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "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 2.0.119", +] + +[[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 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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 = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "bitflags 2.13.1", + "libc", + "plain", + "redox_syscall 0.9.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.1", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "strum", + "termcolor", + "thiserror 2.0.20", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[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 = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[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 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[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-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[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.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[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 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "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_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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_syscall" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[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 = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[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 = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.20", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +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 2.0.119", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[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 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[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 = "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.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[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 2.13.1", + "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 = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[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", + "serde_derive", +] + +[[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.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[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.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +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 = "webbrowser" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec", + "bitflags 2.13.1", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.13.1", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.13.1", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.13.1", + "js-sys", + "log", + "web-sys", +] + +[[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.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.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 = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.1", + "block2", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +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 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/hermes-pair/Cargo.toml b/hermes-pair/Cargo.toml new file mode 100644 index 0000000..4ba5a1c --- /dev/null +++ b/hermes-pair/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "hermes-pair" +version = "0.1.0" +edition = "2021" +authors = ["ochenstarik-ui"] +description = "Fast, secure QR onboarding helper for Hermes Agent" + +[lib] +name = "hermes_pair" +path = "src/lib.rs" + +[[bin]] +name = "hermes-pair" +path = "src/main.rs" + +[dependencies] +tokio = { version = "1.43", features = ["full"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +qrcode = { version = "0.14", default-features = false } +image = { version = "0.25", default-features = false, features = ["png"] } +rand = "0.8" +uuid = { version = "1.13", features = ["v4", "serde"] } +dirs = "6.0" +clap = { version = "4.5", features = ["derive"] } +eframe = { version = "0.31", default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] } +base64 = "0.22" +if-addrs = "0.13" +chrono = { version = "0.4", features = ["serde"] } +url = { version = "2.5", features = ["serde"] } + +[dev-dependencies] +tokio = { version = "1.43", features = ["full"] } diff --git a/hermes-pair/README.md b/hermes-pair/README.md new file mode 100644 index 0000000..72c5ba1 --- /dev/null +++ b/hermes-pair/README.md @@ -0,0 +1,198 @@ +# Hermes Pair (`hermes-pair`) + +[![Build and Test](https://github.com/ochenstarik-ui/hermes-pair/actions/workflows/build.yml/badge.svg)](https://github.com/ochenstarik-ui/hermes-pair/actions/workflows/build.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +Fast, lightweight, cross-platform pairing helper for **Hermes Agent** and the **Hermes Android App**. + +`hermes-pair` bridges your host computer (running Hermes) with the mobile companion app by generating high-contrast QR codes and standard pairing URIs (`hermes://pair?data=...`) containing host network information, authentication state, and cryptographically secure random nonces. + +--- + +## Features + +- 🖥️ **Native GUI & Terminal UI Modes**: Runs as a lightweight native GUI window (`eframe`/`egui`) on desktop or an interactive/single-shot ANSI terminal interface on headless servers. +- 🔍 **Smart Network Interface Discovery**: Automatically discovers and prioritizes physical LAN interfaces (`192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`) and Tailscale overlays (`100.x.x.x`), while filtering out loopback and link-local adapters. +- ⚡ **Real-Time Hermes Status Probing**: Connects to `http://127.0.0.1:/api/status` and `http://:/api/status` to detect whether Hermes is active, verify version and auth requirements, and warn if Hermes is mistakenly bound only to loopback (`127.0.0.1`). +- 🛡️ **Built-in Security Safeguards**: + - Automatically warns if Hermes is exposed over LAN without authentication. + - Generates single-use, 16-byte cryptographically secure random nonces. + - Enforces configurable TTL expiry (default: 120 seconds). + - Validates payload structure, versioning, and UUID integrity. +- 💾 **Persistent Host Identity**: Manages a persistent UUIDv4 `host_id` saved atomically to `%APPDATA%\HermesPair\config.json` (Windows) or `~/.config/hermes-pair/config.json` (Linux). + +--- + +## Pairing Protocol Specification (`v1`) + +The generated QR code encodes a custom URI: +```text +hermes://pair?data= +``` + +### Decoded JSON Payload (`PairingPayloadV1`) + +```json +{ + "v": 1, + "type": "hermes-pair", + "host_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", + "name": "Gaming-PC", + "host": "192.168.1.34", + "port": 9119, + "scheme": "http", + "expires_at": 1756012800, + "nonce": "k7a_QW9jRz1M..." +} +``` + +| Field | Type | Description | +|---|---|---| +| `v` | `u32` | Protocol schema version (`1`). | +| `type` | `string` | Payload identifier (`hermes-pair`). | +| `host_id` | `string` | Persistent UUIDv4 identifying the host machine. | +| `name` | `string` | Human-readable computer or host display name. | +| `host` | `string` | Reachable IPv4 address or hostname for the mobile client. | +| `port` | `u16` | HTTP port on which Hermes Agent is listening (e.g. `9119`). | +| `scheme` | `string` | Connection scheme (`http` or `https`). | +| `expires_at` | `u64` | Unix epoch timestamp (seconds) after which this payload is rejected. | +| `nonce` | `string` | 16 cryptographically random bytes, Base64URL-encoded. | + +--- + +## Installation & Build + +### Prerequisites +- [Rust](https://rustup.rs/) 1.80+ (tested on Rust 1.98.0) + +### Building from Source + +```bash +# Clone the repository +git clone https://github.com/ochenstarik-ui/hermes-pair.git +cd hermes-pair + +# Run tests +cargo test + +# Build release binary +cargo build --release +``` + +Release binaries are produced at: +- **Windows**: `target/release/hermes-pair.exe` +- **Linux**: `target/release/hermes-pair` + +--- + +## Usage + +### 1. Native GUI Mode (Default) + +Simply execute `hermes-pair` without flags to open the native desktop window: + +```bash +hermes-pair +``` + +#### GUI Controls: +- **Status Badge**: Shows Hermes connectivity (Running, Offline, Loopback Only) and auth status. +- **Network Interface Dropdown**: Switch between Wi-Fi, Ethernet, Tailscale, or virtual adapters. +- **[ 🔄 Regenerate QR ]**: Generates a new payload with a fresh nonce and resets the countdown. +- **[ 📋 Copy Link ]**: Copies `hermes://pair?data=...` directly to the system clipboard. +- **[ 🔄 Check ]**: Retries probing the Hermes status endpoint immediately. + +### 2. Interactive Terminal UI Mode + +For remote SSH sessions or terminals without a display server: + +```bash +hermes-pair --terminal +# or short flag: +hermes-pair -t +``` + +Output: +```text +Hermes: Running (v1.2.0, Auth: Required) +Host: Gaming-PC +Address: http://192.168.1.34:9119 +Host ID: 7b31d044... +Expires in: 01:58 + +██████████████████████████████ +██ ██ ██ ██ +██ ██████ ██ ██ ██████ ██ +██ ██████ ██ ██ ██████ ██ +██ ██ ██ ██ +██████████████████████████████ +... +Pairing Link: hermes://pair?data=eyJ2Ijox... +``` + +### 3. Headless / Single-Shot Script Mode + +To print the QR code once to stdout (useful for automation, provisioning scripts, or terminal output piping): + +```bash +hermes-pair qr +# or +hermes-pair --no-gui +``` + +### 4. Command Line Options + +```text +Usage: hermes-pair [OPTIONS] [COMMAND] + +Commands: + qr Output QR code once to stdout and exit + help Print this message or the help of the given subcommand(s) + +Options: + -t, --terminal Run interactive terminal mode with periodic refresh + --no-gui Print QR once to stdout and exit (headless/script mode) + --port Port of Hermes Agent [default: 9119] + --hermes-url Hermes status API URL (e.g. http://127.0.0.1:9119) + -i, --interface Specific network interface name or IPv4 address to advertise + --ttl Pairing QR validity TTL in seconds [default: 120] + -h, --help Print help + -V, --version Print version +``` + +--- + +## Configuration & Storage + +`hermes-pair` generates a persistent `host_id` on first launch and stores it in JSON format: + +- **Windows**: `%APPDATA%\HermesPair\config.json` +- **Linux / macOS**: `~/.config/hermes-pair/config.json` + +Example configuration: +```json +{ + "host_id": "7b31d044-cf64-4e78-9df2-bb58a8f5e1a1", + "display_name": "Studio-Workstation" +} +``` + +--- + +## Troubleshooting & Warnings + +### Warning: Loopback Only +If Hermes is started with default `127.0.0.1` binding, other devices on the LAN cannot connect. +Start Hermes binding to all interfaces: +```bash +hermes serve --host 0.0.0.0 --port 9119 +``` + +### Warning: Unauthenticated Network Access +If Hermes has no authentication configured, any device on your local network could access the API. Consider enabling token or password authentication for production usage. + +--- + +## License + +Licensed under the [MIT License](LICENSE). diff --git a/hermes-pair/dist/linux/hermes-pair b/hermes-pair/dist/linux/hermes-pair new file mode 100644 index 0000000..6b39f13 Binary files /dev/null and b/hermes-pair/dist/linux/hermes-pair differ diff --git a/hermes-pair/dist/windows/HermesPair.exe b/hermes-pair/dist/windows/HermesPair.exe new file mode 100644 index 0000000..0fc34b4 Binary files /dev/null and b/hermes-pair/dist/windows/HermesPair.exe differ diff --git a/hermes-pair/src/app.rs b/hermes-pair/src/app.rs new file mode 100644 index 0000000..e0c031f --- /dev/null +++ b/hermes-pair/src/app.rs @@ -0,0 +1,455 @@ +use crate::config::AppConfig; +use crate::hermes::{HermesProbeClient, ProbeState}; +use crate::identity::{get_display_name, get_host_id}; +use crate::models::{NetworkInterfaceInfo, PairingPayloadV1}; +use crate::network::discover_network_interfaces; +use crate::pairing::{ + create_pairing_payload, encode_pairing_uri, MAX_TTL_SECONDS, MIN_TTL_SECONDS, +}; +use crate::qr::render_egui_image; +use eframe::egui::{self, Color32, RichText, TextureHandle, Vec2}; +use std::net::Ipv4Addr; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::time::{Duration, Instant}; + +pub struct HermesPairApp { + config: AppConfig, + hermes_url: Option, + scheme: String, + port: u16, + ttl: u64, + interfaces: Vec, + selected_iface_index: usize, + + current_payload: PairingPayloadV1, + current_uri: String, + generated_at: Instant, + qr_texture: Option, + + probe_state: ProbeState, + probe_tx: Sender, + probe_rx: Receiver, + is_probing: bool, + + copied_banner_timer: Option, +} + +impl HermesPairApp { + pub fn new( + cc: &eframe::CreationContext<'_>, + config: AppConfig, + hermes_url: Option, + scheme: String, + port: u16, + explicit_interface: Option, + ttl: u64, + ) -> Self { + let ttl = ttl.clamp(MIN_TTL_SECONDS, MAX_TTL_SECONDS); + + let mut interfaces = discover_network_interfaces().unwrap_or_default(); + if interfaces.is_empty() { + interfaces.push(NetworkInterfaceInfo { + name: "Loopback".to_string(), + ip: Ipv4Addr::new(127, 0, 0, 1), + is_loopback: true, + is_virtual: false, + }); + } + + let mut selected_iface_index = 0; + if let Some(ref target) = explicit_interface { + let lower = target.to_lowercase(); + if let Some(idx) = interfaces + .iter() + .position(|i| i.name.to_lowercase().contains(&lower) || i.ip.to_string() == *target) + { + selected_iface_index = idx; + } + } + + let host_ip = interfaces[selected_iface_index].ip; + let host_id = get_host_id(&config); + let display_name = get_display_name(&config); + + let current_payload = create_pairing_payload( + host_id, + display_name, + host_ip.to_string(), + port, + scheme.clone(), + ttl, + ); + let current_uri = encode_pairing_uri(¤t_payload); + + let qr_texture = Self::build_qr_texture(&cc.egui_ctx, ¤t_uri); + + let (probe_tx, probe_rx) = channel(); + + let mut app = Self { + config, + hermes_url, + scheme, + port, + ttl, + interfaces, + selected_iface_index, + current_payload, + current_uri, + generated_at: Instant::now(), + qr_texture, + probe_state: ProbeState::Offline("Initial probe running...".to_string()), + probe_tx, + probe_rx, + is_probing: false, + copied_banner_timer: None, + }; + + app.trigger_probe(); + app + } + + fn build_qr_texture(ctx: &egui::Context, uri: &str) -> Option { + match render_egui_image(uri, 8) { + Ok(img) => Some(ctx.load_texture("qr_code", img, egui::TextureOptions::NEAREST)), + Err(e) => { + eprintln!("Failed to generate QR texture: {}", e); + None + } + } + } + + fn regenerate_payload(&mut self, ctx: &egui::Context) { + let host_ip = self.selected_ip(); + let host_id = get_host_id(&self.config); + let display_name = get_display_name(&self.config); + + self.current_payload = create_pairing_payload( + host_id, + display_name, + host_ip.to_string(), + self.port, + self.scheme.clone(), + self.ttl, + ); + self.current_uri = encode_pairing_uri(&self.current_payload); + self.generated_at = Instant::now(); + self.qr_texture = Self::build_qr_texture(ctx, &self.current_uri); + } + + fn selected_ip(&self) -> Ipv4Addr { + self.interfaces + .get(self.selected_iface_index) + .map(|i| i.ip) + .unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)) + } + + fn trigger_probe(&mut self) { + if self.is_probing { + return; + } + + self.is_probing = true; + let hermes_url = self.hermes_url.clone(); + let scheme = self.scheme.clone(); + let port = self.port; + let lan_ip = self.selected_ip(); + let tx = self.probe_tx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build(); + + if let Ok(rt) = rt { + rt.block_on(async { + let client = HermesProbeClient::new(); + let res = client + .probe(hermes_url.as_deref(), &scheme, port, Some(lan_ip)) + .await; + let _ = tx.send(res); + }); + } + }); + } + + fn refresh_interfaces(&mut self) { + if let Ok(new_ifaces) = discover_network_interfaces() { + if !new_ifaces.is_empty() { + self.interfaces = new_ifaces; + if self.selected_iface_index >= self.interfaces.len() { + self.selected_iface_index = 0; + } + } + } + } +} + +impl eframe::App for HermesPairApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Poll background probe channel + while let Ok(state) = self.probe_rx.try_recv() { + self.probe_state = state; + self.is_probing = false; + } + + // Request repaint every 500ms for smooth timer update + ctx.request_repaint_after(Duration::from_millis(500)); + + let elapsed = self.generated_at.elapsed().as_secs(); + let remaining = self.ttl.saturating_sub(elapsed); + let is_expired = remaining == 0; + + egui::CentralPanel::default().show(ctx, |ui| { + ui.spacing_mut().item_spacing = Vec2::new(8.0, 8.0); + + // Title and Hermes Status Header + ui.vertical_centered(|ui| { + ui.heading(RichText::new("Hermes Pair").size(22.0).strong()); + }); + + ui.add_space(4.0); + + // Status Card + egui::Frame::group(ui.style()) + .inner_margin(8.0) + .show(ui, |ui| { + ui.horizontal(|ui| { + match &self.probe_state { + ProbeState::Online(resp) => { + ui.colored_label(Color32::from_rgb(46, 204, 113), "● Running"); + let ver = resp.version.as_deref().unwrap_or("unknown"); + ui.label(format!("v{}", ver)); + if resp.auth_required { + ui.colored_label( + Color32::from_rgb(52, 152, 219), + "[Auth: Required]", + ); + } else { + ui.colored_label( + Color32::from_rgb(230, 126, 34), + "[Auth: None]", + ); + } + } + ProbeState::LoopbackOnly { .. } => { + ui.colored_label( + Color32::from_rgb(241, 196, 15), + "● Loopback Only", + ); + } + ProbeState::Offline(err) => { + ui.colored_label(Color32::from_rgb(231, 76, 60), "● Offline"); + ui.label(RichText::new(err).size(11.0).italics()); + } + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("🔄 Check").clicked() { + self.trigger_probe(); + } + }); + }); + }); + + // Warning Banners + if let ProbeState::LoopbackOnly { lan_error, .. } = &self.probe_state { + ui.add_space(2.0); + egui::Frame::NONE + .fill(Color32::from_rgb(60, 45, 10)) + .stroke(egui::Stroke::new(1.0_f32, Color32::from_rgb(241, 196, 15))) + .inner_margin(8.0) + .corner_radius(4.0) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("⚠️").size(16.0)); + ui.label( + RichText::new(format!( + "Hermes is bound to loopback only (127.0.0.1).\n\ + LAN connection failed: {}\n\ + Start Hermes with LAN access:\n\ + hermes serve --host 0.0.0.0 --port {}", + lan_error, self.port + )) + .size(11.5) + .color(Color32::from_rgb(241, 196, 15)), + ); + }); + }); + } + + if let ProbeState::Online(resp) = &self.probe_state { + if !resp.auth_required { + ui.add_space(2.0); + egui::Frame::NONE + .fill(Color32::from_rgb(50, 25, 20)) + .stroke(egui::Stroke::new(1.0_f32, Color32::from_rgb(230, 126, 34))) + .inner_margin(8.0) + .corner_radius(4.0) + .show(ui, |ui| { + ui.label( + RichText::new( + "⚠️ Warning: Hermes is reachable over the network without authentication. \ + Do not expose beyond a trusted LAN.", + ) + .size(11.5) + .color(Color32::from_rgb(230, 126, 34)), + ); + }); + } + } + + ui.add_space(4.0); + + // Network Interface Selector + ui.horizontal(|ui| { + ui.label("Network Interface:"); + let current_label = + if let Some(iface) = self.interfaces.get(self.selected_iface_index) { + format!("{} ({})", iface.name, iface.ip) + } else { + "None".to_string() + }; + + let prev_idx = self.selected_iface_index; + egui::ComboBox::from_id_salt("interface_select") + .selected_text(current_label) + .show_ui(ui, |ui| { + for (idx, iface) in self.interfaces.iter().enumerate() { + let tag = if iface.is_virtual { + "[Virt]" + } else if iface.ip.is_private() { + "[LAN]" + } else { + "" + }; + let label = format!("{} ({}) {}", iface.name, iface.ip, tag); + ui.selectable_value(&mut self.selected_iface_index, idx, label); + } + }); + + if prev_idx != self.selected_iface_index { + self.regenerate_payload(ctx); + self.trigger_probe(); + } + }); + + // Address display + ui.horizontal(|ui| { + ui.label(RichText::new("Target Address:").strong()); + ui.monospace(format!( + "{}://{}:{}", + self.scheme, + self.selected_ip(), + self.port + )); + }); + + ui.add_space(4.0); + + // Center QR Code or Placeholder Card + ui.vertical_centered(|ui| { + match &self.probe_state { + ProbeState::Online(_) => { + if is_expired { + egui::Frame::group(ui.style()) + .inner_margin(32.0) + .show(ui, |ui| { + ui.label( + RichText::new("⚠️ QR Code Expired") + .color(Color32::from_rgb(231, 76, 60)) + .strong() + .size(16.0), + ); + ui.add_space(8.0); + ui.label( + "Click [ 🔄 Regenerate QR ] below to create a fresh pairing code.", + ); + }); + } else if let Some(ref texture) = self.qr_texture { + ui.image((texture.id(), Vec2::new(260.0, 260.0))); + ui.add_space(6.0); + let mins = remaining / 60; + let secs = remaining % 60; + ui.label( + RichText::new(format!("Expires in {:02}:{:02}", mins, secs)) + .size(13.0) + .color(Color32::LIGHT_GRAY), + ); + } else { + ui.label("Failed to render QR code"); + } + } + ProbeState::LoopbackOnly { .. } => { + egui::Frame::group(ui.style()) + .inner_margin(32.0) + .show(ui, |ui| { + ui.colored_label( + Color32::from_rgb(241, 196, 15), + RichText::new("⚠️ Pairing QR Hidden").size(15.0).strong(), + ); + ui.add_space(8.0); + ui.label( + "Hermes is running locally on 127.0.0.1, but unreachable on this LAN interface.\n\ + Start Hermes with --host 0.0.0.0 to enable network pairing.", + ); + }); + } + ProbeState::Offline(err) => { + egui::Frame::group(ui.style()) + .inner_margin(32.0) + .show(ui, |ui| { + ui.colored_label( + Color32::from_rgb(231, 76, 60), + RichText::new("● Hermes Offline").size(15.0).strong(), + ); + ui.add_space(8.0); + ui.label(format!( + "Hermes is not responding on this endpoint ({}).\n\ + Start Hermes Agent and click [ 🔄 Check ] to connect.", + err + )); + }); + } + } + }); + + ui.add_space(6.0); + + let can_copy = self.probe_state.is_online() && !is_expired; + + // Action Buttons + ui.horizontal(|ui| { + ui.columns(2, |cols| { + if cols[0].button("🔄 Regenerate QR").clicked() { + self.refresh_interfaces(); + self.regenerate_payload(ctx); + self.trigger_probe(); + } + + if cols[1] + .add_enabled(can_copy, egui::Button::new("📋 Copy Link")) + .clicked() + { + ctx.copy_text(self.current_uri.clone()); + self.copied_banner_timer = Some(Instant::now()); + } + }); + }); + + // Copied banner notification + if let Some(timer) = self.copied_banner_timer { + if timer.elapsed().as_secs() < 2 { + ui.vertical_centered(|ui| { + ui.label( + RichText::new("✓ Pairing link copied to clipboard!") + .color(Color32::from_rgb(46, 204, 113)) + .size(12.0), + ); + }); + } else { + self.copied_banner_timer = None; + } + } + }); + } +} diff --git a/hermes-pair/src/cli.rs b/hermes-pair/src/cli.rs new file mode 100644 index 0000000..68813b0 --- /dev/null +++ b/hermes-pair/src/cli.rs @@ -0,0 +1,309 @@ +use crate::config::AppConfig; +use crate::hermes::{HermesProbeClient, ProbeState}; +use crate::identity::{get_display_name, get_host_id}; +use crate::models::NetworkInterfaceInfo; +use crate::network::discover_network_interfaces; +use crate::pairing::{create_pairing_payload, encode_pairing_uri, validate_ttl}; +use crate::qr::render_terminal_qr; +use clap::{Args, Parser, Subcommand}; +use std::net::Ipv4Addr; +use std::str::FromStr; +use std::time::Duration; +use tokio::time::sleep; + +#[derive(Parser, Debug, Clone)] +#[command( + name = "hermes-pair", + version = "0.1.0", + author = "ochenstarik-ui", + about = "Fast, secure QR onboarding helper for Hermes Agent" +)] +pub struct CliArgs { + /// Run interactive terminal mode with periodic refresh + #[arg(long, short = 't')] + pub terminal: bool, + + /// Print QR once to stdout and exit (headless/script mode) + #[arg(long = "no-gui")] + pub no_gui: bool, + + /// Port of Hermes Agent (default: 9119) + #[arg(long)] + pub port: Option, + + /// Hermes status API URL (e.g. http://127.0.0.1:9119 or http://127.0.0.1:9222) + #[arg(long = "hermes-url")] + pub hermes_url: Option, + + /// Specific network interface name or IPv4 address to advertise + #[arg(long, short = 'i')] + pub interface: Option, + + /// Pairing QR validity TTL in seconds (default 120, range 10..=600) + #[arg(long, default_value = "120")] + pub ttl: u64, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum CliCommand { + /// Output QR code once to stdout and exit + Qr(QrArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct QrArgs { + /// Port of Hermes Agent + #[arg(long)] + pub port: Option, + + /// Hermes status API URL (e.g. http://127.0.0.1:9119 or http://127.0.0.1:9222) + #[arg(long = "hermes-url")] + pub hermes_url: Option, + + /// Specific network interface name or IPv4 address + #[arg(long, short = 'i')] + pub interface: Option, + + /// Pairing QR validity TTL in seconds (range 10..=600) + #[arg(long)] + pub ttl: Option, +} + +/// Parses a Hermes URL into its scheme, host, and port components. +pub fn parse_hermes_url(raw_url: &str) -> Result<(String, String, u16), String> { + let parsed = url::Url::parse(raw_url) + .map_err(|e| format!("Invalid --hermes-url '{}': {}", raw_url, e))?; + let scheme = parsed.scheme().to_lowercase(); + if scheme != "http" && scheme != "https" { + return Err(format!( + "Invalid scheme '{}' in --hermes-url: must be 'http' or 'https'", + scheme + )); + } + let host = parsed + .host_str() + .ok_or_else(|| format!("Missing host in --hermes-url '{}'", raw_url))? + .to_string(); + let port = parsed + .port_or_known_default() + .ok_or_else(|| format!("Could not determine port from --hermes-url '{}'", raw_url))?; + Ok((scheme, host, port)) +} + +/// Resolves the scheme and port based on --hermes-url and explicit --port arguments. +pub fn resolve_cli_endpoint( + hermes_url: Option<&str>, + explicit_port: Option, +) -> Result<(String, u16), String> { + if let Some(url_str) = hermes_url { + let (parsed_scheme, _parsed_host, parsed_port) = parse_hermes_url(url_str)?; + let final_port = explicit_port.unwrap_or(parsed_port); + Ok((parsed_scheme, final_port)) + } else { + let final_port = explicit_port.unwrap_or(9119); + Ok(("http".to_string(), final_port)) + } +} + +/// Resolves the selected IPv4 address based on user input or automatic interface detection. +pub fn resolve_selected_ip( + explicit_interface: Option<&str>, + interfaces: &[NetworkInterfaceInfo], +) -> (String, Ipv4Addr) { + if let Some(target) = explicit_interface { + if let Ok(ip) = Ipv4Addr::from_str(target) { + return (format!("Manual ({})", ip), ip); + } + + let lower = target.to_lowercase(); + if let Some(matched) = interfaces + .iter() + .find(|i| i.name.to_lowercase().contains(&lower) || i.ip.to_string() == target) + { + return (matched.name.clone(), matched.ip); + } + } + + if let Some(first) = interfaces.first() { + return (first.name.clone(), first.ip); + } + + ("Loopback".to_string(), Ipv4Addr::new(127, 0, 0, 1)) +} + +/// Runs single-shot terminal output mode. +pub async fn run_once( + config: &AppConfig, + hermes_url: Option<&str>, + scheme: &str, + port: u16, + explicit_interface: Option<&str>, + ttl: u64, +) -> Result<(), Box> { + validate_ttl(ttl)?; + + let interfaces = discover_network_interfaces().unwrap_or_default(); + let (_iface_name, host_ip) = resolve_selected_ip(explicit_interface, &interfaces); + + let client = HermesProbeClient::new(); + let probe_state = client.probe(hermes_url, scheme, port, Some(host_ip)).await; + + let host_id = get_host_id(config); + let display_name = get_display_name(config); + + match &probe_state { + ProbeState::Online(status) => { + let ver = status.version.as_deref().unwrap_or("unknown"); + let auth = if status.auth_required { + "Required" + } else { + "None" + }; + println!("Hermes: Running (v{}, Auth: {})", ver, auth); + + let payload = create_pairing_payload( + host_id.clone(), + display_name.clone(), + host_ip.to_string(), + port, + scheme.to_string(), + ttl, + ); + + let uri = encode_pairing_uri(&payload); + let qr_rendered = + render_terminal_qr(&uri).map_err(|e| format!("QR Render Error: {}", e))?; + + let short_id = if host_id.len() >= 8 { + format!("{}...", &host_id[..8]) + } else { + host_id.clone() + }; + + println!("Host: {}", display_name); + println!("Address: {}://{}:{}", scheme, host_ip, port); + println!("Host ID: {}", short_id); + println!("Expires in: {:02}:{:02}", ttl / 60, ttl % 60); + println!("\n{}", qr_rendered); + println!("Pairing Link: {}", uri); + + Ok(()) + } + ProbeState::LoopbackOnly { lan_error, .. } => { + eprintln!( + "Hermes: Running locally (127.0.0.1), but LAN unreachable: {}", + lan_error + ); + eprintln!( + "⚠️ Warning: Hermes is bound to loopback only. Start Hermes with LAN access, for example:" + ); + eprintln!(" hermes serve --host 0.0.0.0 --port {}", port); + eprintln!( + "\n[Pairing QR not displayed because Hermes is unreachable from other devices]" + ); + Err("Hermes is bound to loopback only (LAN unreachable)".into()) + } + ProbeState::Offline(err) => { + eprintln!("Hermes: Offline ({})", err); + eprintln!("⚠️ Hermes Agent is unreachable. Please start Hermes before generating pairing QR."); + eprintln!("\n[Pairing QR not displayed because Hermes is offline]"); + Err(format!("Hermes is offline: {}", err).into()) + } + } +} + +/// Runs interactive terminal UI mode that updates in a loop. +pub async fn run_terminal_loop( + config: &AppConfig, + hermes_url: Option<&str>, + scheme: &str, + port: u16, + explicit_interface: Option<&str>, + ttl: u64, +) -> Result<(), Box> { + validate_ttl(ttl)?; + + let mut last_generated_at = std::time::Instant::now(); + let client = HermesProbeClient::new(); + + loop { + let now = std::time::Instant::now(); + let elapsed = now.duration_since(last_generated_at).as_secs(); + + let interfaces = discover_network_interfaces().unwrap_or_default(); + let (_iface_name, host_ip) = resolve_selected_ip(explicit_interface, &interfaces); + + if elapsed >= ttl { + last_generated_at = std::time::Instant::now(); + } + let remaining = ttl.saturating_sub(now.duration_since(last_generated_at).as_secs()); + + let probe_state = client.probe(hermes_url, scheme, port, Some(host_ip)).await; + + // Clear terminal screen (cross-platform ANSI) + print!("\x1B[2J\x1B[1;1H"); + + match &probe_state { + ProbeState::Online(status) => { + let ver = status.version.as_deref().unwrap_or("unknown"); + let auth = if status.auth_required { + "Required" + } else { + "None" + }; + println!("Hermes: Running (v{}, Auth: {})", ver, auth); + + let payload = create_pairing_payload( + get_host_id(config), + get_display_name(config), + host_ip.to_string(), + port, + scheme.to_string(), + ttl, + ); + let uri = encode_pairing_uri(&payload); + let qr_rendered = render_terminal_qr(&uri).unwrap_or_default(); + + let host_id = &payload.host_id; + let short_id = if host_id.len() >= 8 { + format!("{}...", &host_id[..8]) + } else { + host_id.clone() + }; + + println!("Host: {}", payload.name); + println!("Address: {}://{}:{}", scheme, payload.host, payload.port); + println!("Host ID: {}", short_id); + println!("Expires in: {:02}:{:02}", remaining / 60, remaining % 60); + println!("\n{}", qr_rendered); + println!("Pairing Link: {}", uri); + } + ProbeState::LoopbackOnly { lan_error, .. } => { + println!( + "Hermes: Loopback Only (127.0.0.1) [LAN Error: {}]", + lan_error + ); + println!( + "⚠️ Warning: Hermes is bound to loopback only. Start Hermes with LAN access:\n hermes serve --host 0.0.0.0 --port {}", + port + ); + println!("\n[QR Code hidden: Hermes is unreachable over LAN]"); + println!("Address: {}://{}:{}", scheme, host_ip, port); + println!("Retrying probe every second..."); + } + ProbeState::Offline(err) => { + println!("Hermes: Offline ({})", err); + println!("⚠️ Hermes Agent is unreachable. Please start Hermes."); + println!("\n[QR Code hidden: Hermes is offline]"); + println!("Address: {}://{}:{}", scheme, host_ip, port); + println!("Retrying probe every second..."); + } + } + + println!("\nPress Ctrl+C to exit."); + sleep(Duration::from_secs(1)).await; + } +} diff --git a/hermes-pair/src/config.rs b/hermes-pair/src/config.rs new file mode 100644 index 0000000..50ce6a7 --- /dev/null +++ b/hermes-pair/src/config.rs @@ -0,0 +1,115 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AppConfig { + pub host_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + host_id: Uuid::new_v4().to_string(), + display_name: None, + } + } +} + +/// Resolves configuration path according to platform conventions: +/// - Windows: `%APPDATA%\HermesPair\config.json` +/// - Linux/Unix: `~/.config/hermes-pair/config.json` +pub fn get_config_path() -> PathBuf { + #[cfg(target_os = "windows")] + { + if let Ok(appdata) = std::env::var("APPDATA") { + return PathBuf::from(appdata) + .join("HermesPair") + .join("config.json"); + } + if let Some(config_dir) = dirs::config_dir() { + return config_dir.join("HermesPair").join("config.json"); + } + PathBuf::from("config.json") + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { + return PathBuf::from(xdg).join("hermes-pair").join("config.json"); + } + if let Some(config_dir) = dirs::config_dir() { + return config_dir.join("hermes-pair").join("config.json"); + } + if let Some(home_dir) = dirs::home_dir() { + return home_dir + .join(".config") + .join("hermes-pair") + .join("config.json"); + } + PathBuf::from("config.json") + } +} + +/// Saves the configuration atomically by writing to a temporary file and renaming it. +pub fn save_config_to_path(config: &AppConfig, path: &Path) -> Result<(), std::io::Error> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let json_bytes = serde_json::to_vec_pretty(config) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let tmp_file_name = format!( + "{}.tmp.{}", + path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("config"), + Uuid::new_v4() + ); + let tmp_path = match path.parent() { + Some(p) => p.join(tmp_file_name), + None => PathBuf::from(tmp_file_name), + }; + + fs::write(&tmp_path, json_bytes)?; + + // On Windows and Unix, fs::rename replaces the destination atomically if in the same directory. + if let Err(_err) = fs::rename(&tmp_path, path) { + // Fallback in case rename fails due to cross-platform replacement edge cases + let _ = fs::remove_file(path); + if let Err(fallback_err) = fs::rename(&tmp_path, path) { + let _ = fs::remove_file(&tmp_path); + return Err(fallback_err); + } + } + + Ok(()) +} + +/// Loads an existing config or generates a new one with a persistent UUIDv4 `host_id` and saves it. +pub fn load_or_create_config_from_path(path: &Path) -> Result { + if path.exists() { + let content = fs::read_to_string(path)?; + if let Ok(mut config) = serde_json::from_str::(&content) { + if config.host_id.trim().is_empty() { + config.host_id = Uuid::new_v4().to_string(); + save_config_to_path(&config, path)?; + } + return Ok(config); + } + } + + let new_config = AppConfig::default(); + save_config_to_path(&new_config, path)?; + Ok(new_config) +} + +/// Convenience function to load or create configuration at the default system location. +pub fn load_or_create_config() -> Result { + let path = get_config_path(); + load_or_create_config_from_path(&path) +} diff --git a/hermes-pair/src/hermes.rs b/hermes-pair/src/hermes.rs new file mode 100644 index 0000000..11e5a85 --- /dev/null +++ b/hermes-pair/src/hermes.rs @@ -0,0 +1,199 @@ +use crate::models::HermesStatusResponse; +use crate::network::is_loopback; +use std::net::Ipv4Addr; +use std::time::Duration; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProbeState { + Online(HermesStatusResponse), + LoopbackOnly { + local_status: HermesStatusResponse, + lan_error: String, + }, + Offline(String), +} + +impl ProbeState { + pub fn is_online(&self) -> bool { + matches!(self, ProbeState::Online(_)) + } + + pub fn is_loopback_only(&self) -> bool { + matches!(self, ProbeState::LoopbackOnly { .. }) + } + + pub fn is_offline(&self) -> bool { + matches!(self, ProbeState::Offline(_)) + } + + pub fn status_response(&self) -> Option<&HermesStatusResponse> { + match self { + ProbeState::Online(ref resp) => Some(resp), + ProbeState::LoopbackOnly { + ref local_status, .. + } => Some(local_status), + ProbeState::Offline(_) => None, + } + } +} + +#[derive(Clone)] +pub struct HermesProbeClient { + client: reqwest::Client, +} + +impl Default for HermesProbeClient { + fn default() -> Self { + Self::new() + } +} + +impl HermesProbeClient { + pub fn new() -> Self { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + Self { client } + } + + pub async fn fetch_status(&self, base_url: &str) -> Result { + let trimmed = base_url.trim().trim_end_matches('/'); + let url = if trimmed.ends_with("/api/status") { + trimmed.to_string() + } else { + format!("{}/api/status", trimmed) + }; + + let response = self + .client + .get(&url) + .send() + .await + .map_err(|e| format!("Connection error: {}", e))?; + + if !response.status().is_success() { + return Err(format!("HTTP status {}", response.status())); + } + + let body = response + .json::() + .await + .map_err(|e| format!("Failed to parse status response: {}", e))?; + + if body.status.trim().is_empty() { + return Err("Invalid status response: missing 'status' field".to_string()); + } + + Ok(body) + } + + pub async fn probe( + &self, + hermes_url: Option<&str>, + scheme: &str, + port: u16, + lan_ip: Option, + ) -> ProbeState { + if let Some(url_str) = hermes_url { + let direct_res = self.fetch_status(url_str).await; + + if is_url_loopback(url_str) { + match direct_res { + Ok(local_status) => { + if let Some(lan) = lan_ip { + if !is_loopback(&lan) { + let lan_url = format!("{}://{}:{}", scheme, lan, port); + match self.fetch_status(&lan_url).await { + Ok(lan_status) => ProbeState::Online(lan_status), + Err(lan_err) => ProbeState::LoopbackOnly { + local_status, + lan_error: lan_err, + }, + } + } else { + ProbeState::Online(local_status) + } + } else { + ProbeState::Online(local_status) + } + } + Err(err) => { + if let Some(lan) = lan_ip { + if !is_loopback(&lan) { + let lan_url = format!("{}://{}:{}", scheme, lan, port); + match self.fetch_status(&lan_url).await { + Ok(lan_status) => ProbeState::Online(lan_status), + Err(_) => ProbeState::Offline(err), + } + } else { + ProbeState::Offline(err) + } + } else { + ProbeState::Offline(err) + } + } + } + } else { + match direct_res { + Ok(status) => ProbeState::Online(status), + Err(err) => ProbeState::Offline(err), + } + } + } else { + let local_url = format!("{}://127.0.0.1:{}", scheme, port); + let local_result = self.fetch_status(&local_url).await; + + match (local_result, lan_ip) { + (Ok(local_status), Some(lan)) if !is_loopback(&lan) => { + let lan_url = format!("{}://{}:{}", scheme, lan, port); + match self.fetch_status(&lan_url).await { + Ok(lan_status) => ProbeState::Online(lan_status), + Err(lan_err) => ProbeState::LoopbackOnly { + local_status, + lan_error: lan_err, + }, + } + } + (Ok(local_status), _) => ProbeState::Online(local_status), + (Err(local_err), Some(lan)) if !is_loopback(&lan) => { + let lan_url = format!("{}://{}:{}", scheme, lan, port); + match self.fetch_status(&lan_url).await { + Ok(lan_status) => ProbeState::Online(lan_status), + Err(_) => ProbeState::Offline(local_err), + } + } + (Err(local_err), _) => ProbeState::Offline(local_err), + } + } + } +} + +fn is_url_loopback(url_or_host: &str) -> bool { + let host = if let Ok(parsed) = url::Url::parse(url_or_host) { + parsed.host_str().unwrap_or("").to_string() + } else { + url_or_host.to_string() + }; + let host_lower = host.trim().to_lowercase(); + host_lower == "127.0.0.1" + || host_lower == "localhost" + || host_lower == "::1" + || host_lower == "[::1]" + || host_lower.starts_with("127.") +} + +pub async fn probe_hermes_status(base_url: &str) -> Result { + let client = HermesProbeClient::new(); + client.fetch_status(base_url).await +} + +pub async fn probe_hermes( + hermes_url: Option<&str>, + scheme: &str, + port: u16, + lan_ip: Option, +) -> ProbeState { + let client = HermesProbeClient::new(); + client.probe(hermes_url, scheme, port, lan_ip).await +} diff --git a/hermes-pair/src/identity.rs b/hermes-pair/src/identity.rs new file mode 100644 index 0000000..5024ae9 --- /dev/null +++ b/hermes-pair/src/identity.rs @@ -0,0 +1,54 @@ +use crate::config::AppConfig; + +/// Returns the persistent host_id from configuration. +pub fn get_host_id(config: &AppConfig) -> String { + config.host_id.clone() +} + +/// Detects the system hostname from environment variables or sensible fallbacks. +pub fn detect_system_hostname() -> String { + if let Ok(name) = std::env::var("COMPUTERNAME") { + let trimmed = name.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + + if let Ok(name) = std::env::var("HOSTNAME") { + let trimmed = name.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + + if let Ok(name) = std::env::var("HOST") { + let trimmed = name.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + + // Try reading /etc/hostname on Unix + #[cfg(unix)] + { + if let Ok(name) = std::fs::read_to_string("/etc/hostname") { + let trimmed = name.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + } + + "Hermes-Host".to_string() +} + +/// Returns the configured display name, or the detected system hostname if not set. +pub fn get_display_name(config: &AppConfig) -> String { + if let Some(ref name) = config.display_name { + let trimmed = name.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + detect_system_hostname() +} diff --git a/hermes-pair/src/lib.rs b/hermes-pair/src/lib.rs new file mode 100644 index 0000000..088c8e1 --- /dev/null +++ b/hermes-pair/src/lib.rs @@ -0,0 +1,9 @@ +pub mod app; +pub mod cli; +pub mod config; +pub mod hermes; +pub mod identity; +pub mod models; +pub mod network; +pub mod pairing; +pub mod qr; diff --git a/hermes-pair/src/main.rs b/hermes-pair/src/main.rs new file mode 100644 index 0000000..d6cbe70 --- /dev/null +++ b/hermes-pair/src/main.rs @@ -0,0 +1,96 @@ +use clap::Parser; +use eframe::egui::Vec2; +use hermes_pair::app::HermesPairApp; +use hermes_pair::cli::{resolve_cli_endpoint, run_once, run_terminal_loop, CliArgs, CliCommand}; +use hermes_pair::config::load_or_create_config; +use hermes_pair::pairing::validate_ttl; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = CliArgs::parse(); + let config = load_or_create_config()?; + + if let Some(CliCommand::Qr(ref qr_args)) = args.command { + let hermes_url = qr_args.hermes_url.as_deref().or(args.hermes_url.as_deref()); + let (scheme, port) = resolve_cli_endpoint(hermes_url, qr_args.port.or(args.port))?; + let iface = qr_args.interface.as_deref().or(args.interface.as_deref()); + let ttl = qr_args.ttl.unwrap_or(args.ttl); + validate_ttl(ttl)?; + return run_once(&config, hermes_url, &scheme, port, iface, ttl).await; + } + + let hermes_url_str = args.hermes_url.as_deref(); + let (scheme, port) = resolve_cli_endpoint(hermes_url_str, args.port)?; + let ttl = args.ttl; + validate_ttl(ttl)?; + + if args.no_gui { + return run_once( + &config, + hermes_url_str, + &scheme, + port, + args.interface.as_deref(), + ttl, + ) + .await; + } + + if args.terminal { + return run_terminal_loop( + &config, + hermes_url_str, + &scheme, + port, + args.interface.as_deref(), + ttl, + ) + .await; + } + + // Launch GUI + let native_options = eframe::NativeOptions { + viewport: eframe::egui::ViewportBuilder::default() + .with_inner_size(Vec2::new(420.0, 620.0)) + .with_min_inner_size(Vec2::new(380.0, 520.0)) + .with_title("Hermes Pair"), + ..Default::default() + }; + + let config_clone = config.clone(); + let hermes_url_owned = args.hermes_url.clone(); + let scheme_clone = scheme.clone(); + let iface = args.interface.clone(); + + let res = eframe::run_native( + "Hermes Pair", + native_options, + Box::new(move |cc| { + Ok(Box::new(HermesPairApp::new( + cc, + config_clone, + hermes_url_owned, + scheme_clone, + port, + iface, + ttl, + ))) + }), + ); + + if let Err(e) = res { + eprintln!("Failed to launch GUI: {}", e); + eprintln!("Falling back to terminal mode..."); + return run_terminal_loop( + &config, + args.hermes_url.as_deref(), + &scheme, + port, + args.interface.as_deref(), + ttl, + ) + .await; + } + + Ok(()) +} diff --git a/hermes-pair/src/models.rs b/hermes-pair/src/models.rs new file mode 100644 index 0000000..24a9c9b --- /dev/null +++ b/hermes-pair/src/models.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use std::net::Ipv4Addr; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PairingPayloadV1 { + pub v: u32, // 1 + #[serde(rename = "type")] + pub payload_type: String, // "hermes-pair" + pub host_id: String, // UUIDv4 string + pub name: String, // Display name / computer name + pub host: String, // Reachable IPv4 or hostname + pub port: u16, // Port (e.g. 9119) + pub scheme: String, // "http" or "https" + pub expires_at: u64, // Unix timestamp in seconds + pub nonce: String, // Base64URL-encoded cryptographically secure random 16 bytes +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct HermesStatusResponse { + #[serde(default)] + pub status: String, + #[serde(rename = "authRequired", default)] + pub auth_required: bool, + #[serde(rename = "authProviders", default)] + pub auth_providers: Vec, + #[serde(rename = "authFlows", default)] + pub auth_flows: Vec, + #[serde(default)] + pub version: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkInterfaceInfo { + pub name: String, + pub ip: Ipv4Addr, + pub is_loopback: bool, + pub is_virtual: bool, +} diff --git a/hermes-pair/src/network.rs b/hermes-pair/src/network.rs new file mode 100644 index 0000000..04e7698 --- /dev/null +++ b/hermes-pair/src/network.rs @@ -0,0 +1,86 @@ +pub use crate::models::NetworkInterfaceInfo; +use std::net::Ipv4Addr; + +pub fn is_loopback(ip: &Ipv4Addr) -> bool { + ip.is_loopback() || ip.octets()[0] == 127 +} + +pub fn is_link_local(ip: &Ipv4Addr) -> bool { + let octets = ip.octets(); + octets[0] == 169 && octets[1] == 254 +} + +pub fn is_tailscale_ip(ip: &Ipv4Addr) -> bool { + let octets = ip.octets(); + // CGNAT range 100.64.0.0/10 commonly used by Tailscale / WireGuard overlays + octets[0] == 100 && (octets[1] >= 64 && octets[1] <= 127) +} + +pub fn is_virtual_adapter(name: &str) -> bool { + let lower = name.to_lowercase(); + lower.contains("docker") + || lower.contains("veth") + || lower.contains("virbr") + || lower.contains("vbox") + || lower.contains("vmnet") + || lower.contains("hyper-v") + || lower.contains("wsl") + || lower.contains("vethernet") + || lower.contains("virtual") + || lower.contains("tap") + || lower.contains("tun") +} + +fn interface_priority(info: &NetworkInterfaceInfo) -> u32 { + let is_priv = info.ip.is_private(); + let is_ts = is_tailscale_ip(&info.ip); + + match (info.is_virtual, is_priv, is_ts) { + (false, true, _) => 100, // Physical LAN (192.168.x.x, 10.x.x.x, 172.16-31.x.x) + (false, false, true) => 80, // Tailscale / Overlay + (false, false, false) => 60, // Other physical (e.g. public or custom) + (true, true, _) => 40, // Virtual LAN (e.g. WSL, Hyper-V virtual switch) + (true, false, true) => 30, // Virtual Tailscale + (true, false, false) => 20, // Other virtual + } +} + +pub fn filter_and_sort_interfaces( + interfaces: impl IntoIterator, +) -> Vec { + let mut filtered: Vec = interfaces + .into_iter() + .filter(|iface| !iface.is_loopback && !is_loopback(&iface.ip) && !is_link_local(&iface.ip)) + .collect(); + + filtered.sort_by(|a, b| { + let prio_a = interface_priority(a); + let prio_b = interface_priority(b); + prio_b + .cmp(&prio_a) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| a.ip.cmp(&b.ip)) + }); + + filtered +} + +pub fn discover_network_interfaces() -> Result, std::io::Error> { + let if_addrs_list = if_addrs::get_if_addrs()?; + + let mut raw_interfaces = Vec::new(); + for iface in if_addrs_list { + if let if_addrs::IfAddr::V4(ref v4_addr) = iface.addr { + let is_virt = is_virtual_adapter(&iface.name); + let loopback = iface.is_loopback() || is_loopback(&v4_addr.ip); + raw_interfaces.push(NetworkInterfaceInfo { + name: iface.name, + ip: v4_addr.ip, + is_loopback: loopback, + is_virtual: is_virt, + }); + } + } + + Ok(filter_and_sort_interfaces(raw_interfaces)) +} diff --git a/hermes-pair/src/pairing.rs b/hermes-pair/src/pairing.rs new file mode 100644 index 0000000..694f172 --- /dev/null +++ b/hermes-pair/src/pairing.rs @@ -0,0 +1,360 @@ +pub use crate::models::PairingPayloadV1; +use base64::engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD}; +use base64::Engine; +use rand::RngCore; +use std::fmt; +use std::time::{SystemTime, UNIX_EPOCH}; +use url::Url; +use uuid::Uuid; + +pub const MIN_TTL_SECONDS: u64 = 10; +pub const MAX_TTL_SECONDS: u64 = 600; +pub const DEFAULT_TTL_SECONDS: u64 = 120; +pub const MAX_CLOCK_SKEW_SECONDS: u64 = 30; +pub const MAX_ENCODED_URI_BYTES: usize = 4096; +pub const MAX_DECODED_JSON_BYTES: usize = 2048; +pub const MAX_NAME_LENGTH: usize = 128; +pub const MIN_NONCE_BYTES: usize = 16; +pub const MAX_NONCE_BYTES: usize = 64; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PairingError { + InvalidUriScheme(String), + InvalidUriFormat(String), + MissingDataParameter, + PayloadTooLarge { size: usize, max: usize }, + Base64DecodeError(String), + JsonDecodeError(String), + UnsupportedVersion(u32), + InvalidPayloadType(String), + InvalidHostId(String), + InvalidName(String), + EmptyHost, + InvalidHost(String), + InvalidPort(u16), + InvalidScheme(String), + InvalidNonce(String), + PayloadExpired { expires_at: u64, now: u64 }, + TtlExceedsMaximum { expires_at: u64, max_allowed: u64 }, + InvalidTtl { ttl: u64, min: u64, max: u64 }, +} + +impl fmt::Display for PairingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PairingError::InvalidUriScheme(s) => { + write!(f, "Invalid URI scheme '{}', expected 'hermes'", s) + } + PairingError::InvalidUriFormat(s) => write!(f, "Invalid pairing URI format: {}", s), + PairingError::MissingDataParameter => { + write!(f, "Missing 'data' query parameter in pairing URI") + } + PairingError::PayloadTooLarge { size, max } => { + write!( + f, + "Payload size ({} bytes) exceeds maximum limit of {} bytes", + size, max + ) + } + PairingError::Base64DecodeError(e) => { + write!(f, "Failed to decode Base64URL payload: {}", e) + } + PairingError::JsonDecodeError(e) => write!(f, "Failed to parse JSON payload: {}", e), + PairingError::UnsupportedVersion(v) => { + write!(f, "Unsupported payload version {}, expected 1", v) + } + PairingError::InvalidPayloadType(t) => { + write!(f, "Invalid payload type '{}', expected 'hermes-pair'", t) + } + PairingError::InvalidHostId(id) => write!(f, "Invalid host UUID: '{}'", id), + PairingError::InvalidName(msg) => write!(f, "Invalid host display name: {}", msg), + PairingError::EmptyHost => write!(f, "Host address cannot be empty"), + PairingError::InvalidHost(msg) => write!(f, "Invalid host address: {}", msg), + PairingError::InvalidPort(p) => write!(f, "Invalid port number: {}", p), + PairingError::InvalidScheme(s) => { + write!(f, "Invalid scheme '{}', expected 'http' or 'https'", s) + } + PairingError::InvalidNonce(msg) => write!(f, "Invalid nonce: {}", msg), + PairingError::PayloadExpired { expires_at, now } => { + write!( + f, + "Pairing payload expired at timestamp {} (current time: {})", + expires_at, now + ) + } + PairingError::TtlExceedsMaximum { + expires_at, + max_allowed, + } => { + write!( + f, + "Pairing payload expiry timestamp {} exceeds maximum allowed {}", + expires_at, max_allowed + ) + } + PairingError::InvalidTtl { ttl, min, max } => { + write!( + f, + "Invalid TTL {}s: TTL must be between {} and {} seconds", + ttl, min, max + ) + } + } + } +} + +impl std::error::Error for PairingError {} + +pub fn current_unix_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +pub fn generate_nonce() -> String { + let mut bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + URL_SAFE_NO_PAD.encode(bytes) +} + +pub fn validate_ttl(ttl: u64) -> Result<(), PairingError> { + if !(MIN_TTL_SECONDS..=MAX_TTL_SECONDS).contains(&ttl) { + return Err(PairingError::InvalidTtl { + ttl, + min: MIN_TTL_SECONDS, + max: MAX_TTL_SECONDS, + }); + } + Ok(()) +} + +pub fn validate_payload(payload: &PairingPayloadV1, current_time: u64) -> Result<(), PairingError> { + // 1. Version must be 1 + if payload.v != 1 { + return Err(PairingError::UnsupportedVersion(payload.v)); + } + + // 2. Payload type must be "hermes-pair" + if payload.payload_type != "hermes-pair" { + return Err(PairingError::InvalidPayloadType( + payload.payload_type.clone(), + )); + } + + // 3. Host ID must be a valid UUIDv4 string + let host_uuid = Uuid::parse_str(&payload.host_id).map_err(|_| { + PairingError::InvalidHostId(format!("'{}' is not a valid UUID", payload.host_id)) + })?; + if host_uuid.get_version_num() != 4 { + return Err(PairingError::InvalidHostId(format!( + "UUID must be version 4 (random), got version {}", + host_uuid.get_version_num() + ))); + } + + // 4. Name: not blank, trimmed <= 128 chars, no control characters + let trimmed_name = payload.name.trim(); + if trimmed_name.is_empty() { + return Err(PairingError::InvalidName( + "Host display name cannot be blank".into(), + )); + } + if trimmed_name.chars().count() > MAX_NAME_LENGTH { + return Err(PairingError::InvalidName(format!( + "Host display name length ({}) exceeds maximum allowed {}", + trimmed_name.chars().count(), + MAX_NAME_LENGTH + ))); + } + if payload + .name + .chars() + .any(|c| (c as u32) < 0x20 || (c as u32) == 0x7F) + { + return Err(PairingError::InvalidName( + "Host display name contains forbidden control characters".into(), + )); + } + + // 5. Host: not blank, no whitespace, no forbidden chars: / \ ? # @ : control chars + let trimmed_host = payload.host.trim(); + if trimmed_host.is_empty() { + return Err(PairingError::EmptyHost); + } + if payload.host.chars().any(|c| { + c.is_whitespace() + || ['/', '\\', '?', '#', '@', ':'].contains(&c) + || (c as u32) < 0x20 + || (c as u32) == 0x7F + }) { + return Err(PairingError::InvalidHost(format!( + "Host '{}' contains forbidden characters (whitespace, delimiters, or control characters)", + payload.host + ))); + } + + // 6. Port: 1..=65535 (u16 is <= 65535, port 0 is invalid) + if payload.port == 0 { + return Err(PairingError::InvalidPort(0)); + } + + // 7. Scheme: "http" or "https" + if payload.scheme != "http" && payload.scheme != "https" { + return Err(PairingError::InvalidScheme(format!( + "Invalid scheme '{}', must be 'http' or 'https'", + payload.scheme + ))); + } + + // 8. Nonce: Base64URL-encoded, decodes to >= 16 bytes and <= 64 bytes + let trimmed_nonce = payload.nonce.trim(); + if trimmed_nonce.is_empty() { + return Err(PairingError::InvalidNonce("Nonce cannot be empty".into())); + } + let decoded_nonce = URL_SAFE_NO_PAD + .decode(trimmed_nonce.as_bytes()) + .or_else(|_| URL_SAFE.decode(trimmed_nonce.as_bytes())) + .or_else(|_| STANDARD.decode(trimmed_nonce.as_bytes())) + .map_err(|e| PairingError::InvalidNonce(format!("Nonce Base64 decode failed: {}", e)))?; + + if decoded_nonce.len() < MIN_NONCE_BYTES { + return Err(PairingError::InvalidNonce(format!( + "Nonce length {} bytes is below minimum {} bytes (128 bits)", + decoded_nonce.len(), + MIN_NONCE_BYTES + ))); + } + if decoded_nonce.len() > MAX_NONCE_BYTES { + return Err(PairingError::InvalidNonce(format!( + "Nonce length {} bytes exceeds maximum {} bytes", + decoded_nonce.len(), + MAX_NONCE_BYTES + ))); + } + + // 9. Expires at: now - 30 <= expires_at <= now + 600 + let min_allowed_expiry = current_time.saturating_sub(MAX_CLOCK_SKEW_SECONDS); + if payload.expires_at < min_allowed_expiry { + return Err(PairingError::PayloadExpired { + expires_at: payload.expires_at, + now: current_time, + }); + } + let max_allowed_expiry = current_time + MAX_TTL_SECONDS; + if payload.expires_at > max_allowed_expiry { + return Err(PairingError::TtlExceedsMaximum { + expires_at: payload.expires_at, + max_allowed: max_allowed_expiry, + }); + } + + Ok(()) +} + +pub fn create_pairing_payload( + host_id: String, + name: String, + host: String, + port: u16, + scheme: String, + ttl_seconds: u64, +) -> PairingPayloadV1 { + let now = current_unix_timestamp(); + let ttl = ttl_seconds.clamp(MIN_TTL_SECONDS, MAX_TTL_SECONDS); + let expires_at = now + ttl; + let nonce = generate_nonce(); + + PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name, + host, + port, + scheme, + expires_at, + nonce, + } +} + +pub fn encode_pairing_uri(payload: &PairingPayloadV1) -> String { + let json = serde_json::to_string(payload) + .expect("Serialization of PairingPayloadV1 should never fail"); + let encoded = URL_SAFE_NO_PAD.encode(json.as_bytes()); + format!("hermes://pair?data={}", encoded) +} + +pub fn decode_pairing_uri_at_time( + uri: &str, + current_time: u64, +) -> Result { + // Check encoded URI length bound + if uri.len() > MAX_ENCODED_URI_BYTES { + return Err(PairingError::PayloadTooLarge { + size: uri.len(), + max: MAX_ENCODED_URI_BYTES, + }); + } + + let data_str = if let Ok(url) = Url::parse(uri) { + if url.scheme() != "hermes" { + return Err(PairingError::InvalidUriScheme(url.scheme().to_string())); + } + let host = url.host_str().unwrap_or_default(); + if host != "pair" && url.path() != "pair" && url.path() != "/pair" { + return Err(PairingError::InvalidUriFormat(uri.to_string())); + } + + url.query_pairs() + .find(|(k, _)| k == "data") + .map(|(_, v)| v.into_owned()) + .ok_or(PairingError::MissingDataParameter)? + } else { + // Fallback simple parsing for custom hermes:// URIs + if !uri.starts_with("hermes://") && !uri.starts_with("hermes:") { + return Err(PairingError::InvalidUriScheme("unknown".to_string())); + } + let query_part = uri + .split_once('?') + .map(|x| x.1) + .ok_or(PairingError::MissingDataParameter)?; + let mut found = None; + for pair in query_part.split('&') { + if let Some((k, v)) = pair.split_once('=') { + if k == "data" { + found = Some(v.to_string()); + break; + } + } + } + found.ok_or(PairingError::MissingDataParameter)? + }; + + // Decode Base64 (supporting URL_SAFE_NO_PAD, URL_SAFE, and STANDARD) + let decoded_bytes = URL_SAFE_NO_PAD + .decode(data_str.as_bytes()) + .or_else(|_| URL_SAFE.decode(data_str.as_bytes())) + .or_else(|_| STANDARD.decode(data_str.as_bytes())) + .map_err(|e| PairingError::Base64DecodeError(e.to_string()))?; + + // Check decoded payload size limit + if decoded_bytes.len() > MAX_DECODED_JSON_BYTES { + return Err(PairingError::PayloadTooLarge { + size: decoded_bytes.len(), + max: MAX_DECODED_JSON_BYTES, + }); + } + + let payload: PairingPayloadV1 = serde_json::from_slice(&decoded_bytes) + .map_err(|e| PairingError::JsonDecodeError(e.to_string()))?; + + validate_payload(&payload, current_time)?; + + Ok(payload) +} + +pub fn decode_pairing_uri(uri: &str) -> Result { + let now = current_unix_timestamp(); + decode_pairing_uri_at_time(uri, now) +} diff --git a/hermes-pair/src/qr.rs b/hermes-pair/src/qr.rs new file mode 100644 index 0000000..1475b69 --- /dev/null +++ b/hermes-pair/src/qr.rs @@ -0,0 +1,110 @@ +use eframe::egui::{Color32, ColorImage}; +use qrcode::{Color, QrCode}; + +pub type QrError = qrcode::types::QrError; + +/// Generates a 2D boolean matrix of QR modules (true = dark, false = light) including a quiet zone. +pub fn generate_qr_matrix(data: &str) -> Result>, QrError> { + let code = QrCode::new(data.as_bytes())?; + let colors = code.to_colors(); + let width = code.width(); + let quiet_zone = 2; + let total_size = width + quiet_zone * 2; + + let mut matrix = vec![vec![false; total_size]; total_size]; + + for y in 0..width { + for x in 0..width { + let is_dark = colors[y * width + x] == Color::Dark; + matrix[y + quiet_zone][x + quiet_zone] = is_dark; + } + } + + Ok(matrix) +} + +/// Renders a terminal-friendly QR code using Unicode full blocks and ANSI colors. +pub fn render_terminal_qr(data: &str) -> Result { + let code = QrCode::new(data.as_bytes())?; + let colors = code.to_colors(); + let width = code.width(); + let quiet_zone = 2; + let total_size = width + quiet_zone * 2; + + let mut out = String::new(); + + // Render using ANSI inverted / double-width blocks for standard aspect ratio + // Dark modules: "██", Light modules: " " + for y in 0..total_size { + for x in 0..total_size { + let is_dark = if x >= quiet_zone + && x < quiet_zone + width + && y >= quiet_zone + && y < quiet_zone + width + { + let qx = x - quiet_zone; + let qy = y - quiet_zone; + colors[qy * width + qx] == Color::Dark + } else { + false + }; + + if is_dark { + out.push_str("██"); + } else { + out.push_str(" "); + } + } + out.push('\n'); + } + + Ok(out) +} + +/// Renders a high-contrast ColorImage for egui rendering with a quiet zone and configurable scale. +pub fn render_egui_image(data: &str, scale: usize) -> Result { + let code = QrCode::new(data.as_bytes())?; + let colors = code.to_colors(); + let width = code.width(); + let quiet_zone = 3; + let total_modules = width + quiet_zone * 2; + + let scale = scale.max(1); + let image_width = total_modules * scale; + let image_height = total_modules * scale; + + let mut pixels = Vec::with_capacity(image_width * image_height); + + for my in 0..total_modules { + for _sy in 0..scale { + for mx in 0..total_modules { + let is_dark = if mx >= quiet_zone + && mx < quiet_zone + width + && my >= quiet_zone + && my < quiet_zone + width + { + let qx = mx - quiet_zone; + let qy = my - quiet_zone; + colors[qy * width + qx] == Color::Dark + } else { + false + }; + + let color = if is_dark { + Color32::from_rgb(18, 18, 20) + } else { + Color32::from_rgb(255, 255, 255) + }; + + for _sx in 0..scale { + pixels.push(color); + } + } + } + } + + Ok(ColorImage { + size: [image_width, image_height], + pixels, + }) +} diff --git a/hermes-pair/tests/unit_and_contract_tests.rs b/hermes-pair/tests/unit_and_contract_tests.rs new file mode 100644 index 0000000..d4c0885 --- /dev/null +++ b/hermes-pair/tests/unit_and_contract_tests.rs @@ -0,0 +1,603 @@ +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use hermes_pair::cli::{parse_hermes_url, resolve_cli_endpoint}; +use hermes_pair::config::load_or_create_config_from_path; +use hermes_pair::hermes::HermesProbeClient; +use hermes_pair::models::{NetworkInterfaceInfo, PairingPayloadV1}; +use hermes_pair::network::filter_and_sort_interfaces; +use hermes_pair::pairing::{ + create_pairing_payload, decode_pairing_uri, decode_pairing_uri_at_time, encode_pairing_uri, + validate_payload, validate_ttl, PairingError, MAX_DECODED_JSON_BYTES, MAX_ENCODED_URI_BYTES, +}; +use std::net::Ipv4Addr; +use std::path::PathBuf; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; +use uuid::Uuid; + +#[test] +fn test_canonical_cross_contract_fixture() { + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id: "58af1471-a0a2-4e2b-9426-5068f2a2deab".to_string(), + name: "Office-PC".to_string(), + host: "192.168.1.150".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1800000000, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let json_str = serde_json::to_string(&payload).expect("Serialization failed"); + assert!(json_str.contains("\"v\":1")); + assert!(json_str.contains("\"type\":\"hermes-pair\"")); + assert!(json_str.contains("\"host_id\":\"58af1471-a0a2-4e2b-9426-5068f2a2deab\"")); + assert!(json_str.contains("\"name\":\"Office-PC\"")); + assert!(json_str.contains("\"host\":\"192.168.1.150\"")); + assert!(json_str.contains("\"port\":9119")); + assert!(json_str.contains("\"scheme\":\"http\"")); + assert!(json_str.contains("\"expires_at\":1800000000")); + assert!(json_str.contains("\"nonce\":\"QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY\"")); + + let uri = encode_pairing_uri(&payload); + assert!(uri.starts_with("hermes://pair?data=")); + + // Decode at current_time = expires_at - 60 (well within validity window) + let decode_time = 1800000000 - 60; + let decoded = decode_pairing_uri_at_time(&uri, decode_time) + .expect("Canonical fixture must decode successfully"); + + assert_eq!(decoded.v, 1); + assert_eq!(decoded.payload_type, "hermes-pair"); + assert_eq!(decoded.host_id, "58af1471-a0a2-4e2b-9426-5068f2a2deab"); + assert_eq!(decoded.name, "Office-PC"); + assert_eq!(decoded.host, "192.168.1.150"); + assert_eq!(decoded.port, 9119); + assert_eq!(decoded.scheme, "http"); + assert_eq!(decoded.expires_at, 1800000000); + assert_eq!(decoded.nonce, "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY"); +} + +#[test] +fn test_config_persistence() { + let tmp_dir = std::env::temp_dir(); + let config_path: PathBuf = tmp_dir.join(format!("hermes_test_config_{}.json", Uuid::new_v4())); + + let _ = std::fs::remove_file(&config_path); + + let config1 = load_or_create_config_from_path(&config_path).expect("Should create new config"); + assert!(!config1.host_id.is_empty()); + assert!(Uuid::parse_str(&config1.host_id).is_ok()); + + let config2 = + load_or_create_config_from_path(&config_path).expect("Should load existing config"); + assert_eq!(config1.host_id, config2.host_id); + + let _ = std::fs::remove_file(&config_path); +} + +#[test] +fn test_pairing_payload_serde() { + let host_id = Uuid::new_v4().to_string(); + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id: host_id.clone(), + name: "Test-Rig".to_string(), + host: "192.168.1.100".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1800000000, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let json = serde_json::to_string(&payload).expect("Serialization failed"); + let deserialized: PairingPayloadV1 = + serde_json::from_str(&json).expect("Deserialization failed"); + assert_eq!(payload, deserialized); + + let b64 = URL_SAFE_NO_PAD.encode(json.as_bytes()); + let decoded_bytes = URL_SAFE_NO_PAD + .decode(b64.as_bytes()) + .expect("B64 decode failed"); + let from_b64: PairingPayloadV1 = + serde_json::from_slice(&decoded_bytes).expect("JSON from b64 failed"); + assert_eq!(payload, from_b64); +} + +#[test] +fn test_pairing_uri_encoding_and_validation() { + let host_id = Uuid::new_v4().to_string(); + let payload = create_pairing_payload( + host_id.clone(), + "Studio-PC".to_string(), + "192.168.0.50".to_string(), + 9119, + "http".to_string(), + 300, + ); + + let uri = encode_pairing_uri(&payload); + assert!(uri.starts_with("hermes://pair?data=")); + + let decoded = decode_pairing_uri(&uri).expect("Decoding valid pairing URI must succeed"); + assert_eq!(decoded.v, 1); + assert_eq!(decoded.payload_type, "hermes-pair"); + assert_eq!(decoded.host_id, host_id); + assert_eq!(decoded.name, "Studio-PC"); + assert_eq!(decoded.host, "192.168.0.50"); + assert_eq!(decoded.port, 9119); + assert_eq!(decoded.scheme, "http"); +} + +#[test] +fn test_ttl_validation_bounds() { + assert!(validate_ttl(0).is_err()); + assert!(validate_ttl(5).is_err()); + assert!(validate_ttl(9).is_err()); + assert!(validate_ttl(10).is_ok()); + assert!(validate_ttl(120).is_ok()); + assert!(validate_ttl(600).is_ok()); + assert!(validate_ttl(601).is_err()); + assert!(validate_ttl(1000).is_err()); +} + +#[test] +fn test_expired_payload_rejection() { + let host_id = Uuid::new_v4().to_string(); + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Old-Node".to_string(), + host: "10.0.0.5".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1000, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let uri = encode_pairing_uri(&payload); + let result = decode_pairing_uri_at_time(&uri, 2000); + + match result { + Err(PairingError::PayloadExpired { expires_at, now }) => { + assert_eq!(expires_at, 1000); + assert_eq!(now, 2000); + } + other => panic!("Expected PayloadExpired error, got {:?}", other), + } +} + +#[test] +fn test_excessive_future_ttl_rejection() { + let host_id = Uuid::new_v4().to_string(); + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Future-Node".to_string(), + host: "10.0.0.5".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1000 + 700, // Exceeds now + 600 + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let uri = encode_pairing_uri(&payload); + let result = decode_pairing_uri_at_time(&uri, 1000); + + match result { + Err(PairingError::TtlExceedsMaximum { .. }) => {} + other => panic!("Expected TtlExceedsMaximum error, got {:?}", other), + } +} + +#[test] +fn test_invalid_version_rejection() { + let host_id = Uuid::new_v4().to_string(); + let payload = PairingPayloadV1 { + v: 2, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Future-Node".to_string(), + host: "10.0.0.5".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let uri = encode_pairing_uri(&payload); + let result = decode_pairing_uri_at_time(&uri, 1000); + + match result { + Err(PairingError::UnsupportedVersion(v)) => { + assert_eq!(v, 2); + } + other => panic!("Expected UnsupportedVersion error, got {:?}", other), + } +} + +#[test] +fn test_invalid_payload_type_rejection() { + let host_id = Uuid::new_v4().to_string(); + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-auth".to_string(), + host_id, + name: "Bad-Type-Node".to_string(), + host: "10.0.0.5".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let uri = encode_pairing_uri(&payload); + let result = decode_pairing_uri_at_time(&uri, 1000); + + match result { + Err(PairingError::InvalidPayloadType(t)) => { + assert_eq!(t, "hermes-auth"); + } + other => panic!("Expected InvalidPayloadType error, got {:?}", other), + } +} + +#[test] +fn test_invalid_uuid_rejection() { + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id: "not-a-valid-uuid".to_string(), + name: "Bad-UUID-Node".to_string(), + host: "10.0.0.5".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let uri = encode_pairing_uri(&payload); + let result = decode_pairing_uri_at_time(&uri, 1000); + + match result { + Err(PairingError::InvalidHostId(_)) => {} + other => panic!("Expected InvalidHostId error, got {:?}", other), + } +} + +#[test] +fn test_blank_or_oversized_name_rejection() { + let host_id = Uuid::new_v4().to_string(); + + // Blank name + let mut payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id: host_id.clone(), + name: " ".to_string(), + host: "10.0.0.5".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + assert!(validate_payload(&payload, 1000).is_err()); + + // Control characters in name + payload.name = "Office\x00PC".to_string(); + assert!(validate_payload(&payload, 1000).is_err()); + payload.name = "Office\nPC".to_string(); + assert!(validate_payload(&payload, 1000).is_err()); + + // Oversized name (>128 chars) + payload.name = "A".repeat(129); + assert!(validate_payload(&payload, 1000).is_err()); + + // Valid 128-char name + payload.name = "A".repeat(128); + assert!(validate_payload(&payload, 1000).is_ok()); +} + +#[test] +fn test_malicious_host_rejection() { + let host_id = Uuid::new_v4().to_string(); + let base_payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Node".to_string(), + host: "".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let forbidden_hosts = vec![ + "", + " ", + "user@evil.com", + "evil.com/path", + "evil.com\\path", + "evil.com?param=1", + "evil.com#frag", + "192.168.1.1:9119", + "192.168.1.1 evil.com", + "192.168.1.1\x00", + ]; + + for bad_host in forbidden_hosts { + let mut p = base_payload.clone(); + p.host = bad_host.to_string(); + assert!( + validate_payload(&p, 1000).is_err(), + "Host '{}' should be rejected", + bad_host + ); + } +} + +#[test] +fn test_invalid_port_rejection() { + let host_id = Uuid::new_v4().to_string(); + let payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Invalid-Port-Node".to_string(), + host: "192.168.1.1".to_string(), + port: 0, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + let uri = encode_pairing_uri(&payload); + let result = decode_pairing_uri_at_time(&uri, 1000); + + match result { + Err(PairingError::InvalidPort(p)) => { + assert_eq!(p, 0); + } + other => panic!("Expected InvalidPort error, got {:?}", other), + } +} + +#[test] +fn test_invalid_scheme_rejection() { + let host_id = Uuid::new_v4().to_string(); + let mut payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Node".to_string(), + host: "192.168.1.1".to_string(), + port: 9119, + scheme: "ftp".to_string(), + expires_at: 1100, + nonce: "QUJDREVGR0hJSktMTU5PUHFyc3R1dnd4eXoxMjM0NTY".to_string(), + }; + + assert!(validate_payload(&payload, 1000).is_err()); + + payload.scheme = "ws".to_string(); + assert!(validate_payload(&payload, 1000).is_err()); + + payload.scheme = "http".to_string(); + assert!(validate_payload(&payload, 1000).is_ok()); + + payload.scheme = "https".to_string(); + assert!(validate_payload(&payload, 1000).is_ok()); +} + +#[test] +fn test_nonce_validation() { + let host_id = Uuid::new_v4().to_string(); + let mut payload = PairingPayloadV1 { + v: 1, + payload_type: "hermes-pair".to_string(), + host_id, + name: "Node".to_string(), + host: "192.168.1.1".to_string(), + port: 9119, + scheme: "http".to_string(), + expires_at: 1100, + nonce: "".to_string(), + }; + + // Empty nonce rejected + assert!(validate_payload(&payload, 1000).is_err()); + + // Nonce too short (< 16 bytes decoded) + let short_bytes = [1u8; 15]; + payload.nonce = URL_SAFE_NO_PAD.encode(short_bytes); + assert!(validate_payload(&payload, 1000).is_err()); + + // Valid 16-byte nonce + let valid_16 = [1u8; 16]; + payload.nonce = URL_SAFE_NO_PAD.encode(valid_16); + assert!(validate_payload(&payload, 1000).is_ok()); + + // Valid 32-byte nonce + let valid_32 = [1u8; 32]; + payload.nonce = URL_SAFE_NO_PAD.encode(valid_32); + assert!(validate_payload(&payload, 1000).is_ok()); + + // Valid 64-byte nonce + let valid_64 = [1u8; 64]; + payload.nonce = URL_SAFE_NO_PAD.encode(valid_64); + assert!(validate_payload(&payload, 1000).is_ok()); + + // Nonce too long (> 64 bytes decoded) + let too_long = [1u8; 65]; + payload.nonce = URL_SAFE_NO_PAD.encode(too_long); + assert!(validate_payload(&payload, 1000).is_err()); + + // Invalid base64 characters + payload.nonce = "not-valid-base64!@#$%".to_string(); + assert!(validate_payload(&payload, 1000).is_err()); +} + +#[test] +fn test_oversized_payload_rejection() { + let huge_uri = format!( + "hermes://pair?data={}", + "A".repeat(MAX_ENCODED_URI_BYTES + 10) + ); + let result = decode_pairing_uri(&huge_uri); + match result { + Err(PairingError::PayloadTooLarge { .. }) => {} + other => panic!("Expected PayloadTooLarge error, got {:?}", other), + } + + // Huge JSON decoded payload + let huge_data = vec![b' '; MAX_DECODED_JSON_BYTES + 100]; + let encoded = URL_SAFE_NO_PAD.encode(&huge_data); + let uri = format!("hermes://pair?data={}", encoded); + let result = decode_pairing_uri(&uri); + match result { + Err(PairingError::PayloadTooLarge { .. }) => {} + other => panic!("Expected PayloadTooLarge error, got {:?}", other), + } +} + +#[test] +fn test_cli_parse_hermes_url_and_endpoint_resolution() { + let (scheme, host, port) = + parse_hermes_url("http://127.0.0.1:9222").expect("Should parse hermes url"); + assert_eq!(scheme, "http"); + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 9222); + + let (scheme, host, port) = + parse_hermes_url("https://localhost:8443").expect("Should parse https hermes url"); + assert_eq!(scheme, "https"); + assert_eq!(host, "localhost"); + assert_eq!(port, 8443); + + assert!(parse_hermes_url("ftp://127.0.0.1:9119").is_err()); + + // Endpoint resolution + let (s, p) = resolve_cli_endpoint(Some("http://127.0.0.1:9222"), None).unwrap(); + assert_eq!(s, "http"); + assert_eq!(p, 9222); + + let (s, p) = resolve_cli_endpoint(Some("https://127.0.0.1:9222"), Some(8888)).unwrap(); + assert_eq!(s, "https"); + assert_eq!(p, 8888); + + let (s, p) = resolve_cli_endpoint(None, None).unwrap(); + assert_eq!(s, "http"); + assert_eq!(p, 9119); + + let (s, p) = resolve_cli_endpoint(None, Some(9555)).unwrap(); + assert_eq!(s, "http"); + assert_eq!(p, 9555); +} + +#[test] +fn test_network_interface_filtering() { + let test_interfaces = vec![ + NetworkInterfaceInfo { + name: "lo".to_string(), + ip: Ipv4Addr::new(127, 0, 0, 1), + is_loopback: true, + is_virtual: false, + }, + NetworkInterfaceInfo { + name: "link-local".to_string(), + ip: Ipv4Addr::new(169, 254, 10, 20), + is_loopback: false, + is_virtual: false, + }, + NetworkInterfaceInfo { + name: "docker0".to_string(), + ip: Ipv4Addr::new(172, 17, 0, 1), + is_loopback: false, + is_virtual: true, + }, + NetworkInterfaceInfo { + name: "tailscale0".to_string(), + ip: Ipv4Addr::new(100, 80, 5, 6), + is_loopback: false, + is_virtual: false, + }, + NetworkInterfaceInfo { + name: "eth0".to_string(), + ip: Ipv4Addr::new(192, 168, 1, 10), + is_loopback: false, + is_virtual: false, + }, + ]; + + let sorted = filter_and_sort_interfaces(test_interfaces); + + // Loopback and link-local must be eliminated + assert!(!sorted + .iter() + .any(|i| i.is_loopback || i.ip == Ipv4Addr::new(127, 0, 0, 1))); + assert!(!sorted + .iter() + .any(|i| i.ip == Ipv4Addr::new(169, 254, 10, 20))); + + // Order: Physical LAN (eth0 192.168.1.10) -> Tailscale (100.80.5.6) -> Virtual LAN (docker0 172.17.0.1) + assert_eq!(sorted.len(), 3); + assert_eq!(sorted[0].name, "eth0"); + assert_eq!(sorted[1].name, "tailscale0"); + assert_eq!(sorted[2].name, "docker0"); +} + +#[tokio::test] +async fn test_mock_hermes_probe() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind mock listener"); + let port = listener.local_addr().unwrap().port(); + + let server_task = tokio::spawn(async move { + if let Ok((mut socket, _)) = listener.accept().await { + let mut buf = [0u8; 1024]; + let _ = socket.read(&mut buf).await; + + let response_body = serde_json::json!({ + "status": "running", + "authRequired": true, + "authProviders": ["bearer", "oauth2"], + "authFlows": ["token"], + "version": "1.2.0" + }) + .to_string(); + + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + response_body.len(), + response_body + ); + let _ = socket.write_all(response.as_bytes()).await; + } + }); + + let client = HermesProbeClient::new(); + let base_url = format!("http://127.0.0.1:{}", port); + let status_res = client.fetch_status(&base_url).await; + + assert!( + status_res.is_ok(), + "Probe should succeed against mock server" + ); + let status = status_res.unwrap(); + assert_eq!(status.status, "running"); + assert!(status.auth_required); + assert_eq!( + status.auth_providers, + vec!["bearer".to_string(), "oauth2".to_string()] + ); + assert_eq!(status.auth_flows, vec!["token".to_string()]); + assert_eq!(status.version, Some("1.2.0".to_string())); + + let _ = server_task.await; +}