diff --git a/Cargo.lock b/Cargo.lock index ee0f160f..7d18970f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -74,17 +74,17 @@ dependencies = [ "chacha20poly1305", "cookie-factory", "futures", - "hmac", + "hmac 0.12.1", "i18n-embed", "i18n-embed-fl", "lazy_static", "memchr", "nom 7.1.3", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "rust-embed", "scrypt", - "sha2", + "sha2 0.10.9", "subtle", "x25519-dalek", "zeroize", @@ -102,21 +102,9 @@ dependencies = [ "hkdf", "io_tee", "nom 7.1.3", - "rand 0.8.5", + "rand 0.8.6", "secrecy", - "sha2", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", + "sha2 0.10.9", ] [[package]] @@ -235,7 +223,7 @@ dependencies = [ "libloading", "linkme", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "rustc_version_runtime", "serde", "serde_json", @@ -249,9 +237,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -284,17 +272,29 @@ dependencies = [ "futures-core", "libc", "portable-atomic", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "tokio", "tokio-stream", "xattr", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -302,6 +302,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -338,9 +349,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -348,9 +359,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -507,8 +518,9 @@ dependencies = [ "fs4", "futures", "glob", - "hickory-resolver", + "hickory-resolver 0.25.2", "humantime", + "improv-wifi", "indicatif", "itertools 0.14.0", "jiff", @@ -525,7 +537,7 @@ dependencies = [ "percent-encoding", "privilege", "pulldown-cmark", - "quick-xml 0.39.2", + "quick-xml 0.39.3", "regex", "reqwest", "rpi-st7789v2-driver", @@ -701,7 +713,7 @@ version = "0.66.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "lazy_static", @@ -724,7 +736,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.12.1", @@ -743,9 +755,9 @@ dependencies = [ [[package]] name = "binstalk-downloader" -version = "0.13.38" +version = "0.13.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce41dd1861ad0ad8432c3826c5237b8fb7ce06ea063c42edf3b54e522de4651" +checksum = "c96b1433e45dc900b57e3e8b4242e25d989690dcc008d05d599d366c9e34be62" dependencies = [ "astral-tokio-tar", "async-compression", @@ -759,7 +771,7 @@ dependencies = [ "flate2", "futures-io", "futures-util", - "hickory-resolver", + "hickory-resolver 0.26.1", "httpdate", "liblzma", "native-tls", @@ -830,9 +842,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -869,6 +881,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -1005,9 +1026,9 @@ checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" [[package]] name = "cargo-platform" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", "serde_core", @@ -1024,9 +1045,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -1034,12 +1055,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -1096,7 +1111,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -1165,7 +1180,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -1263,13 +1278,19 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "colorchoice" version = "1.0.5" @@ -1314,9 +1335,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "bzip2", @@ -1330,9 +1351,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -1361,6 +1382,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1449,9 +1476,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32c" @@ -1549,7 +1576,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "crossterm_winapi", "derive_more", "document-features", @@ -1593,6 +1620,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -1634,6 +1670,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1697,15 +1742,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deflate64" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "der" @@ -1713,7 +1758,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "der_derive", "flagset", "pem-rfc7468", @@ -1764,9 +1809,9 @@ dependencies = [ [[package]] name = "detect-targets" -version = "0.1.84" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e50d56b1985b81f1fe7dd79791675bc41396aba3c522f8cf6924bb929bd5deb" +checksum = "ee67e8ac676321561b8d5d6a745de77029adfb3b2e873357137bfe439cbb96fd" dependencies = [ "cfg-if", "guess_host_triple", @@ -1803,17 +1848,29 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "dircpy" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88521b0517f5f9d51d11925d8ab4523497dcf947073fa3231a311b63941131c" +checksum = "ebcbec2b9a580ddee352ac38523d2ecd4dcaad53532957034394556909e27f4b" dependencies = [ "jwalk", "log", @@ -1847,7 +1904,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -1909,7 +1966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -1940,7 +1997,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -1954,16 +2011,16 @@ dependencies = [ [[package]] name = "embed-resource" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "vswhom", - "winreg 0.55.0", + "winreg", ] [[package]] @@ -2013,6 +2070,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + [[package]] name = "endian-type" version = "0.2.0" @@ -2031,20 +2094,41 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", ] [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "env_filter", "log", @@ -2093,6 +2177,27 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2124,15 +2229,15 @@ checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" dependencies = [ "getrandom 0.3.4", "libm", - "rand 0.9.2", + "rand 0.9.4", "siphasher", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -2397,6 +2502,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -2529,7 +2647,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2575,7 +2693,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "ignore", "walkdir", ] @@ -2605,9 +2723,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -2624,9 +2742,9 @@ dependencies = [ [[package]] name = "h3" -version = "0.0.7" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfb059a4f28a66f186ed16ad912d142f490676acba59353831d7cb45a96b0d3" +checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be" dependencies = [ "bytes", "fastrand", @@ -2638,9 +2756,9 @@ dependencies = [ [[package]] name = "h3-quinn" -version = "0.0.9" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d482318ae94198fc8e3cbb0b7ba3099c865d744e6ec7c62039ca7b6b6c66fbf" +checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4" dependencies = [ "bytes", "futures", @@ -2684,30 +2802,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", "h2", "h3", "h3-quinn", + "hickory-proto 0.26.1", "http", "idna", "ipnet", - "once_cell", + "jni", + "lru-cache", + "parking_lot", "pin-project-lite", "quinn", - "rand 0.9.2", + "rand 0.10.1", "ring", "rustls", "rustls-pki-types", @@ -2720,6 +2840,54 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "bitflags 2.11.1", + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "rustls-pki-types", + "thiserror 2.0.18", + "time", + "tinyvec", + "tracing", + "url", +] + [[package]] name = "hickory-resolver" version = "0.25.2" @@ -2728,16 +2896,42 @@ checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", - "hickory-proto", + "hickory-proto 0.25.2", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto 0.26.1", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", "quinn", - "rand 0.9.2", + "rand 0.10.1", "resolv-conf", "rustls", "smallvec", + "system-configuration", "thiserror 2.0.18", "tokio", "tokio-rustls", @@ -2750,7 +2944,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -2759,7 +2953,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -2841,11 +3044,20 @@ dependencies = [ "serde", ] +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -2858,7 +3070,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2866,15 +3077,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -2897,7 +3107,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2", "system-configuration", "tokio", "tower-service", @@ -2997,12 +3207,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3010,9 +3221,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -3023,9 +3234,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3037,15 +3248,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3057,15 +3268,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3101,9 +3312,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -3125,6 +3336,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "improv-wifi" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", + "winnow 1.0.2", + "zbus", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -3157,7 +3380,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "inotify-sys", "libc", ] @@ -3210,11 +3433,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -3227,14 +3450,15 @@ checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2", "widestring 1.2.1", - "windows-sys 0.48.0", - "winreg 0.50.0", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", ] [[package]] @@ -3242,14 +3466,7 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ - "memchr", "serde", ] @@ -3294,9 +3511,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -3341,25 +3558,52 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -3373,10 +3617,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -3413,11 +3659,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] @@ -3466,9 +3712,9 @@ dependencies = [ [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" [[package]] name = "libc" @@ -3497,9 +3743,9 @@ dependencies = [ [[package]] name = "liblzma-sys" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2db66f3268487b5033077f266da6777d057949b8f93c8ad82e441df25e6186" +checksum = "1a60851d15cd8c5346eca4ab8babff585be2ae4bc8097c067291d3ffe2add3b6" dependencies = [ "cc", "libc", @@ -3523,30 +3769,36 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linkme" -version = "0.3.35" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" +checksum = "e83272d46373fb8decca684579ac3e7c8f3d71d4cc3aa693df8759e260ae41cf" dependencies = [ "linkme-impl", ] [[package]] name = "linkme-impl" -version = "0.3.35" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" +checksum = "32d59e20403c7d08fe62b4376edfe5c7fb2ef1e6b1465379686d0f21c8df444b" dependencies = [ "proc-macro2", "quote", @@ -3567,9 +3819,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -3619,6 +3871,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -3680,12 +3941,12 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.3", ] [[package]] @@ -3716,12 +3977,12 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] @@ -3845,9 +4106,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.14" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -3883,11 +4144,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "netdev" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395f1acaf7a073f756fc22068e0b526d2cf22d09f5e13802b516ef8eeaeee5eb" +checksum = "e30af1a5073b82356d9317c18226826370b4288eba2f71c7e84e18bae51b3847" dependencies = [ "block2", "dispatch2", @@ -3920,7 +4187,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "log", "netlink-packet-core", @@ -3952,7 +4219,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -4002,7 +4269,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "fsevent-sys", "inotify", "kqueue", @@ -4020,7 +4287,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -4076,7 +4343,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -4092,9 +4359,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -4152,7 +4419,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "dispatch2", "libc", @@ -4181,7 +4448,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] @@ -4192,7 +4459,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "libc", "objc2", @@ -4242,15 +4509,14 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -4274,18 +4540,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -4300,6 +4566,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -4358,7 +4634,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4370,7 +4646,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4384,18 +4660,24 @@ dependencies = [ "elliptic-curve", "primeorder", "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", ] [[package]] name = "pack1" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e7cd9bd638dc2c831519a0caa1c006cab771a92b1303403a8322773c5b72d6" +checksum = "e3b7bb0ecf2e447b1f20ee94ee79ef6eed1e9d4b3c36ce1903b9dea3bf205523" dependencies = [ "bytemuck", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -4440,8 +4722,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -4505,7 +4787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4572,7 +4854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -4595,18 +4877,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", @@ -4617,13 +4899,7 @@ dependencies = [ name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pinentry" @@ -4663,9 +4939,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -4675,13 +4951,13 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap", - "quick-xml 0.38.4", + "quick-xml 0.39.3", "serde", "time", ] @@ -4731,9 +5007,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -4751,19 +5027,19 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" dependencies = [ "base64 0.22.1", "byteorder", "bytes", "fallible-iterator 0.2.0", - "hmac", + "hmac 0.13.0", "md-5", "memchr", - "rand 0.9.2", - "sha2", + "rand 0.10.1", + "sha2 0.11.0", "stringprep", ] @@ -4785,9 +5061,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -4807,6 +5083,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -4838,6 +5125,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -4985,7 +5281,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -5009,18 +5305,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.39.2" +version = "0.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" dependencies = [ "memchr", "serde", @@ -5038,9 +5325,9 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", - "socket2 0.6.3", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -5057,9 +5344,9 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -5078,7 +5365,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -5122,9 +5409,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5133,9 +5420,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5149,7 +5436,7 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20 0.10.0", "getrandom 0.4.2", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -5192,9 +5479,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_pcg" @@ -5216,9 +5503,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -5270,9 +5557,9 @@ dependencies = [ [[package]] name = "redb" -version = "3.1.1" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef99362319c782aa4639ad3a306b64c3bb90e12874e99b8df124cb679d988611" +checksum = "4ba239c1c1693315d3cc0e601db3b3965543afbf48c41730fdca2f069f510f4a" dependencies = [ "libc", ] @@ -5283,16 +5570,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -5404,7 +5691,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -5424,9 +5711,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -5434,13 +5721,13 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.4.0" +version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5471,15 +5758,15 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", "signature", "spki", "subtle", @@ -5488,12 +5775,12 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.3" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5526,7 +5813,7 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -5553,9 +5840,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -5582,7 +5869,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", @@ -5595,7 +5882,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno 0.3.14", "libc", "linux-raw-sys 0.12.1", @@ -5604,9 +5891,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -5632,9 +5919,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -5642,9 +5929,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -5669,9 +5956,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -5691,7 +5978,7 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "clipboard-win", "home", @@ -5700,7 +5987,7 @@ dependencies = [ "memchr", "nix", "radix_trie", - "signal-hook 0.4.3", + "signal-hook 0.4.4", "unicode-segmentation", "unicode-width 0.2.2", "utf8parse", @@ -5760,7 +6047,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5792,7 +6079,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5899,6 +6186,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -5956,7 +6254,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -6009,7 +6318,7 @@ dependencies = [ "generator", "hex", "owo-colors 3.5.0", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "rand_pcg", "scoped-tls", @@ -6075,15 +6384,31 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" @@ -6102,9 +6427,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -6159,16 +6484,6 @@ dependencies = [ "anstream", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.3" @@ -6219,7 +6534,7 @@ checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" dependencies = [ "base64ct", "pem-rfc7468", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -6234,7 +6549,7 @@ dependencies = [ "rand_core 0.6.4", "rsa", "sec1", - "sha2", + "sha2 0.10.9", "signature", "ssh-cipher", "ssh-encoding", @@ -6338,6 +6653,12 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.117" @@ -6407,7 +6728,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6532,7 +6853,7 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand 0.8.5", + "rand 0.8.6", "regex", "serde", "serde_json", @@ -6568,12 +6889,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6669,9 +6990,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -6726,8 +7047,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -6762,7 +7084,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.10.1", - "socket2 0.6.3", + "socket2", "tokio", "tokio-util", "whoami", @@ -6774,7 +7096,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144" dependencies = [ - "const-oid", + "const-oid 0.9.6", "ring", "rustls", "tokio", @@ -6841,17 +7163,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow 1.0.2", ] [[package]] @@ -6863,15 +7185,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -6942,19 +7255,18 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "a28f0d049ccfaa566e14e9663d304d8577427b368cb4710a20528690287a738b" dependencies = [ "async-compression", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", "http", "http-body", "http-body-util", - "iri-string", "pin-project-lite", "tokio", "tokio-util", @@ -6962,6 +7274,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -6990,11 +7303,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", + "symlink", "thiserror 2.0.18", "time", "tracing-subscriber", @@ -7112,7 +7426,7 @@ dependencies = [ "antithesis_sdk", "arc-swap", "bigdecimal", - "bitflags 2.11.0", + "bitflags 2.11.1", "branches", "built", "bumpalo", @@ -7138,12 +7452,12 @@ dependencies = [ "parking_lot", "paste", "polling", - "rand 0.9.2", + "rand 0.9.4", "rapidhash", "regex", "regex-syntax", "roaring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustix 1.1.4", "ryu", "serde_json", @@ -7193,7 +7507,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8402ba98c236e3e6d6ed6a43557a9a0b3a682f86a37fcafe02b659b9e6c06b82" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "memchr", "miette", "strum 0.26.3", @@ -7276,7 +7590,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" dependencies = [ - "rand 0.9.2", + "rand 0.9.4", ] [[package]] @@ -7285,7 +7599,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -7316,9 +7630,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -7326,6 +7640,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "uncased" version = "0.9.10" @@ -7395,9 +7720,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -7429,7 +7754,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -7581,11 +7906,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -7594,7 +7919,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -7608,9 +7933,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -7621,23 +7946,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7645,9 +7966,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -7658,9 +7979,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -7706,7 +8027,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -7714,9 +8035,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -7734,9 +8055,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -7764,9 +8085,9 @@ dependencies = [ [[package]] name = "whoami" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" dependencies = [ "libc", "libredox", @@ -7906,7 +8227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eca4d36df7dced7a3c149ee4af503ef736105a1ad4f062b50d14d34e0d69eb4" dependencies = [ "windows 0.62.2", - "winreg 0.55.0", + "winreg", ] [[package]] @@ -8020,7 +8341,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "widestring 1.2.1", "windows-sys 0.59.0", ] @@ -8043,15 +8364,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -8097,21 +8409,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -8187,12 +8484,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8211,12 +8502,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8245,12 +8530,6 @@ dependencies = [ "embed-resource", ] -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8281,12 +8560,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8305,12 +8578,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8329,12 +8596,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8353,12 +8614,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -8404,16 +8659,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -8433,6 +8678,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -8482,7 +8733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -8514,9 +8765,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -8545,7 +8796,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ - "const-oid", + "const-oid 0.9.6", "der", "spki", "tls_codec", @@ -8569,9 +8820,9 @@ checksum = "7008a9d8ba97a7e47d9b2df63fcdb8dade303010c5a7cd5bf2469d4da6eba673" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8580,9 +8831,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -8590,20 +8841,76 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.2", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.2", + "zvariant", +] + [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -8612,18 +8919,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -8663,9 +8970,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -8674,9 +8981,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -8686,9 +8993,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -8795,3 +9102,43 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.2", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow 1.0.2", +] diff --git a/Cargo.toml b/Cargo.toml index 926d8e71..d5b36986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/alertd", "crates/algae-cli", "crates/bestool", + "crates/improv-wifi", "crates/postgres", "crates/psql", "crates/rpi-st7789v2-driver", diff --git a/crates/bestool/Cargo.toml b/crates/bestool/Cargo.toml index cb91d3c1..db1bc015 100644 --- a/crates/bestool/Cargo.toml +++ b/crates/bestool/Cargo.toml @@ -41,6 +41,7 @@ futures = { workspace = true } glob = { version = "0.3.3", optional = true } hickory-resolver = { version = "0.25.2", optional = true } humantime = { version = "2.2.0", optional = true } +improv-wifi = { version = "0.1.0", path = "../improv-wifi", features = ["networkmanager"], optional = true } indicatif = { workspace = true, optional = true } itertools = { workspace = true, optional = true } jiff = "0.2.24" @@ -160,10 +161,12 @@ __tamanu = [ # internal feature to enable the tamanu subcommand common code ## Iti subcommands iti = [ # enable all iti subcommands "iti-battery", + "iti-improv-wifi", "iti-lcd", "iti-temperature", ] iti-battery = ["__iti", "dep:folktime", "dep:humantime", "dep:rppal"] +iti-improv-wifi = ["__iti", "dep:humantime", "dep:improv-wifi", "dep:rppal"] iti-lcd = ["__iti", "dep:ctrlc", "dep:embedded-graphics", "dep:rpi-st7789v2-driver", "dep:sysinfo"] iti-temperature = ["__iti", "dep:duct", "dep:humantime"] __iti = ["dep:zmq"] # internal feature to enable the iti subcommand common code diff --git a/crates/bestool/src/actions/iti.rs b/crates/bestool/src/actions/iti.rs index 677a111f..834129d7 100644 --- a/crates/bestool/src/actions/iti.rs +++ b/crates/bestool/src/actions/iti.rs @@ -20,6 +20,8 @@ super::subcommands! { #[cfg(feature = "iti-battery")] battery => Battery(BatteryArgs), + #[cfg(feature = "iti-improv-wifi")] + improv_wifi => ImprovWifi(ImprovWifiArgs), #[cfg(feature = "iti-lcd")] lcd => Lcd(LcdArgs), #[cfg(feature = "iti-lcd")] diff --git a/crates/bestool/src/actions/iti/improv_wifi.rs b/crates/bestool/src/actions/iti/improv_wifi.rs new file mode 100644 index 00000000..73793511 --- /dev/null +++ b/crates/bestool/src/actions/iti/improv_wifi.rs @@ -0,0 +1,420 @@ +use std::time::{Duration, Instant}; + +use clap::Parser; +use improv_wifi::{ + AuthHandle, AuthorizeMode, Connection, ImprovWifi, ImprovWifiConfig, OwnedObjectPath, + find_adapter, networkmanager::NetworkManagerBackend, power_on_adapter, +}; +use miette::{IntoDiagnostic, Result, WrapErr}; +use rppal::gpio::{Gpio, InputPin, Trigger}; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + sync::{broadcast, mpsc}, +}; +use tracing::{debug, info, warn}; + +use crate::actions::{Context, iti::ItiArgs}; + +/// Run the Improv-Wi-Fi BLE peripheral so a phone or browser can provision the device's Wi-Fi. +/// +/// Uses BlueZ for BLE and NetworkManager for Wi-Fi configuration. +/// +/// Default mode is a long-running daemon that advertises only on demand (a fresh device +/// with no Wi-Fi config advertises immediately for first-boot provisioning; once +/// provisioned, the device stays idle until a long-press on `--auth-gpio` re-enters +/// provisioning mode). Use `--one-shot` for the legacy single-provisioning behaviour. +#[derive(Debug, Clone, Parser)] +pub struct ImprovWifiArgs { + /// Bluetooth adapter to use (e.g. `hci0`). Defaults to the system's first powered adapter. + #[arg(long)] + pub adapter: Option, + + /// Local name advertised over BLE. Defaults to the system hostname. + #[arg(long)] + pub local_name: Option, + + /// Device name reported in Device Info / Device Name commands. Defaults to the system hostname. + #[arg(long)] + pub device_name: Option, + + /// Authorise when a line is received on stdin (the line content is ignored). + /// + /// When set, the device starts in `AuthorizationRequired` and only accepts credentials + /// after the first line on stdin. Only valid with `--one-shot`. + #[arg( + long, + requires = "one_shot", + conflicts_with_all = ["auth_gpio", "no_auth"], + )] + pub auth_stdin: bool, + + /// Authorise on a button press on this BCM GPIO pin. + /// + /// The pin is configured as input with the internal pull-up resistor; wire a momentary + /// switch from the pin to GND. + /// + /// In default (daemon) mode this is the long-press trigger to enter provisioning mode and + /// the short-press trigger to authorise an in-progress session. In `--one-shot` mode any + /// press authorises the single session. + #[arg(long, required_unless_present_any = ["auth_stdin", "no_auth"])] + pub auth_gpio: Option, + + /// Debounce window for `--auth-gpio`. + #[arg(long, default_value = "50ms")] + pub auth_gpio_debounce: humantime::Duration, + + /// Hold time on `--auth-gpio` that counts as a long press (daemon mode only). + #[arg(long, default_value = "3s")] + pub auth_gpio_long_press: humantime::Duration, + + /// How long an authorisation stays valid before the device reverts to + /// `AuthorizationRequired`. If unset, the device stays authorised until provisioned or + /// shut down. + #[arg(long)] + pub auth_timeout: Option, + + /// Skip authorisation gating: start the advertising session in `Authorized` and accept + /// credentials from any device in BLE range without requiring a button press or stdin + /// input. + /// + /// SECURITY WARNING: this removes the physical-presence guarantee. Any device in BLE + /// range during an advertising session can overwrite the device's Wi-Fi configuration. + /// Requires `--one-shot`. + #[arg(long, requires = "one_shot", conflicts_with = "auth_gpio")] + pub no_auth: bool, + + /// Run even if Wi-Fi is already connected. + /// + /// In `--one-shot` mode, the command exits cleanly when NetworkManager reports the Wi-Fi + /// device is in the `Activated` state. Pass this flag to override that check. + #[arg(long, requires = "one_shot")] + pub always: bool, + + /// Run a single provisioning session and exit, instead of staying alive as a daemon. + /// + /// SECURITY WARNING: while running, the device advertises over BLE until it is + /// provisioned, expanding the BLE attack surface. The default daemon mode is invisible + /// after provisioning and only re-enters advertising on a long-press of `--auth-gpio`. + #[arg(long)] + pub one_shot: bool, +} + +pub async fn run(ctx: Context) -> Result<()> { + let args = ctx.args_sub; + + let connection = Connection::system() + .await + .into_diagnostic() + .wrap_err("zbus: connect to system bus")?; + + let adapter_path = find_adapter(&connection, args.adapter.as_deref()) + .await + .into_diagnostic() + .wrap_err("BlueZ: locate adapter")?; + + power_on_adapter(&connection, &adapter_path) + .await + .into_diagnostic() + .wrap_err("BlueZ: power on adapter")?; + + let hostname = read_hostname(); + let device_name = args.device_name.clone().unwrap_or_else(|| hostname.clone()); + let local_name = args.local_name.clone().or(Some(hostname)); + + let backend = NetworkManagerBackend::new(device_name) + .await + .into_diagnostic() + .wrap_err("improv-wifi: connect to NetworkManager")?; + + let config = ImprovWifiConfig { + authorize: if args.no_auth { + AuthorizeMode::NotRequired + } else { + AuthorizeMode::Required + }, + auth_timeout: args.auth_timeout.map(Duration::from), + local_name, + }; + + if args.one_shot { + run_one_shot(connection, adapter_path, backend, args, config).await + } else { + run_watch(connection, adapter_path, backend, args, config).await + } +} + +async fn run_one_shot( + connection: Connection, + adapter_path: OwnedObjectPath, + backend: NetworkManagerBackend, + args: ImprovWifiArgs, + config: ImprovWifiConfig, +) -> Result<()> { + if !args.always + && backend + .is_connected() + .await + .into_diagnostic() + .wrap_err("improv-wifi: query NetworkManager state")? + { + info!("Wi-Fi already connected; skipping Improv-Wi-Fi provisioning. Pass --always to override."); + return Ok(()); + } + + info!("starting Improv-Wi-Fi service"); + let service = ImprovWifi::install(connection, adapter_path, backend, config) + .await + .into_diagnostic() + .wrap_err("improv-wifi: install GATT service")?; + + let auth_handle = service.auth_handle(); + + let stdin_task = args.auth_stdin.then(|| spawn_stdin_authoriser(auth_handle.clone())); + + let _gpio_pin = if let Some(pin) = args.auth_gpio { + Some( + install_short_press_authoriser( + pin, + args.auth_gpio_debounce.into(), + auth_handle, + ) + .wrap_err_with(|| format!("auth-gpio: configure BCM pin {pin}"))?, + ) + } else { + None + }; + + let result = service + .run() + .await + .into_diagnostic() + .wrap_err("improv-wifi: service loop"); + + if let Some(handle) = stdin_task { + handle.abort(); + } + + result?; + + info!("Improv-Wi-Fi service finished"); + Ok(()) +} + +async fn run_watch( + connection: Connection, + adapter_path: OwnedObjectPath, + backend: NetworkManagerBackend, + args: ImprovWifiArgs, + config: ImprovWifiConfig, +) -> Result<()> { + let pin_num = args + .auth_gpio + .expect("--watch requires --auth-gpio (clap should enforce)"); + + let mut classifier = install_press_classifier( + pin_num, + args.auth_gpio_debounce.into(), + args.auth_gpio_long_press.into(), + ) + .wrap_err_with(|| format!("auth-gpio: configure BCM pin {pin_num}"))?; + + let configured = backend + .is_configured() + .await + .into_diagnostic() + .wrap_err("improv-wifi: query NetworkManager configured-connections")?; + + let mut should_advertise = !configured; + + if !configured { + info!("no Wi-Fi configured; entering provisioning mode for initialisation"); + } else { + info!(pin = pin_num, "Wi-Fi configured; idle until long-press"); + } + + loop { + if should_advertise { + run_advertising_session( + connection.clone(), + adapter_path.clone(), + backend.clone(), + config.clone(), + classifier.short_press_tx.subscribe(), + ) + .await?; + info!("provisioning session ended; idle until next long-press"); + } + + match classifier.long_press_rx.recv().await { + Some(()) => { + info!("long press detected; entering provisioning mode"); + should_advertise = true; + } + None => { + warn!("long-press channel closed; exiting watcher"); + break; + } + } + } + + Ok(()) +} + +async fn run_advertising_session( + connection: Connection, + adapter_path: OwnedObjectPath, + backend: NetworkManagerBackend, + config: ImprovWifiConfig, + mut short_press_rx: broadcast::Receiver<()>, +) -> Result<()> { + info!("starting Improv-Wi-Fi advertising session"); + let service = ImprovWifi::install(connection, adapter_path, backend, config) + .await + .into_diagnostic() + .wrap_err("improv-wifi: install GATT service")?; + + let auth_handle = service.auth_handle(); + let press_task = tokio::spawn(async move { + loop { + match short_press_rx.recv().await { + Ok(()) => { + info!("short press, authorising"); + auth_handle.authorize(); + } + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => break, + } + } + }); + + let result = service + .run() + .await + .into_diagnostic() + .wrap_err("improv-wifi: service loop"); + press_task.abort(); + result +} + +fn read_hostname() -> String { + std::fs::read_to_string("/etc/hostname") + .ok() + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "improv-device".into()) +} + +fn spawn_stdin_authoriser(auth: AuthHandle) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + let mut lines = BufReader::new(tokio::io::stdin()).lines(); + loop { + match lines.next_line().await { + Ok(Some(_)) => { + info!("stdin line received, authorising"); + auth.authorize(); + } + Ok(None) => { + debug!("stdin closed, ending stdin authoriser"); + break; + } + Err(err) => { + warn!(?err, "stdin read error, ending stdin authoriser"); + break; + } + } + } + }) +} + +fn install_short_press_authoriser( + pin: u8, + debounce: Duration, + auth: AuthHandle, +) -> Result { + let gpio = Gpio::new().into_diagnostic().wrap_err("gpio: init")?; + let mut input = gpio + .get(pin) + .into_diagnostic() + .wrap_err_with(|| format!("gpio: open BCM pin {pin}"))? + .into_input_pullup(); + input + .set_async_interrupt(Trigger::FallingEdge, Some(debounce), move |_event| { + info!(pin, "gpio button press, authorising"); + auth.authorize(); + }) + .into_diagnostic() + .wrap_err_with(|| format!("gpio: register interrupt on BCM pin {pin}"))?; + Ok(input) +} + +/// Owns the GPIO pin and the classifier task; drop to release both. +struct PressClassifier { + long_press_rx: mpsc::UnboundedReceiver<()>, + short_press_tx: broadcast::Sender<()>, + _pin: InputPin, + _classifier_task: tokio::task::JoinHandle<()>, +} + +#[derive(Debug, Clone, Copy)] +enum Edge { + Down, + Up, +} + +fn install_press_classifier( + pin: u8, + debounce: Duration, + long_press_window: Duration, +) -> Result { + let gpio = Gpio::new().into_diagnostic().wrap_err("gpio: init")?; + let mut input = gpio + .get(pin) + .into_diagnostic() + .wrap_err_with(|| format!("gpio: open BCM pin {pin}"))? + .into_input_pullup(); + + let (edge_tx, mut edge_rx) = mpsc::unbounded_channel::(); + let (long_press_tx, long_press_rx) = mpsc::unbounded_channel::<()>(); + let (short_press_tx, _) = broadcast::channel::<()>(8); + + input + .set_async_interrupt(Trigger::Both, Some(debounce), move |event| { + let kind = match event.trigger { + Trigger::FallingEdge => Edge::Down, + Trigger::RisingEdge => Edge::Up, + _ => return, + }; + let _ = edge_tx.send(kind); + }) + .into_diagnostic() + .wrap_err_with(|| format!("gpio: register interrupt on BCM pin {pin}"))?; + + let short_press_tx_clone = short_press_tx.clone(); + let classifier_task = tokio::spawn(async move { + let mut press_start: Option = None; + while let Some(edge) = edge_rx.recv().await { + match edge { + Edge::Down => press_start = Some(Instant::now()), + Edge::Up => { + if let Some(start) = press_start.take() { + let elapsed = start.elapsed(); + if elapsed >= long_press_window { + debug!(?elapsed, "long press"); + let _ = long_press_tx.send(()); + } else { + debug!(?elapsed, "short press"); + let _ = short_press_tx_clone.send(()); + } + } + } + } + } + }); + + Ok(PressClassifier { + long_press_rx, + short_press_tx, + _pin: input, + _classifier_task: classifier_task, + }) +} diff --git a/crates/improv-wifi/Cargo.toml b/crates/improv-wifi/Cargo.toml new file mode 100644 index 00000000..169676c5 --- /dev/null +++ b/crates/improv-wifi/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "improv-wifi" +version = "0.1.0" +edition = "2024" +rust-version = "1.85.0" +resolver = "2" + +authors = ["Félix Saparelli ", "BES Developers "] +license = "GPL-3.0-or-later" +description = "Implementation of the Improv-Wi-Fi BLE peripheral protocol for Linux" +keywords = ["improv-wifi", "linux", "wifi", "ble", "networkmanager"] +categories = ["embedded", "network-programming"] +repository = "https://github.com/beyondessential/bestool" + +[lints] +workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +thiserror = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } +tracing = { workspace = true } +uuid = "1.19.0" +winnow = "1.0.2" +zbus = { version = "5.15.0", default-features = false, features = ["tokio"] } + +[features] +networkmanager = [] diff --git a/crates/improv-wifi/src/backend.rs b/crates/improv-wifi/src/backend.rs new file mode 100644 index 00000000..4a5b7397 --- /dev/null +++ b/crates/improv-wifi/src/backend.rs @@ -0,0 +1,93 @@ +use std::future::Future; + +use crate::{Capabilities, Error}; + +/// Information returned by the Device Info command (`0x03`). +/// +/// Per spec, the RPC Result for Device Info is a list of strings: firmware name, firmware version, +/// hardware identifier, device name, plus optionally OS name and OS version. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DeviceInfo { + pub firmware: String, + pub version: String, + pub hardware: String, + pub device_name: String, + pub os_name: Option, + pub os_version: Option, +} + +impl DeviceInfo { + /// Encode as the string list expected by the Improv-Wi-Fi RPC Result format. + pub fn into_strings(self) -> Vec { + let mut out = vec![self.firmware, self.version, self.hardware, self.device_name]; + if let Some(os) = self.os_name { + out.push(os); + out.push(self.os_version.unwrap_or_default()); + } + out + } +} + +/// One Wi-Fi network discovered by [`WifiConfigurator::scan`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Network { + pub ssid: String, + /// RSSI in dBm (e.g. `-60`). + pub rssi: i16, + /// Auth type string per Improv-Wi-Fi spec: `WEP`, `WPA`, `WPA2`, `WPA2 EAP`, `WPA3`, `WAPI`, + /// `NO`. Combine multiple values with `/` (e.g. `WPA/WPA2`). + pub auth: String, +} + +/// A backend that knows how to talk to the host's network stack. +/// +/// Implementations live in this crate (`networkmanager` feature) or downstream. The Improv-Wi-Fi +/// service drives this trait in response to RPC commands. +/// +/// All methods take `&self` so the backend can be shared across BLE callbacks; implementations are +/// expected to use interior mutability (or to be stateless). +pub trait WifiConfigurator: Send + Sync + 'static { + /// Capability bits for the Capabilities characteristic. + fn capabilities(&self) -> Capabilities; + + /// Make the device perform a visual or audible signal. Default: no-op. + fn identify(&self) -> impl Future> + Send { + async { Ok(()) } + } + + /// Return device info. Required if [`Capabilities::device_info`] is set. + fn device_info(&self) -> impl Future> + Send; + + /// List visible Wi-Fi networks. Required if [`Capabilities::scan`] is set. + fn scan(&self) -> impl Future, Error>> + Send; + + /// Get the current hostname. Required if [`Capabilities::hostname`] is set. + fn get_hostname(&self) -> impl Future> + Send; + + /// Set the hostname. Required if [`Capabilities::hostname`] is set. + /// + /// Implementations should validate against RFC 1123 and return [`Error::BadHostname`] on + /// failure. + fn set_hostname(&self, name: String) -> impl Future> + Send; + + /// Get the device name. The Improv-Wi-Fi spec uses the same capability bit as hostname for + /// this command. Default: returns the same value as `device_info().device_name`. + fn get_device_name(&self) -> impl Future> + Send { + async { self.device_info().await.map(|i| i.device_name) } + } + + /// Set the device name. Default: returns [`Error::Unknown`]; override if supported. + fn set_device_name(&self, _name: String) -> impl Future> + Send { + async { Err(Error::Unknown) } + } + + /// Provision the device with the given Wi-Fi credentials. + /// + /// On success, returns a list of strings to include in the RPC Result — typically a single + /// URL the client can redirect to (e.g. `http://192.0.2.42`). The list may be empty. + fn provision( + &self, + ssid: String, + password: String, + ) -> impl Future, Error>> + Send; +} diff --git a/crates/improv-wifi/src/bluez.rs b/crates/improv-wifi/src/bluez.rs new file mode 100644 index 00000000..b2d02c68 --- /dev/null +++ b/crates/improv-wifi/src/bluez.rs @@ -0,0 +1,20 @@ +//! BlueZ peripheral implementation backed by `zbus`. +//! +//! We expose ourselves to BlueZ as a tree of D-Bus objects rooted at [`APP_PATH`]: +//! +//! - `` exports `org.freedesktop.DBus.ObjectManager` (provided by zbus when an interface +//! is registered at any descendant path). +//! - `/service0` is a single primary GATT service. +//! - `/service0/charN` are the five Improv-Wi-Fi characteristics. +//! - `/adv0` is the BLE advertisement object that we register with BlueZ's +//! `LEAdvertisingManager1`. +//! +//! BlueZ then drives `ReadValue`/`WriteValue`/`StartNotify`/`StopNotify` on the characteristics +//! and reads our properties via the standard `org.freedesktop.DBus.Properties` interface. + +mod advertisement; +mod app; +mod gatt; +mod proxy; + +pub(crate) use app::{AppHandles, find_adapter, install, power_on_adapter, run}; diff --git a/crates/improv-wifi/src/bluez/advertisement.rs b/crates/improv-wifi/src/bluez/advertisement.rs new file mode 100644 index 00000000..d52dffc4 --- /dev/null +++ b/crates/improv-wifi/src/bluez/advertisement.rs @@ -0,0 +1,50 @@ +//! Server-side `org.bluez.LEAdvertisement1` interface. +//! +//! BlueZ calls `Release` once it stops advertising us. We treat that as informational only — +//! tear-down on our side is driven by the service's `run` loop dropping the registration handle. + +use std::collections::HashMap; + +use tracing::debug; +use zbus::interface; + +#[derive(Debug, Clone)] +pub(crate) struct Advertisement { + pub(crate) advertisement_type: String, + pub(crate) service_uuids: Vec, + pub(crate) service_data: HashMap>, + pub(crate) local_name: Option, + pub(crate) discoverable: bool, +} + +#[interface(name = "org.bluez.LEAdvertisement1")] +impl Advertisement { + #[zbus(property, name = "Type")] + fn ty(&self) -> &str { + &self.advertisement_type + } + + #[zbus(property)] + fn service_uuids(&self) -> Vec { + self.service_uuids.clone() + } + + #[zbus(property)] + fn service_data(&self) -> HashMap> { + self.service_data.clone() + } + + #[zbus(property)] + fn local_name(&self) -> String { + self.local_name.clone().unwrap_or_default() + } + + #[zbus(property)] + fn discoverable(&self) -> bool { + self.discoverable + } + + fn release(&self) { + debug!("LEAdvertisement1.Release called by BlueZ"); + } +} diff --git a/crates/improv-wifi/src/bluez/app.rs b/crates/improv-wifi/src/bluez/app.rs new file mode 100644 index 00000000..2a0fd823 --- /dev/null +++ b/crates/improv-wifi/src/bluez/app.rs @@ -0,0 +1,513 @@ +//! Improv-Wi-Fi BLE peripheral lifecycle: connect, register, run, tear down. + +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use tokio::{ + sync::{Mutex, broadcast, mpsc, watch}, + task::JoinHandle, +}; +use tracing::{debug, info, warn}; +use zbus::{ + Connection, + zvariant::{ObjectPath, OwnedObjectPath}, +}; + +use crate::{ + ADVERTISEMENT_SERVICE_DATA_UUID, CHARACTERISTIC_UUID_CAPABILITIES, + CHARACTERISTIC_UUID_CURRENT_STATE, CHARACTERISTIC_UUID_ERROR_STATE, + CHARACTERISTIC_UUID_RPC_COMMAND, CHARACTERISTIC_UUID_RPC_RESULT, Error, SERVICE_UUID, Status, + WifiConfigurator, + bluez::{ + advertisement::Advertisement, + gatt::{CharKind, Characteristic, Service}, + proxy::{ + Adapter1Proxy, BluezObjectManagerProxy, GattManager1Proxy, LEAdvertisingManager1Proxy, + }, + }, + rpc::Reassembler, + service::{AuthorizeMode, ImprovWifiConfig, InnerState, State}, +}; + +const APP_PATH: &str = "/au/bes/improv"; +const SERVICE_PATH: &str = "/au/bes/improv/service0"; +const ADV_PATH: &str = "/au/bes/improv/adv0"; + +const ADAPTER_INTERFACE: &str = "org.bluez.Adapter1"; + +/// Power on the BlueZ adapter at `adapter_path`. +pub(crate) async fn power_on_adapter( + connection: &Connection, + adapter_path: &OwnedObjectPath, +) -> Result<(), Error> { + let adapter = Adapter1Proxy::builder(connection) + .path(adapter_path.clone()) + .map_err(map)? + .build() + .await + .map_err(map)?; + adapter.set_powered(true).await.map_err(map) +} + +/// Resolve an adapter object path on `org.bluez`. If `name` is `Some("hciN")`, looks for a +/// matching adapter; otherwise returns the first adapter found. +pub(crate) async fn find_adapter( + connection: &Connection, + name: Option<&str>, +) -> Result { + let manager = BluezObjectManagerProxy::new(connection) + .await + .map_err(map)?; + let objects = manager.get_managed_objects().await.map_err(map)?; + let mut found_first: Option = None; + let want_suffix = name.map(|n| format!("/{n}")); + for (path, ifaces) in objects { + if !ifaces.contains_key(ADAPTER_INTERFACE) { + continue; + } + if let Some(suffix) = &want_suffix { + if path.as_str().ends_with(suffix) { + return Ok(path); + } + } else if found_first.is_none() { + found_first = Some(path); + } + } + if let Some(suffix) = want_suffix { + warn!(adapter = %&suffix[1..], "no matching BlueZ adapter found"); + return Err(Error::Unknown); + } + found_first.ok_or_else(|| { + warn!("no BlueZ adapters found"); + Error::Unknown + }) +} + +/// Handles for everything we registered with BlueZ. Dropping the inner state shuts the GATT +/// callbacks down; explicit unregister calls happen in `run` after the loop exits. +pub(crate) struct AppHandles { + pub(crate) connection: Connection, + pub(crate) adapter_path: OwnedObjectPath, + pub(crate) state: Arc>, + pub(crate) provisioned_rx: watch::Receiver, + pub(crate) status_change_for_adv: broadcast::Receiver, + pub(crate) local_name: Option, + pub(crate) auth_timeout: Option, + pub(crate) notify_tasks: Vec>, + pub(crate) auth_tx: mpsc::UnboundedSender<()>, + pub(crate) auth_rx: mpsc::UnboundedReceiver<()>, +} + +/// Build the shared state, register all objects on the object server, and call +/// `RegisterApplication` + `RegisterAdvertisement`. +pub(crate) async fn install( + connection: Connection, + adapter_path: OwnedObjectPath, + configurator: T, + config: ImprovWifiConfig, +) -> Result, Error> { + let capabilities = configurator.capabilities(); + let auth_required = matches!(config.authorize, AuthorizeMode::Required); + let initial_status = if auth_required { + Status::AuthorizationRequired + } else { + Status::Authorized + }; + + let (status_tx, _) = broadcast::channel(8); + let (error_tx, _) = broadcast::channel(8); + let (rpc_result_tx, _) = broadcast::channel(8); + let (auth_reset_tx, _) = watch::channel(()); + let (provisioned_tx, provisioned_rx) = watch::channel(false); + let (auth_tx, auth_rx) = mpsc::unbounded_channel(); + let status_change_for_adv = status_tx.subscribe(); + + let state = Arc::new(State { + inner: Mutex::new(InnerState { + status: initial_status, + last_error: 0, + rpc_result: Vec::new(), + }), + capabilities, + configurator, + reassembler: Mutex::new(Reassembler::new()), + status_tx: status_tx.clone(), + error_tx: error_tx.clone(), + rpc_result_tx: rpc_result_tx.clone(), + auth_reset_tx, + provisioned_tx, + auth_required, + }); + + let object_server = connection.object_server(); + + // Service object. + let service = Service { + uuid: SERVICE_UUID.to_string(), + primary: true, + }; + object_server.at(SERVICE_PATH, service).await.map_err(map)?; + + let service_path = OwnedObjectPath::try_from(SERVICE_PATH).map_err(|_| Error::Unknown)?; + + // Characteristic objects. + let chars: &[(CharKind, &str, uuid::Uuid, &[&str])] = &[ + ( + CharKind::Capabilities, + "char0", + CHARACTERISTIC_UUID_CAPABILITIES, + &["read"], + ), + ( + CharKind::CurrentState, + "char1", + CHARACTERISTIC_UUID_CURRENT_STATE, + &["read", "notify"], + ), + ( + CharKind::ErrorState, + "char2", + CHARACTERISTIC_UUID_ERROR_STATE, + &["read", "notify"], + ), + ( + CharKind::RpcCommand, + "char3", + CHARACTERISTIC_UUID_RPC_COMMAND, + &["write", "write-without-response"], + ), + ( + CharKind::RpcResult, + "char4", + CHARACTERISTIC_UUID_RPC_RESULT, + &["read", "notify"], + ), + ]; + + let mut notify_paths: Vec<(CharKind, String)> = Vec::new(); + for (kind, leaf, uuid, flags) in chars { + let path = format!("{SERVICE_PATH}/{leaf}"); + let initial_value = match kind { + CharKind::Capabilities => vec![capabilities.as_byte()], + CharKind::CurrentState => vec![initial_status.as_byte()], + CharKind::ErrorState => vec![0], + _ => Vec::new(), + }; + let char_obj = Characteristic { + uuid: uuid.to_string(), + service_path: service_path.clone(), + flags: flags.iter().map(|s| (*s).to_string()).collect(), + value: initial_value, + notifying: false, + kind: *kind, + state: state.clone(), + }; + object_server + .at(path.clone(), char_obj) + .await + .map_err(map)?; + if flags.contains(&"notify") { + notify_paths.push((*kind, path)); + } + } + + // Initial advertisement. + let initial_adv = build_advertisement( + &capabilities, + initial_status.as_byte(), + config.local_name.as_deref(), + ); + object_server.at(ADV_PATH, initial_adv).await.map_err(map)?; + + // Register the application + advertisement with BlueZ on the chosen adapter. + let gatt_mgr = GattManager1Proxy::builder(&connection) + .path(adapter_path.clone()) + .map_err(map)? + .build() + .await + .map_err(map)?; + let app_path = ObjectPath::try_from(APP_PATH).map_err(|_| Error::Unknown)?; + gatt_mgr + .register_application(&app_path, HashMap::new()) + .await + .map_err(|err| { + warn!(?err, "GattManager1.RegisterApplication failed"); + Error::Unknown + })?; + debug!("registered GATT application with BlueZ"); + + let adv_mgr = LEAdvertisingManager1Proxy::builder(&connection) + .path(adapter_path.clone()) + .map_err(map)? + .build() + .await + .map_err(map)?; + let adv_path = ObjectPath::try_from(ADV_PATH).map_err(|_| Error::Unknown)?; + adv_mgr + .register_advertisement(&adv_path, HashMap::new()) + .await + .map_err(|err| { + warn!(?err, "LEAdvertisingManager1.RegisterAdvertisement failed"); + // Best-effort cleanup of GATT registration on failure. + Error::Unknown + })?; + debug!("registered LE advertisement with BlueZ"); + + // Spawn notify-push tasks: each notify-capable characteristic subscribes to its broadcast + // channel and emits PropertiesChanged on `Value` whenever a fresh value comes through. + let mut notify_tasks = Vec::with_capacity(notify_paths.len()); + for (kind, path) in notify_paths { + let conn = connection.clone(); + let st = state.clone(); + let task = match kind { + CharKind::CurrentState => { + let mut rx = status_tx.subscribe(); + tokio::spawn(async move { + while let Ok(status) = rx.recv().await { + push_value::(&conn, &path, vec![status.as_byte()]).await; + } + let _ = st; + }) + } + CharKind::ErrorState => { + let mut rx = error_tx.subscribe(); + tokio::spawn(async move { + while let Ok(byte) = rx.recv().await { + push_value::(&conn, &path, vec![byte]).await; + } + let _ = st; + }) + } + CharKind::RpcResult => { + let mut rx = rpc_result_tx.subscribe(); + tokio::spawn(async move { + while let Ok(bytes) = rx.recv().await { + push_value::(&conn, &path, bytes).await; + } + let _ = st; + }) + } + _ => continue, + }; + notify_tasks.push(task); + } + + Ok(AppHandles { + connection, + adapter_path, + state, + provisioned_rx, + status_change_for_adv, + local_name: config.local_name, + auth_timeout: config.auth_timeout, + notify_tasks, + auth_tx, + auth_rx, + }) +} + +/// Run the service until provisioning succeeds, then unregister everything. +pub(crate) async fn run( + mut handles: AppHandles, +) -> Result<(), Error> { + let auth_required = handles.state.auth_required; + let auth_timeout = handles.auth_timeout; + let timeout_state = handles.state.clone(); + let mut auth_reset_rx = handles.state.auth_reset_tx.subscribe(); + let mut provisioned_for_timeout = handles.provisioned_rx.clone(); + let timeout_task: Option> = match (auth_required, auth_timeout) { + (true, Some(auth_timeout)) => Some(tokio::spawn(async move { + loop { + let sleep = tokio::time::sleep(auth_timeout); + tokio::pin!(sleep); + tokio::select! { + biased; + _ = provisioned_for_timeout.changed() => { + if *provisioned_for_timeout.borrow() { + return; + } + } + res = auth_reset_rx.changed() => { + if res.is_err() { + return; + } + continue; + } + _ = &mut sleep => { + if matches!(timeout_state.inner.lock().await.status, Status::Authorized) { + info!("authorisation timed out, reverting to AuthorizationRequired"); + timeout_state.set_status(Status::AuthorizationRequired).await; + } + } + } + } + })), + _ => None, + }; + + let mut provisioned = handles.provisioned_rx.clone(); + + loop { + tokio::select! { + res = handles.status_change_for_adv.recv() => { + match res { + Ok(_) => { + let new_byte = handles.state.current_state_byte().await; + refresh_advertisement( + &handles.connection, + &handles.adapter_path, + &handles.state.capabilities, + new_byte, + handles.local_name.as_deref(), + ).await?; + } + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => break, + } + } + Some(()) = handles.auth_rx.recv() => { + debug!("authorisation signal received"); + handles.state.set_status(Status::Authorized).await; + } + res = provisioned.changed() => { + if res.is_err() { + break; + } + if *provisioned.borrow() { + info!("provisioning successful, shutting down Improv service"); + break; + } + } + } + } + + if let Some(task) = timeout_task { + task.abort(); + } + for task in handles.notify_tasks.drain(..) { + task.abort(); + } + + // Best-effort unregister. + let adv_path = ObjectPath::try_from(ADV_PATH).map_err(|_| Error::Unknown)?; + let app_path = ObjectPath::try_from(APP_PATH).map_err(|_| Error::Unknown)?; + + match LEAdvertisingManager1Proxy::builder(&handles.connection) + .path(handles.adapter_path.clone()) + { + Ok(builder) => match builder.build().await { + Ok(p) => { + if let Err(err) = p.unregister_advertisement(&adv_path).await { + debug!(?err, "UnregisterAdvertisement failed (continuing)"); + } + } + Err(err) => debug!(?err, "build LEAdvertisingManager1 proxy for cleanup failed"), + }, + Err(err) => debug!(?err, "LEAdvertisingManager1 builder path failed"), + } + + match GattManager1Proxy::builder(&handles.connection).path(handles.adapter_path.clone()) { + Ok(builder) => match builder.build().await { + Ok(p) => { + if let Err(err) = p.unregister_application(&app_path).await { + debug!(?err, "UnregisterApplication failed (continuing)"); + } + } + Err(err) => debug!(?err, "build GattManager1 proxy for cleanup failed"), + }, + Err(err) => debug!(?err, "GattManager1 builder path failed"), + } + + let object_server = handles.connection.object_server(); + let _ = object_server.remove::(ADV_PATH).await; + for leaf in ["char0", "char1", "char2", "char3", "char4"] { + let _ = object_server + .remove::, _>(format!("{SERVICE_PATH}/{leaf}")) + .await; + } + let _ = object_server.remove::(SERVICE_PATH).await; + + Ok(()) +} + +fn build_advertisement( + capabilities: &crate::Capabilities, + status_byte: u8, + local_name: Option<&str>, +) -> Advertisement { + let cap_byte = capabilities.as_byte(); + let mut service_data = HashMap::new(); + service_data.insert( + ADVERTISEMENT_SERVICE_DATA_UUID.to_string(), + vec![status_byte, cap_byte, 0, 0, 0, 0], + ); + Advertisement { + advertisement_type: "peripheral".to_owned(), + service_uuids: vec![SERVICE_UUID.to_string()], + service_data, + local_name: local_name.map(str::to_owned), + discoverable: true, + } +} + +async fn refresh_advertisement( + connection: &Connection, + adapter_path: &OwnedObjectPath, + capabilities: &crate::Capabilities, + status_byte: u8, + local_name: Option<&str>, +) -> Result<(), Error> { + let adv_path = ObjectPath::try_from(ADV_PATH).map_err(|_| Error::Unknown)?; + let adv_mgr = LEAdvertisingManager1Proxy::builder(connection) + .path(adapter_path.clone()) + .map_err(map)? + .build() + .await + .map_err(map)?; + + if let Err(err) = adv_mgr.unregister_advertisement(&adv_path).await { + debug!(?err, "UnregisterAdvertisement before refresh failed"); + } + + let object_server = connection.object_server(); + let _ = object_server.remove::(ADV_PATH).await; + let new_adv = build_advertisement(capabilities, status_byte, local_name); + object_server.at(ADV_PATH, new_adv).await.map_err(map)?; + + adv_mgr + .register_advertisement(&adv_path, HashMap::new()) + .await + .map_err(|err| { + warn!(?err, "RegisterAdvertisement during refresh failed"); + Error::Unknown + })?; + Ok(()) +} + +async fn push_value( + connection: &Connection, + path: &str, + new_value: Vec, +) { + match connection + .object_server() + .interface::<_, Characteristic>(path) + .await + { + Ok(iface_ref) => { + { + let mut iface = iface_ref.get_mut().await; + iface.value = new_value; + } + let iface = iface_ref.get().await; + if let Err(err) = iface.value_changed(iface_ref.signal_emitter()).await { + debug!(?err, path, "value_changed signal failed"); + } + } + Err(err) => debug!(?err, path, "lookup characteristic interface failed"), + } +} + +fn map(err: zbus::Error) -> Error { + warn!(?err, "zbus error"); + Error::Unknown +} diff --git a/crates/improv-wifi/src/bluez/gatt.rs b/crates/improv-wifi/src/bluez/gatt.rs new file mode 100644 index 00000000..d22c49eb --- /dev/null +++ b/crates/improv-wifi/src/bluez/gatt.rs @@ -0,0 +1,108 @@ +//! Server-side `org.bluez.GattService1` and `GattCharacteristic1` interfaces. + +use std::{collections::HashMap, sync::Arc}; + +use zbus::{ + interface, + zvariant::{ObjectPath, OwnedObjectPath, Value}, +}; + +use crate::{WifiConfigurator, service::State}; + +#[derive(Debug, Clone)] +pub(crate) struct Service { + pub(crate) uuid: String, + pub(crate) primary: bool, +} + +#[interface(name = "org.bluez.GattService1")] +impl Service { + #[zbus(property, name = "UUID")] + fn uuid(&self) -> String { + self.uuid.clone() + } + + #[zbus(property)] + fn primary(&self) -> bool { + self.primary + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CharKind { + Capabilities, + CurrentState, + ErrorState, + RpcCommand, + RpcResult, +} + +pub(crate) struct Characteristic { + pub(crate) uuid: String, + pub(crate) service_path: OwnedObjectPath, + pub(crate) flags: Vec, + pub(crate) value: Vec, + pub(crate) notifying: bool, + pub(crate) kind: CharKind, + pub(crate) state: Arc>, +} + +#[interface(name = "org.bluez.GattCharacteristic1")] +impl Characteristic { + #[zbus(property, name = "UUID")] + fn uuid(&self) -> String { + self.uuid.clone() + } + + #[zbus(property)] + fn service(&self) -> ObjectPath<'_> { + self.service_path.as_ref() + } + + #[zbus(property)] + fn flags(&self) -> Vec { + self.flags.clone() + } + + #[zbus(property)] + fn value(&self) -> Vec { + self.value.clone() + } + + #[zbus(property)] + fn notifying(&self) -> bool { + self.notifying + } + + async fn read_value(&self, _options: HashMap>) -> zbus::fdo::Result> { + let bytes = match self.kind { + CharKind::Capabilities => vec![self.state.capabilities.as_byte()], + CharKind::CurrentState => vec![self.state.current_state_byte().await], + CharKind::ErrorState => vec![self.state.error_byte().await], + CharKind::RpcCommand => Vec::new(), + CharKind::RpcResult => self.state.rpc_result_bytes().await, + }; + Ok(bytes) + } + + async fn write_value( + &self, + value: Vec, + _options: HashMap>, + ) -> zbus::fdo::Result<()> { + if matches!(self.kind, CharKind::RpcCommand) { + self.state.handle_write(value).await; + } + Ok(()) + } + + async fn start_notify(&mut self) -> zbus::fdo::Result<()> { + self.notifying = true; + Ok(()) + } + + async fn stop_notify(&mut self) -> zbus::fdo::Result<()> { + self.notifying = false; + Ok(()) + } +} diff --git a/crates/improv-wifi/src/bluez/proxy.rs b/crates/improv-wifi/src/bluez/proxy.rs new file mode 100644 index 00000000..d7a225b8 --- /dev/null +++ b/crates/improv-wifi/src/bluez/proxy.rs @@ -0,0 +1,66 @@ +//! Client proxies for BlueZ services we call out to. + +use std::collections::HashMap; + +use zbus::{ + proxy, + zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}, +}; + +#[proxy( + interface = "org.bluez.Adapter1", + default_service = "org.bluez", + gen_blocking = false +)] +pub(crate) trait Adapter1 { + #[zbus(property)] + fn powered(&self) -> zbus::Result; + + #[zbus(property)] + fn set_powered(&self, powered: bool) -> zbus::Result<()>; + + #[zbus(property)] + fn address(&self) -> zbus::Result; +} + +#[proxy( + interface = "org.bluez.GattManager1", + default_service = "org.bluez", + gen_blocking = false +)] +pub(crate) trait GattManager1 { + fn register_application( + &self, + application: &ObjectPath<'_>, + options: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; + + fn unregister_application(&self, application: &ObjectPath<'_>) -> zbus::Result<()>; +} + +#[proxy( + interface = "org.bluez.LEAdvertisingManager1", + default_service = "org.bluez", + gen_blocking = false +)] +pub(crate) trait LEAdvertisingManager1 { + fn register_advertisement( + &self, + advertisement: &ObjectPath<'_>, + options: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; + + fn unregister_advertisement(&self, advertisement: &ObjectPath<'_>) -> zbus::Result<()>; +} + +#[proxy( + interface = "org.freedesktop.DBus.ObjectManager", + default_service = "org.bluez", + default_path = "/", + gen_blocking = false +)] +pub(crate) trait BluezObjectManager { + fn get_managed_objects( + &self, + ) -> zbus::Result>>>; +} diff --git a/crates/improv-wifi/src/error.rs b/crates/improv-wifi/src/error.rs new file mode 100644 index 00000000..4f9f1ef6 --- /dev/null +++ b/crates/improv-wifi/src/error.rs @@ -0,0 +1,51 @@ +use thiserror::Error; + +/// Improv Wi-Fi error codes, as transmitted on the Error State characteristic. +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Error { + /// RPC packet was malformed or had a bad checksum. + #[error("invalid RPC packet")] + InvalidRPC = 0x01, + + /// The command sent is unknown. + #[error("unknown RPC command")] + UnknownRPC = 0x02, + + /// Credentials were received but the device couldn't connect to the network. + #[error("unable to connect to the requested network")] + UnableToConnect = 0x03, + + /// Credentials were sent via RPC but the Improv service is not authorised. + #[error("not authorised")] + NotAuthorized = 0x04, + + /// A hostname value is not RFC 1123 compliant. + #[error("bad hostname")] + BadHostname = 0x05, + + /// Catch-all for backend errors. + #[error("unknown error")] + Unknown = 0xFF, +} + +impl Error { + pub fn as_byte(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn error_byte_values() { + assert_eq!(Error::InvalidRPC.as_byte(), 0x01); + assert_eq!(Error::UnknownRPC.as_byte(), 0x02); + assert_eq!(Error::UnableToConnect.as_byte(), 0x03); + assert_eq!(Error::NotAuthorized.as_byte(), 0x04); + assert_eq!(Error::BadHostname.as_byte(), 0x05); + assert_eq!(Error::Unknown.as_byte(), 0xFF); + } +} diff --git a/crates/improv-wifi/src/lib.rs b/crates/improv-wifi/src/lib.rs new file mode 100644 index 00000000..1cb98000 --- /dev/null +++ b/crates/improv-wifi/src/lib.rs @@ -0,0 +1,44 @@ +//! An implementation of the [Improv Wi-Fi] BLE peripheral protocol for Linux. +//! +//! This crate provides the device side of the Improv Wi-Fi configuration protocol via BlueZ's +//! D-Bus API. It is intended for embedded Linux devices that need to be provisioned onto a Wi-Fi +//! network without a display or input peripherals. +//! +//! [Improv Wi-Fi]: https://www.improv-wifi.com +#![cfg(target_os = "linux")] + +use uuid::Uuid; + +mod backend; +mod bluez; +mod error; +pub mod rpc; +mod service; +mod state; + +#[cfg(feature = "networkmanager")] +pub mod networkmanager; + +pub use backend::{DeviceInfo, Network, WifiConfigurator}; +pub use error::Error; +pub use service::{ + AuthHandle, AuthorizeMode, ImprovWifi, ImprovWifiConfig, find_adapter, power_on_adapter, +}; +pub use state::{Capabilities, Status}; +pub use zbus::{Connection, zvariant::OwnedObjectPath}; + +pub const SERVICE_UUID: Uuid = Uuid::from_u128(0x00467768_6228_2272_4663_277478268000); +pub const CHARACTERISTIC_UUID_CAPABILITIES: Uuid = + Uuid::from_u128(0x00467768_6228_2272_4663_277478268005); +pub const CHARACTERISTIC_UUID_CURRENT_STATE: Uuid = + Uuid::from_u128(0x00467768_6228_2272_4663_277478268001); +pub const CHARACTERISTIC_UUID_ERROR_STATE: Uuid = + Uuid::from_u128(0x00467768_6228_2272_4663_277478268002); +pub const CHARACTERISTIC_UUID_RPC_COMMAND: Uuid = + Uuid::from_u128(0x00467768_6228_2272_4663_277478268003); +pub const CHARACTERISTIC_UUID_RPC_RESULT: Uuid = + Uuid::from_u128(0x00467768_6228_2272_4663_277478268004); + +/// 16-bit Service Data UUID used in BLE advertisements (`0x4677`). +pub const ADVERTISEMENT_SERVICE_DATA_UUID: Uuid = + Uuid::from_u128(0x00004677_0000_1000_8000_00805f9b34fb); diff --git a/crates/improv-wifi/src/networkmanager.rs b/crates/improv-wifi/src/networkmanager.rs new file mode 100644 index 00000000..8d9a3311 --- /dev/null +++ b/crates/improv-wifi/src/networkmanager.rs @@ -0,0 +1,566 @@ +//! NetworkManager backend for [`crate::WifiConfigurator`], implemented over D-Bus. + +use std::{collections::HashMap, time::Duration}; + +use tokio::time::{sleep, timeout}; +use tracing::{debug, info, warn}; +use zbus::{Connection, proxy}; + +use crate::{Capabilities, DeviceInfo, Error, Network, WifiConfigurator}; + +/// NM device type for Wi-Fi (`NM_DEVICE_TYPE_WIFI`). +const NM_DEVICE_TYPE_WIFI: u32 = 2; + +/// NM device state (`NM_DEVICE_STATE_ACTIVATED`). +const NM_DEVICE_STATE_ACTIVATED: u32 = 100; + +/// NM active-connection state (`NM_ACTIVE_CONNECTION_STATE_ACTIVATED`). +const NM_ACTIVE_CONNECTION_STATE_ACTIVATED: u32 = 2; + +/// NM active-connection state (`NM_ACTIVE_CONNECTION_STATE_DEACTIVATING`). +const NM_ACTIVE_CONNECTION_STATE_DEACTIVATING: u32 = 3; + +/// 802.11 security flags from `NM80211ApSecurityFlags`. +const NM_AP_SEC_NONE: u32 = 0x0; +const NM_AP_SEC_KEY_MGMT_PSK: u32 = 0x100; +const NM_AP_SEC_KEY_MGMT_802_1X: u32 = 0x200; +const NM_AP_SEC_KEY_MGMT_SAE: u32 = 0x400; +const NM_AP_SEC_KEY_MGMT_OWE: u32 = 0x800; + +/// 802.11 AP flags from `NM80211ApFlags`. +const NM_AP_FLAGS_PRIVACY: u32 = 0x1; + +/// How long we wait for a new connection to reach `ACTIVATED`. +const PROVISION_TIMEOUT: Duration = Duration::from_secs(30); + +/// How long we give NM after `RequestScan` before reading APs. +const SCAN_SETTLE: Duration = Duration::from_secs(4); + +#[proxy( + interface = "org.freedesktop.NetworkManager", + default_service = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager" +)] +trait NetworkManager { + fn get_devices(&self) -> zbus::Result>; + + #[zbus(name = "AddAndActivateConnection")] + fn add_and_activate_connection( + &self, + connection: HashMap<&str, HashMap<&str, zbus::zvariant::Value<'_>>>, + device: &zbus::zvariant::ObjectPath<'_>, + specific_object: &zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<( + zbus::zvariant::OwnedObjectPath, // connection path + zbus::zvariant::OwnedObjectPath, // active-connection path + )>; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Device", + default_service = "org.freedesktop.NetworkManager" +)] +trait NmDevice { + #[zbus(property)] + fn device_type(&self) -> zbus::Result; + + #[zbus(property)] + fn state(&self) -> zbus::Result; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Device.Wireless", + default_service = "org.freedesktop.NetworkManager" +)] +trait NmDeviceWireless { + fn request_scan(&self, options: HashMap<&str, zbus::zvariant::Value<'_>>) -> zbus::Result<()>; + + #[zbus(property)] + fn access_points(&self) -> zbus::Result>; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.AccessPoint", + default_service = "org.freedesktop.NetworkManager" +)] +trait NmAccessPoint { + #[zbus(property, name = "Ssid")] + fn ssid(&self) -> zbus::Result>; + + #[zbus(property)] + fn strength(&self) -> zbus::Result; + + #[zbus(property)] + fn flags(&self) -> zbus::Result; + + #[zbus(property)] + fn wpa_flags(&self) -> zbus::Result; + + #[zbus(property)] + fn rsn_flags(&self) -> zbus::Result; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Connection.Active", + default_service = "org.freedesktop.NetworkManager" +)] +trait NmActiveConnection { + #[zbus(property)] + fn state(&self) -> zbus::Result; +} + +#[proxy( + interface = "org.freedesktop.hostname1", + default_service = "org.freedesktop.hostname1", + default_path = "/org/freedesktop/hostname1" +)] +trait Hostname1 { + fn set_static_hostname(&self, name: &str, interactive: bool) -> zbus::Result<()>; + + #[zbus(property, name = "StaticHostname")] + fn static_hostname(&self) -> zbus::Result; +} + +/// NetworkManager-backed [`WifiConfigurator`]. +#[derive(Clone, Debug)] +pub struct NetworkManagerBackend { + connection: Connection, + device_name: String, + firmware: String, + firmware_version: String, + hardware: String, +} + +impl NetworkManagerBackend { + /// Connect to the system bus and prepare the backend. + pub async fn new(device_name: impl Into) -> Result { + let connection = Connection::system().await.map_err(map_zbus_err)?; + Ok(Self { + connection, + device_name: device_name.into(), + firmware: "bestool".into(), + firmware_version: env!("CARGO_PKG_VERSION").into(), + hardware: std::env::consts::ARCH.into(), + }) + } + + pub fn with_firmware(mut self, name: impl Into, version: impl Into) -> Self { + self.firmware = name.into(); + self.firmware_version = version.into(); + self + } + + pub fn with_hardware(mut self, hardware: impl Into) -> Self { + self.hardware = hardware.into(); + self + } + + async fn first_wifi_device(&self) -> Result { + let nm = NetworkManagerProxy::new(&self.connection) + .await + .map_err(map_zbus_err)?; + let devices = nm.get_devices().await.map_err(map_zbus_err)?; + for path in devices { + let dev = NmDeviceProxy::builder(&self.connection) + .path(path.clone()) + .map_err(map_zbus_err)? + .build() + .await + .map_err(map_zbus_err)?; + if let Ok(t) = dev.device_type().await + && t == NM_DEVICE_TYPE_WIFI + { + return Ok(path); + } + } + warn!("no Wi-Fi device found via NetworkManager"); + Err(Error::Unknown) + } + + /// Whether the Wi-Fi device is currently connected to a network. + /// + /// Returns `true` if the first Wi-Fi device is in NM state `Activated`. If no Wi-Fi + /// device is found, returns `false` (treated as "not connected" so the caller can fall + /// through to its usual failure path on actual provisioning attempts). + pub async fn is_connected(&self) -> Result { + let device_path = match self.first_wifi_device().await { + Ok(p) => p, + Err(_) => return Ok(false), + }; + let dev = NmDeviceProxy::builder(&self.connection) + .path(device_path) + .map_err(map_zbus_err)? + .build() + .await + .map_err(map_zbus_err)?; + let state = dev.state().await.map_err(map_zbus_err)?; + Ok(state == NM_DEVICE_STATE_ACTIVATED) + } + + /// Whether any saved Wi-Fi connection profile exists. + /// + /// Returns `true` if NM has at least one connection profile of type `802-11-wireless`, + /// regardless of whether it is currently connected. Use this to gate boot-time + /// initialisation: a fresh device returns `false`, a previously provisioned one returns + /// `true` even if it's not currently connected to the network. + pub async fn is_configured(&self) -> Result { + let settings = NmSettingsProxy::new(&self.connection) + .await + .map_err(map_zbus_err)?; + let conns = settings.list_connections().await.map_err(map_zbus_err)?; + for path in conns { + let conn = NmSettingsConnectionProxy::builder(&self.connection) + .path(path) + .map_err(map_zbus_err)? + .build() + .await + .map_err(map_zbus_err)?; + let Ok(settings_dict) = conn.get_settings().await else { + continue; + }; + let Some(connection) = settings_dict.get("connection") else { + continue; + }; + let Some(type_val) = connection.get("type") else { + continue; + }; + if let Ok(type_str) = String::try_from(type_val.clone()) + && type_str == "802-11-wireless" + { + return Ok(true); + } + } + Ok(false) + } +} + +impl WifiConfigurator for NetworkManagerBackend { + fn capabilities(&self) -> Capabilities { + Capabilities { + identify: false, + device_info: true, + scan: true, + hostname: true, + } + } + + async fn device_info(&self) -> Result { + let (os_name, os_version) = read_os_release(); + Ok(DeviceInfo { + firmware: self.firmware.clone(), + version: self.firmware_version.clone(), + hardware: self.hardware.clone(), + device_name: self.device_name.clone(), + os_name, + os_version, + }) + } + + async fn scan(&self) -> Result, Error> { + let device_path = self.first_wifi_device().await?; + let wireless = NmDeviceWirelessProxy::builder(&self.connection) + .path(device_path) + .map_err(map_zbus_err)? + .build() + .await + .map_err(map_zbus_err)?; + + // Best-effort: errors here usually mean "scan already in progress" — ignore. + if let Err(err) = wireless.request_scan(HashMap::new()).await { + debug!(?err, "RequestScan returned an error (often benign)"); + } + sleep(SCAN_SETTLE).await; + + let aps = wireless.access_points().await.map_err(map_zbus_err)?; + let mut out = Vec::with_capacity(aps.len()); + for ap_path in aps { + let ap = NmAccessPointProxy::builder(&self.connection) + .path(ap_path) + .map_err(map_zbus_err)? + .build() + .await + .map_err(map_zbus_err)?; + let ssid_bytes = ap.ssid().await.map_err(map_zbus_err)?; + let ssid = match String::from_utf8(ssid_bytes) { + Ok(s) if !s.is_empty() => s, + _ => continue, + }; + let strength_pct = ap.strength().await.unwrap_or(0); + let flags = ap.flags().await.unwrap_or(0); + let wpa_flags = ap.wpa_flags().await.unwrap_or(0); + let rsn_flags = ap.rsn_flags().await.unwrap_or(0); + out.push(Network { + ssid, + rssi: strength_to_dbm(strength_pct), + auth: auth_string(flags, wpa_flags, rsn_flags), + }); + } + Ok(out) + } + + async fn get_hostname(&self) -> Result { + let proxy = Hostname1Proxy::new(&self.connection) + .await + .map_err(map_zbus_err)?; + proxy.static_hostname().await.map_err(map_zbus_err) + } + + async fn set_hostname(&self, name: String) -> Result<(), Error> { + if !is_valid_rfc1123_hostname(&name) { + return Err(Error::BadHostname); + } + let proxy = Hostname1Proxy::new(&self.connection) + .await + .map_err(map_zbus_err)?; + proxy + .set_static_hostname(&name, false) + .await + .map_err(map_zbus_err) + } + + async fn provision(&self, ssid: String, password: String) -> Result, Error> { + let device_path = self.first_wifi_device().await?; + let nm = NetworkManagerProxy::new(&self.connection) + .await + .map_err(map_zbus_err)?; + + let mut connection: HashMap<&str, HashMap<&str, zbus::zvariant::Value<'_>>> = + HashMap::new(); + + let mut conn_settings = HashMap::new(); + conn_settings.insert("type", zbus::zvariant::Value::from("802-11-wireless")); + conn_settings.insert("id", zbus::zvariant::Value::from(ssid.clone())); + connection.insert("connection", conn_settings); + + let mut wifi_settings = HashMap::new(); + wifi_settings.insert( + "ssid", + zbus::zvariant::Value::from(ssid.as_bytes().to_vec()), + ); + wifi_settings.insert("mode", zbus::zvariant::Value::from("infrastructure")); + connection.insert("802-11-wireless", wifi_settings); + + if !password.is_empty() { + let mut sec_settings = HashMap::new(); + sec_settings.insert("key-mgmt", zbus::zvariant::Value::from("wpa-psk")); + sec_settings.insert("psk", zbus::zvariant::Value::from(password)); + connection.insert("802-11-wireless-security", sec_settings); + } + + let empty_path = zbus::zvariant::ObjectPath::try_from("/").unwrap(); + let device_obj = device_path.as_ref(); + let (conn_path, active_path) = nm + .add_and_activate_connection(connection, &device_obj, &empty_path) + .await + .map_err(|err| { + warn!(?err, "AddAndActivateConnection failed"); + Error::UnableToConnect + })?; + info!(?conn_path, ?active_path, "activated connection"); + + let active = NmActiveConnectionProxy::builder(&self.connection) + .path(active_path.clone()) + .map_err(map_zbus_err)? + .build() + .await + .map_err(map_zbus_err)?; + + let activated = timeout(PROVISION_TIMEOUT, async { + loop { + let state = active.state().await.unwrap_or(0); + if state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED { + return true; + } + if state >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING { + return false; + } + sleep(Duration::from_millis(500)).await; + } + }) + .await + .unwrap_or(false); + + if !activated { + warn!("connection did not reach ACTIVATED in time, deleting"); + let _ = delete_connection(&self.connection, &conn_path).await; + return Err(Error::UnableToConnect); + } + + Ok(Vec::new()) + } +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Settings.Connection", + default_service = "org.freedesktop.NetworkManager" +)] +trait NmSettingsConnection { + fn delete(&self) -> zbus::Result<()>; + + fn get_settings( + &self, + ) -> zbus::Result>>; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Settings", + default_service = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager/Settings" +)] +trait NmSettings { + fn list_connections(&self) -> zbus::Result>; +} + +async fn delete_connection( + conn: &Connection, + path: &zbus::zvariant::OwnedObjectPath, +) -> zbus::Result<()> { + let proxy = NmSettingsConnectionProxy::builder(conn) + .path(path.clone())? + .build() + .await?; + proxy.delete().await +} + +fn map_zbus_err(err: zbus::Error) -> Error { + warn!(?err, "zbus error"); + Error::Unknown +} + +/// Map NM's 0-100% strength to a fake dBm value matching the Improv-Wi-Fi expectations +/// (negative number, e.g. -60). NM doesn't expose raw dBm; this is a reasonable approximation. +fn strength_to_dbm(strength_pct: u8) -> i16 { + // 100% → -30 dBm, 0% → -90 dBm. + -90 + (strength_pct as i16) * 60 / 100 +} + +fn auth_string(ap_flags: u32, wpa_flags: u32, rsn_flags: u32) -> String { + let mut parts: Vec<&str> = Vec::new(); + let privacy = ap_flags & NM_AP_FLAGS_PRIVACY != 0; + + if wpa_flags != NM_AP_SEC_NONE { + // "WPA" version 1. + parts.push(if wpa_flags & NM_AP_SEC_KEY_MGMT_802_1X != 0 { + "WPA EAP" + } else { + "WPA" + }); + } + if rsn_flags != NM_AP_SEC_NONE { + if rsn_flags & NM_AP_SEC_KEY_MGMT_SAE != 0 { + parts.push("WPA3"); + } else if rsn_flags & NM_AP_SEC_KEY_MGMT_OWE != 0 { + parts.push("WPA2"); + } else if rsn_flags & NM_AP_SEC_KEY_MGMT_802_1X != 0 { + parts.push("WPA2 EAP"); + } else if rsn_flags & NM_AP_SEC_KEY_MGMT_PSK != 0 { + parts.push("WPA2"); + } + } + if parts.is_empty() { + if privacy { "WEP".into() } else { "NO".into() } + } else { + parts.join("/") + } +} + +/// RFC 1123 hostname check: 1-253 chars, dot-separated labels of 1-63 chars each, each label +/// containing only `[A-Za-z0-9-]` and not starting/ending with `-`. +fn is_valid_rfc1123_hostname(s: &str) -> bool { + if s.is_empty() || s.len() > 253 { + return false; + } + s.split('.').all(|label| { + !label.is_empty() + && label.len() <= 63 + && !label.starts_with('-') + && !label.ends_with('-') + && label + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + }) +} + +fn read_os_release() -> (Option, Option) { + let Ok(content) = std::fs::read_to_string("/etc/os-release") else { + return (None, None); + }; + let mut name = None; + let mut version = None; + for line in content.lines() { + let Some((k, v)) = line.split_once('=') else { + continue; + }; + let v = v.trim_matches('"').to_owned(); + match k { + "NAME" => name = Some(v), + "VERSION_ID" => version = Some(v), + _ => {} + } + } + (name, version) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rfc1123_acceptance() { + assert!(is_valid_rfc1123_hostname("a")); + assert!(is_valid_rfc1123_hostname("my-host")); + assert!(is_valid_rfc1123_hostname("a.b.c")); + assert!(is_valid_rfc1123_hostname(&"a".repeat(63))); + assert!(!is_valid_rfc1123_hostname("")); + assert!(!is_valid_rfc1123_hostname("-bad")); + assert!(!is_valid_rfc1123_hostname("bad-")); + assert!(!is_valid_rfc1123_hostname("a..b")); + assert!(!is_valid_rfc1123_hostname("under_score")); + assert!(!is_valid_rfc1123_hostname(&"a".repeat(64))); + } + + #[test] + fn auth_string_combinations() { + assert_eq!(auth_string(0, 0, 0), "NO"); + assert_eq!(auth_string(NM_AP_FLAGS_PRIVACY, 0, 0), "WEP"); + assert_eq!( + auth_string(NM_AP_FLAGS_PRIVACY, NM_AP_SEC_KEY_MGMT_PSK, 0), + "WPA" + ); + assert_eq!( + auth_string(NM_AP_FLAGS_PRIVACY, 0, NM_AP_SEC_KEY_MGMT_PSK), + "WPA2" + ); + assert_eq!( + auth_string(NM_AP_FLAGS_PRIVACY, 0, NM_AP_SEC_KEY_MGMT_SAE), + "WPA3" + ); + assert_eq!( + auth_string( + NM_AP_FLAGS_PRIVACY, + NM_AP_SEC_KEY_MGMT_PSK, + NM_AP_SEC_KEY_MGMT_PSK, + ), + "WPA/WPA2" + ); + assert_eq!( + auth_string(NM_AP_FLAGS_PRIVACY, 0, NM_AP_SEC_KEY_MGMT_802_1X), + "WPA2 EAP" + ); + } + + #[test] + fn strength_mapping() { + assert_eq!(strength_to_dbm(0), -90); + assert_eq!(strength_to_dbm(100), -30); + assert_eq!(strength_to_dbm(50), -60); + } + + #[test] + fn os_release_handles_missing() { + // We can't really test reading /etc/os-release, but we can ensure the function doesn't + // panic on a malformed file by using a private helper if we had one. For now, trust the + // implementation and just exercise the path. + let _ = read_os_release(); + } +} diff --git a/crates/improv-wifi/src/rpc.rs b/crates/improv-wifi/src/rpc.rs new file mode 100644 index 00000000..cdfe80bc --- /dev/null +++ b/crates/improv-wifi/src/rpc.rs @@ -0,0 +1,80 @@ +//! Improv-Wi-Fi RPC packet types and parsing. +//! +//! Packets sent by the client on the RPC Command characteristic have the structure: +//! +//! ```text +//! [command_id: u8] [data_length: u8] [data: u8 * data_length] [checksum: u8] +//! ``` +//! +//! Where `checksum` is the least-significant byte of the additive sum of all preceding bytes +//! (including `command_id` and `data_length`). + +mod parse; +mod reassembly; +mod result; + +pub use parse::{ParseError, parse_packet}; +pub use reassembly::{Reassembler, Yielded}; +pub use result::encode_response; + +/// A parsed RPC command from the client. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Command { + /// `0x01` Send Wi-Fi Settings — provision the device with the given credentials. + SendWifiSettings { ssid: String, password: String }, + + /// `0x02` Identify — make the device perform a visual or audible signal. + Identify, + + /// `0x03` Device Info — request the device's firmware/hardware info. + DeviceInfo, + + /// `0x04` Scan — list visible Wi-Fi networks. + Scan, + + /// `0x05` (zero data) Get the device hostname. + GetHostname, + + /// `0x05` (with data) Set the device hostname. + SetHostname(String), + + /// `0x06` (zero data) Get the device name. + GetDeviceName, + + /// `0x06` (with data) Set the device name. + SetDeviceName(String), +} + +impl Command { + /// The command ID byte that introduces this command on the wire. + pub fn id(&self) -> u8 { + match self { + Self::SendWifiSettings { .. } => 0x01, + Self::Identify => 0x02, + Self::DeviceInfo => 0x03, + Self::Scan => 0x04, + Self::GetHostname | Self::SetHostname(_) => 0x05, + Self::GetDeviceName | Self::SetDeviceName(_) => 0x06, + } + } +} + +/// Compute the LSB-only additive checksum used by Improv-Wi-Fi packets. +pub fn checksum(bytes: &[u8]) -> u8 { + bytes.iter().fold(0u8, |acc, &b| acc.wrapping_add(b)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checksum_lsb_wraps() { + assert_eq!(checksum(&[]), 0); + assert_eq!(checksum(&[0x01, 0x02, 0x03]), 0x06); + // 0xFF + 0x01 wraps to 0x00. + assert_eq!(checksum(&[0xFF, 0x01]), 0x00); + // Sum of 256 ones = 256 → LSB 0. + assert_eq!(checksum(&[1u8; 256]), 0); + } +} diff --git a/crates/improv-wifi/src/rpc/parse.rs b/crates/improv-wifi/src/rpc/parse.rs new file mode 100644 index 00000000..43764612 --- /dev/null +++ b/crates/improv-wifi/src/rpc/parse.rs @@ -0,0 +1,297 @@ +use thiserror::Error; +use winnow::{ModalResult, Parser, binary::u8 as parse_u8, error::ContextError, token::take}; + +use super::{Command, checksum}; + +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ParseError { + #[error("incomplete RPC packet: need {needed} more bytes")] + Incomplete { needed: usize }, + + #[error("malformed RPC packet")] + Malformed, + + #[error("RPC packet checksum mismatch")] + BadChecksum, + + #[error("unknown RPC command id: {0:#04x}")] + UnknownCommand(u8), + + #[error("RPC payload contains invalid UTF-8")] + BadUtf8, + + #[error("Send Wi-Fi Settings payload is malformed")] + BadWifiSettings, +} + +/// Outcome of attempting to parse a single packet from the head of a byte buffer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Parsed { + pub command: Command, + /// Number of bytes consumed from the input. + pub consumed: usize, +} + +/// Parse a single RPC packet from the head of `input`. +/// +/// Returns `Ok(Parsed)` on success, [`ParseError::Incomplete`] when the buffer doesn't yet hold a +/// full packet, and any other [`ParseError`] for unrecoverable problems with this packet. +pub fn parse_packet(input: &[u8]) -> Result { + if input.len() < 2 { + return Err(ParseError::Incomplete { + needed: 2 - input.len(), + }); + } + + let command_id = input[0]; + let data_length = input[1] as usize; + let total_len = 2 + data_length + 1; + + if input.len() < total_len { + return Err(ParseError::Incomplete { + needed: total_len - input.len(), + }); + } + + let header_and_data = &input[..2 + data_length]; + let provided_checksum = input[2 + data_length]; + let expected_checksum = checksum(header_and_data); + if provided_checksum != expected_checksum { + return Err(ParseError::BadChecksum); + } + + let data = &input[2..2 + data_length]; + let command = decode_command(command_id, data)?; + Ok(Parsed { + command, + consumed: total_len, + }) +} + +fn decode_command(id: u8, data: &[u8]) -> Result { + match id { + 0x01 => decode_wifi_settings(data), + 0x02 => expect_empty(data).map(|_| Command::Identify), + 0x03 => expect_empty(data).map(|_| Command::DeviceInfo), + 0x04 => expect_empty(data).map(|_| Command::Scan), + 0x05 => decode_hostname_or_get(data, || Command::GetHostname, Command::SetHostname), + 0x06 => decode_hostname_or_get(data, || Command::GetDeviceName, Command::SetDeviceName), + other => Err(ParseError::UnknownCommand(other)), + } +} + +fn expect_empty(data: &[u8]) -> Result<(), ParseError> { + if data.is_empty() { + Ok(()) + } else { + Err(ParseError::Malformed) + } +} + +fn decode_hostname_or_get( + data: &[u8], + get: impl FnOnce() -> Command, + set: impl FnOnce(String) -> Command, +) -> Result { + if data.is_empty() { + Ok(get()) + } else { + let s = std::str::from_utf8(data).map_err(|_| ParseError::BadUtf8)?; + Ok(set(s.to_owned())) + } +} + +fn decode_wifi_settings(data: &[u8]) -> Result { + let mut input = data; + let (ssid_bytes, pwd_bytes) = parse_wifi_payload + .parse_next(&mut input) + .map_err(|_| ParseError::BadWifiSettings)?; + if !input.is_empty() { + return Err(ParseError::BadWifiSettings); + } + let ssid = std::str::from_utf8(ssid_bytes) + .map_err(|_| ParseError::BadUtf8)? + .to_owned(); + let password = std::str::from_utf8(pwd_bytes) + .map_err(|_| ParseError::BadUtf8)? + .to_owned(); + Ok(Command::SendWifiSettings { ssid, password }) +} + +fn parse_wifi_payload<'a>(input: &mut &'a [u8]) -> ModalResult<(&'a [u8], &'a [u8]), ContextError> { + let ssid_len = parse_u8.parse_next(input)?; + let ssid_bytes = take(ssid_len as usize).parse_next(input)?; + let pwd_len = parse_u8.parse_next(input)?; + let pwd_bytes = take(pwd_len as usize).parse_next(input)?; + Ok((ssid_bytes, pwd_bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn build(command_id: u8, data: &[u8]) -> Vec { + let mut packet = Vec::with_capacity(2 + data.len() + 1); + packet.push(command_id); + packet.push(data.len() as u8); + packet.extend_from_slice(data); + let cs = checksum(&packet); + packet.push(cs); + packet + } + + #[test] + fn empty_buffer_is_incomplete() { + assert_eq!(parse_packet(&[]), Err(ParseError::Incomplete { needed: 2 })); + assert_eq!( + parse_packet(&[0x02]), + Err(ParseError::Incomplete { needed: 1 }) + ); + } + + #[test] + fn header_only_is_incomplete() { + // Header says data_length=4 but no data or checksum. + assert_eq!( + parse_packet(&[0x01, 0x04]), + Err(ParseError::Incomplete { needed: 5 }) + ); + } + + #[test] + fn bad_checksum_is_rejected() { + let mut pkt = build(0x02, &[]); + let last = pkt.len() - 1; + pkt[last] = pkt[last].wrapping_add(1); + assert_eq!(parse_packet(&pkt), Err(ParseError::BadChecksum)); + } + + #[test] + fn identify_round_trip() { + let pkt = build(0x02, &[]); + let parsed = parse_packet(&pkt).unwrap(); + assert_eq!(parsed.command, Command::Identify); + assert_eq!(parsed.consumed, pkt.len()); + } + + #[test] + fn device_info_round_trip() { + let pkt = build(0x03, &[]); + assert_eq!(parse_packet(&pkt).unwrap().command, Command::DeviceInfo); + } + + #[test] + fn scan_round_trip() { + let pkt = build(0x04, &[]); + assert_eq!(parse_packet(&pkt).unwrap().command, Command::Scan); + } + + #[test] + fn wifi_settings_round_trip() { + let mut data = Vec::new(); + data.push(4); // ssid len + data.extend_from_slice(b"home"); + data.push(8); // pwd len + data.extend_from_slice(b"abcd1234"); + let pkt = build(0x01, &data); + let parsed = parse_packet(&pkt).unwrap(); + assert_eq!( + parsed.command, + Command::SendWifiSettings { + ssid: "home".into(), + password: "abcd1234".into(), + } + ); + } + + #[test] + fn wifi_settings_empty_password() { + let mut data = Vec::new(); + data.push(4); + data.extend_from_slice(b"open"); + data.push(0); + let pkt = build(0x01, &data); + assert_eq!( + parse_packet(&pkt).unwrap().command, + Command::SendWifiSettings { + ssid: "open".into(), + password: String::new(), + } + ); + } + + #[test] + fn wifi_settings_truncated_payload() { + let mut data = Vec::new(); + data.push(4); + data.extend_from_slice(b"home"); + // Missing password length. + let pkt = build(0x01, &data); + assert_eq!(parse_packet(&pkt), Err(ParseError::BadWifiSettings)); + } + + #[test] + fn wifi_settings_extra_trailing_bytes() { + let mut data = Vec::new(); + data.push(2); + data.extend_from_slice(b"hi"); + data.push(2); + data.extend_from_slice(b"pw"); + data.push(0xff); // junk + let pkt = build(0x01, &data); + assert_eq!(parse_packet(&pkt), Err(ParseError::BadWifiSettings)); + } + + #[test] + fn hostname_get_vs_set() { + assert_eq!( + parse_packet(&build(0x05, &[])).unwrap().command, + Command::GetHostname + ); + assert_eq!( + parse_packet(&build(0x05, b"my-host")).unwrap().command, + Command::SetHostname("my-host".into()) + ); + } + + #[test] + fn device_name_get_vs_set() { + assert_eq!( + parse_packet(&build(0x06, &[])).unwrap().command, + Command::GetDeviceName + ); + assert_eq!( + parse_packet(&build(0x06, "Frosty's Device".as_bytes())) + .unwrap() + .command, + Command::SetDeviceName("Frosty's Device".into()) + ); + } + + #[test] + fn unknown_command_id() { + let pkt = build(0x99, &[]); + assert_eq!(parse_packet(&pkt), Err(ParseError::UnknownCommand(0x99))); + } + + #[test] + fn identify_with_unexpected_data_is_malformed() { + let pkt = build(0x02, &[0xab]); + assert_eq!(parse_packet(&pkt), Err(ParseError::Malformed)); + } + + #[test] + fn invalid_utf8_in_hostname() { + let pkt = build(0x05, &[0xff, 0xfe, 0xfd]); + assert_eq!(parse_packet(&pkt), Err(ParseError::BadUtf8)); + } + + #[test] + fn extra_bytes_after_packet_are_left_for_caller() { + let mut buf = build(0x02, &[]); + buf.extend_from_slice(&[0xaa, 0xbb]); + let parsed = parse_packet(&buf).unwrap(); + assert_eq!(parsed.command, Command::Identify); + assert_eq!(parsed.consumed, buf.len() - 2); + } +} diff --git a/crates/improv-wifi/src/rpc/reassembly.rs b/crates/improv-wifi/src/rpc/reassembly.rs new file mode 100644 index 00000000..61d38dd9 --- /dev/null +++ b/crates/improv-wifi/src/rpc/reassembly.rs @@ -0,0 +1,164 @@ +use super::{ + Command, + parse::{ParseError, parse_packet}, +}; + +/// Buffers raw bytes from successive BLE writes and yields complete RPC packets. +/// +/// BLE writes carry up to one MTU (often ~20 bytes by default) per attribute write, but Improv-Wi-Fi +/// payloads can exceed that, so a client may split one logical packet across several writes. This +/// reassembler accumulates bytes until a full packet can be parsed, returning each as soon as it +/// becomes available. +#[derive(Debug, Default)] +pub struct Reassembler { + buf: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Yielded { + /// A full packet was decoded. + Command(Command), + /// The current packet was malformed; the buffer has been cleared so the next bytes start a + /// fresh packet. Inspect the inner [`ParseError`] to surface to the client. + Error(ParseError), +} + +impl Reassembler { + pub fn new() -> Self { + Self::default() + } + + /// Append bytes from a fresh BLE write and drain all complete packets. + pub fn feed(&mut self, bytes: &[u8]) -> Vec { + self.buf.extend_from_slice(bytes); + let mut out = Vec::new(); + loop { + match parse_packet(&self.buf) { + Ok(parsed) => { + self.buf.drain(..parsed.consumed); + out.push(Yielded::Command(parsed.command)); + } + Err(ParseError::Incomplete { .. }) => break, + Err(other) => { + self.buf.clear(); + out.push(Yielded::Error(other)); + break; + } + } + } + out + } + + #[cfg(test)] + fn buffered(&self) -> &[u8] { + &self.buf + } +} + +#[cfg(test)] +mod tests { + use super::super::checksum; + use super::*; + + fn build(command_id: u8, data: &[u8]) -> Vec { + let mut p = Vec::with_capacity(2 + data.len() + 1); + p.push(command_id); + p.push(data.len() as u8); + p.extend_from_slice(data); + let cs = checksum(&p); + p.push(cs); + p + } + + #[test] + fn single_packet_one_chunk() { + let mut r = Reassembler::new(); + let pkt = build(0x02, &[]); + let out = r.feed(&pkt); + assert_eq!(out, vec![Yielded::Command(Command::Identify)]); + assert!(r.buffered().is_empty()); + } + + #[test] + fn split_mid_header() { + let mut r = Reassembler::new(); + let pkt = build(0x02, &[]); + assert!(r.feed(&pkt[..1]).is_empty()); + let out = r.feed(&pkt[1..]); + assert_eq!(out, vec![Yielded::Command(Command::Identify)]); + } + + #[test] + fn split_mid_payload() { + let mut payload = Vec::new(); + payload.push(4); + payload.extend_from_slice(b"home"); + payload.push(4); + payload.extend_from_slice(b"abcd"); + let pkt = build(0x01, &payload); + + let mut r = Reassembler::new(); + // Feed in tiny chunks to exercise reassembly across many writes. + for chunk in pkt.chunks(3) { + let out = r.feed(chunk); + if !out.is_empty() { + assert_eq!( + out, + vec![Yielded::Command(Command::SendWifiSettings { + ssid: "home".into(), + password: "abcd".into() + })] + ); + assert!(r.buffered().is_empty()); + return; + } + } + panic!("never yielded a complete packet"); + } + + #[test] + fn two_packets_in_one_chunk() { + let mut r = Reassembler::new(); + let mut combined = build(0x02, &[]); + combined.extend_from_slice(&build(0x03, &[])); + let out = r.feed(&combined); + assert_eq!( + out, + vec![ + Yielded::Command(Command::Identify), + Yielded::Command(Command::DeviceInfo), + ] + ); + } + + #[test] + fn bad_checksum_clears_buffer() { + let mut r = Reassembler::new(); + let mut pkt = build(0x02, &[]); + let last = pkt.len() - 1; + pkt[last] = pkt[last].wrapping_add(1); + let out = r.feed(&pkt); + assert_eq!(out, vec![Yielded::Error(ParseError::BadChecksum)]); + assert!(r.buffered().is_empty()); + } + + #[test] + fn partial_packet_held_across_calls() { + let mut r = Reassembler::new(); + let pkt = build(0x05, b"hello"); + // Feed one byte at a time. + let mut yielded = None; + for (i, b) in pkt.iter().enumerate() { + let out = r.feed(std::slice::from_ref(b)); + if i + 1 < pkt.len() { + assert!(out.is_empty(), "yielded too early at byte {i}"); + } else { + yielded = Some(out); + } + } + assert_eq!( + yielded.unwrap(), + vec![Yielded::Command(Command::SetHostname("hello".into()))] + ); + } +} diff --git a/crates/improv-wifi/src/rpc/result.rs b/crates/improv-wifi/src/rpc/result.rs new file mode 100644 index 00000000..6d153b8a --- /dev/null +++ b/crates/improv-wifi/src/rpc/result.rs @@ -0,0 +1,64 @@ +use super::checksum; + +/// Encode an RPC Result packet for the given command id and string list. +/// +/// Wire format: +/// +/// ```text +/// [command_id] [data_length] [s1_len] [s1] [s2_len] [s2] ... [checksum] +/// ``` +/// +/// `data_length` is the total payload length (sum of `1 + s.len()` for each string). +pub fn encode_response(command_id: u8, strings: &[impl AsRef<[u8]>]) -> Vec { + let mut data: Vec = Vec::new(); + for s in strings { + let bytes = s.as_ref(); + assert!(bytes.len() <= u8::MAX as usize, "RPC string too long"); + data.push(bytes.len() as u8); + data.extend_from_slice(bytes); + } + assert!(data.len() <= u8::MAX as usize, "RPC payload too long"); + + let mut packet = Vec::with_capacity(2 + data.len() + 1); + packet.push(command_id); + packet.push(data.len() as u8); + packet.extend_from_slice(&data); + let cs = checksum(&packet); + packet.push(cs); + packet +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_response() { + let pkt = encode_response(0x05, &[] as &[&str]); + assert_eq!(pkt, vec![0x05, 0x00, 0x05]); + } + + #[test] + fn single_string_response() { + let pkt = encode_response(0x05, &["hi"]); + // 0x05, 0x03, 0x02, 'h', 'i', cs + // cs = 5 + 3 + 2 + 104 + 105 = 219 = 0xDB + assert_eq!(pkt, vec![0x05, 0x03, 0x02, b'h', b'i', 0xDB]); + } + + #[test] + fn multi_string_response() { + let pkt = encode_response(0x01, &["http://x", "extra"]); + // data = [8, "http://x", 5, "extra"] -> 1+8+1+5 = 15 bytes + assert_eq!(pkt[0], 0x01); + assert_eq!(pkt[1], 15); + // payload starts at index 2 + assert_eq!(&pkt[2..3], &[8u8]); + assert_eq!(&pkt[3..11], b"http://x"); + assert_eq!(&pkt[11..12], &[5u8]); + assert_eq!(&pkt[12..17], b"extra"); + // last byte is checksum over pkt[..17] + let cs = checksum(&pkt[..pkt.len() - 1]); + assert_eq!(*pkt.last().unwrap(), cs); + } +} diff --git a/crates/improv-wifi/src/service.rs b/crates/improv-wifi/src/service.rs new file mode 100644 index 00000000..e32fcd1b --- /dev/null +++ b/crates/improv-wifi/src/service.rs @@ -0,0 +1,314 @@ +use std::{sync::Arc, time::Duration}; + +use tokio::sync::{Mutex, broadcast, mpsc, watch::Sender as WatchSender}; +use tracing::{debug, instrument, warn}; +use zbus::{Connection, zvariant::OwnedObjectPath}; + +use crate::{ + Capabilities, Error, Status, WifiConfigurator, + bluez::{self, AppHandles}, + rpc::{Command, Reassembler, Yielded, encode_response}, +}; + +/// Whether the device requires explicit user authorisation before accepting credentials. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum AuthorizeMode { + /// Start in [`Status::AuthorizationRequired`]; caller must signal authorisation (button press, + /// etc.) via [`ImprovWifi::authorize`]. + Required, + + /// Start already in [`Status::Authorized`]. The authorisation timeout is not enforced. + #[default] + NotRequired, +} + +/// Top-level configuration for the Improv-Wi-Fi service. +#[derive(Debug, Default, Clone)] +pub struct ImprovWifiConfig { + /// How the device gates access to the credential-write commands. + pub authorize: AuthorizeMode, + + /// How long the device stays in [`Status::Authorized`] before reverting (only meaningful for + /// [`AuthorizeMode::Required`]). `None` (the default) disables the timeout — the device stays + /// authorised until provisioned or shut down. + pub auth_timeout: Option, + + /// Local name advertised over BLE. Defaults to the configurator's device name. + pub local_name: Option, +} + +#[derive(Debug)] +pub(crate) struct InnerState { + pub(crate) status: Status, + pub(crate) last_error: u8, // 0 = no error + pub(crate) rpc_result: Vec, +} + +/// Shared service state, held behind an `Arc` and mutated from BLE callbacks. +pub(crate) struct State { + pub(crate) inner: Mutex, + pub(crate) capabilities: Capabilities, + pub(crate) configurator: T, + pub(crate) reassembler: Mutex, + pub(crate) status_tx: broadcast::Sender, + pub(crate) error_tx: broadcast::Sender, + pub(crate) rpc_result_tx: broadcast::Sender>, + pub(crate) auth_reset_tx: WatchSender<()>, + pub(crate) provisioned_tx: WatchSender, + pub(crate) auth_required: bool, +} + +impl State +where + T: WifiConfigurator, +{ + pub(crate) async fn current_state_byte(&self) -> u8 { + self.inner.lock().await.status.as_byte() + } + + pub(crate) async fn error_byte(&self) -> u8 { + self.inner.lock().await.last_error + } + + pub(crate) async fn rpc_result_bytes(&self) -> Vec { + self.inner.lock().await.rpc_result.clone() + } + + pub(crate) async fn set_status(&self, new: Status) { + { + let mut inner = self.inner.lock().await; + if inner.status == new { + return; + } + inner.status = new; + } + let _ = self.status_tx.send(new); + if new == Status::Authorized { + let _ = self.auth_reset_tx.send(()); + } + if new == Status::Provisioned { + let _ = self.provisioned_tx.send(true); + } + } + + async fn set_error(&self, err: Option) { + let byte = err.map_or(0, Error::as_byte); + { + let mut inner = self.inner.lock().await; + if inner.last_error == byte { + return; + } + inner.last_error = byte; + } + let _ = self.error_tx.send(byte); + } + + async fn set_rpc_result(&self, bytes: Vec) { + { + let mut inner = self.inner.lock().await; + inner.rpc_result = bytes.clone(); + } + let _ = self.rpc_result_tx.send(bytes); + } + + #[instrument(level = "debug", skip(self, write))] + pub(crate) async fn handle_write(&self, write: Vec) { + let yielded = self.reassembler.lock().await.feed(&write); + for item in yielded { + match item { + Yielded::Command(cmd) => self.dispatch(cmd).await, + Yielded::Error(parse_err) => { + warn!(?parse_err, "RPC parse error"); + let mapped = match parse_err { + crate::rpc::ParseError::UnknownCommand(_) => Error::UnknownRPC, + _ => Error::InvalidRPC, + }; + self.set_error(Some(mapped)).await; + } + } + } + } + + async fn dispatch(&self, cmd: Command) { + debug!(?cmd, "RPC command"); + match cmd { + Command::Identify => { + self.set_error(None).await; + if let Err(err) = self.configurator.identify().await { + self.set_error(Some(err)).await; + } + } + Command::DeviceInfo => { + self.respond( + 0x03, + self.configurator + .device_info() + .await + .map(|i| i.into_strings()), + ) + .await + } + Command::Scan => { + let res = self.configurator.scan().await.map(|nets| { + let mut out = Vec::with_capacity(nets.len() * 3); + for n in nets { + out.push(n.ssid); + out.push(n.rssi.to_string()); + out.push(n.auth); + } + out + }); + self.respond(0x04, res).await; + } + Command::GetHostname => { + let res = self.configurator.get_hostname().await.map(|h| vec![h]); + self.respond(0x05, res).await; + } + Command::SetHostname(name) => { + if !self.is_authorized().await { + self.set_error(Some(Error::NotAuthorized)).await; + return; + } + let _ = self.auth_reset_tx.send(()); + match self.configurator.set_hostname(name.clone()).await { + Ok(()) => { + self.set_error(None).await; + self.set_rpc_result(encode_response(0x05, &[name])).await; + } + Err(err) => self.set_error(Some(err)).await, + } + } + Command::GetDeviceName => { + let res = self.configurator.get_device_name().await.map(|n| vec![n]); + self.respond(0x06, res).await; + } + Command::SetDeviceName(name) => { + if !self.is_authorized().await { + self.set_error(Some(Error::NotAuthorized)).await; + return; + } + let _ = self.auth_reset_tx.send(()); + match self.configurator.set_device_name(name.clone()).await { + Ok(()) => { + self.set_error(None).await; + self.set_rpc_result(encode_response(0x06, &[name])).await; + } + Err(err) => self.set_error(Some(err)).await, + } + } + Command::SendWifiSettings { ssid, password } => { + if !self.is_authorized().await { + self.set_error(Some(Error::NotAuthorized)).await; + return; + } + self.set_error(None).await; + self.set_status(Status::Provisioning).await; + match self.configurator.provision(ssid, password).await { + Ok(strings) => { + self.set_rpc_result(encode_response(0x01, &strings)).await; + self.set_status(Status::Provisioned).await; + } + Err(err) => { + self.set_error(Some(err)).await; + self.set_status(Status::Authorized).await; + } + } + } + } + } + + async fn respond(&self, command_id: u8, res: Result, Error>) { + match res { + Ok(strings) => { + self.set_error(None).await; + self.set_rpc_result(encode_response(command_id, &strings)) + .await; + } + Err(err) => self.set_error(Some(err)).await, + } + } + + async fn is_authorized(&self) -> bool { + matches!(self.inner.lock().await.status, Status::Authorized) + } +} + +/// Improv-Wi-Fi service handle. Construct via [`ImprovWifi::install`], then call +/// [`ImprovWifi::run`] to drive advertising, the authorisation timeout, and the +/// shutdown-on-`Provisioned` behaviour. +pub struct ImprovWifi { + state: Arc>, + handles: AppHandles, +} + +impl ImprovWifi +where + T: WifiConfigurator + 'static, +{ + /// Register the Improv-Wi-Fi GATT application + advertisement on the given BlueZ adapter. + /// + /// `connection` should be a system-bus connection. `adapter_path` is the BlueZ adapter + /// object path (typically `/org/bluez/hciN`); use [`find_adapter`] to discover one. + pub async fn install( + connection: Connection, + adapter_path: OwnedObjectPath, + configurator: T, + config: ImprovWifiConfig, + ) -> Result { + let handles = bluez::install(connection, adapter_path, configurator, config).await?; + let state = handles.state.clone(); + Ok(Self { state, handles }) + } + + /// Signal that the user has authorised the device (e.g. by pressing a button). + pub async fn authorize(&self) { + self.state.set_status(Status::Authorized).await; + } + + /// Get a cloneable handle that can signal authorisation from another task. + /// + /// The handle stays valid for the lifetime of the channel: triggers fired after + /// [`Self::run`] returns (i.e. after the service has shut down) are silently dropped. + pub fn auth_handle(&self) -> AuthHandle { + AuthHandle { + tx: self.handles.auth_tx.clone(), + } + } + + /// Drive the service until provisioning succeeds, then tear down BLE. + pub async fn run(self) -> Result<(), Error> { + bluez::run(self.handles).await + } +} + +/// Cloneable handle for triggering authorisation from another task. +/// +/// Obtain via [`ImprovWifi::auth_handle`]. Each call to [`Self::authorize`] drives the +/// service into [`Status::Authorized`] (subject to the existing auth-timeout behaviour). +#[derive(Clone, Debug)] +pub struct AuthHandle { + tx: mpsc::UnboundedSender<()>, +} + +impl AuthHandle { + /// Signal authorisation. No-op if the service has already shut down. + pub fn authorize(&self) { + let _ = self.tx.send(()); + } +} + +/// Resolve a BlueZ adapter object path. Pass `None` for the first adapter found. +pub async fn find_adapter( + connection: &Connection, + name: Option<&str>, +) -> Result { + bluez::find_adapter(connection, name).await +} + +/// Power on the BlueZ adapter at `adapter_path` (sets the `Powered` property to `true`). +pub async fn power_on_adapter( + connection: &Connection, + adapter_path: &OwnedObjectPath, +) -> Result<(), Error> { + bluez::power_on_adapter(connection, adapter_path).await +} diff --git a/crates/improv-wifi/src/state.rs b/crates/improv-wifi/src/state.rs new file mode 100644 index 00000000..0188a1dd --- /dev/null +++ b/crates/improv-wifi/src/state.rs @@ -0,0 +1,108 @@ +/// Improv Wi-Fi service state, as transmitted on the Current State characteristic. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum Status { + /// Awaiting authorisation via physical interaction with the device. + #[default] + AuthorizationRequired = 0x01, + + /// Ready to accept credentials. + Authorized = 0x02, + + /// Credentials received; attempting to connect. + Provisioning = 0x03, + + /// Connection successful. + Provisioned = 0x04, +} + +impl Status { + pub fn as_byte(self) -> u8 { + self as u8 + } +} + +/// Capabilities bitfield, as transmitted on the Capabilities characteristic. +/// +/// All four bits are independent. The Improv-Wi-Fi spec assigns: +/// - bit 0: identify command supported +/// - bit 1: device-info command supported +/// - bit 2: scan-Wi-Fi command supported +/// - bit 3: hostname command supported +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Capabilities { + pub identify: bool, + pub device_info: bool, + pub scan: bool, + pub hostname: bool, +} + +impl Capabilities { + pub const fn as_byte(self) -> u8 { + (self.identify as u8) + | ((self.device_info as u8) << 1) + | ((self.scan as u8) << 2) + | ((self.hostname as u8) << 3) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn status_byte_values() { + assert_eq!(Status::AuthorizationRequired.as_byte(), 0x01); + assert_eq!(Status::Authorized.as_byte(), 0x02); + assert_eq!(Status::Provisioning.as_byte(), 0x03); + assert_eq!(Status::Provisioned.as_byte(), 0x04); + assert_eq!(Status::default(), Status::AuthorizationRequired); + } + + #[test] + fn capabilities_byte_packing() { + assert_eq!(Capabilities::default().as_byte(), 0b0000); + assert_eq!( + Capabilities { + identify: true, + ..Default::default() + } + .as_byte(), + 0b0001 + ); + assert_eq!( + Capabilities { + device_info: true, + ..Default::default() + } + .as_byte(), + 0b0010 + ); + assert_eq!( + Capabilities { + scan: true, + ..Default::default() + } + .as_byte(), + 0b0100 + ); + assert_eq!( + Capabilities { + hostname: true, + ..Default::default() + } + .as_byte(), + 0b1000 + ); + assert_eq!( + Capabilities { + identify: true, + device_info: true, + scan: true, + hostname: true, + } + .as_byte(), + 0b1111 + ); + } +} diff --git a/services/iti-improv-wifi.service b/services/iti-improv-wifi.service new file mode 100644 index 00000000..ee155176 --- /dev/null +++ b/services/iti-improv-wifi.service @@ -0,0 +1,19 @@ +[Unit] +Description=Iti Improv-Wi-Fi BLE provisioning watcher +After=bluetooth.service NetworkManager.service +Requires=bluetooth.service NetworkManager.service + +[Service] +# The BCM pin number must match the button wired on your device. The button must short +# the pin to GND when pressed; the internal pull-up handles the rest. +# +# The daemon stays alive: a fresh device (no Wi-Fi profile) advertises immediately for +# first-boot provisioning; once provisioned, it stays idle until a long press (3s+) on +# the button re-enters provisioning mode. Short presses during a session authorise the +# in-progress credential write. +ExecStart=/usr/local/bin/bestool --log-timeless iti improv-wifi --auth-gpio 17 +Restart=always +RestartSec=10s + +[Install] +WantedBy=multi-user.target