diff --git a/Cargo.lock b/Cargo.lock index 8568a0a..626936d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,6 +129,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -201,7 +210,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -212,7 +221,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -473,6 +482,68 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -787,7 +858,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -820,6 +891,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -848,6 +928,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -1044,6 +1133,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1108,7 +1206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1189,12 +1287,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1202,7 +1315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1216,6 +1329,12 @@ dependencies = [ "syn", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1231,6 +1350,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.32" @@ -1479,6 +1607,25 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -1535,6 +1682,131 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1678,6 +1950,22 @@ dependencies = [ "serde_core", ] +[[package]] +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", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1798,6 +2086,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1880,6 +2174,21 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.8.0" @@ -1913,12 +2222,28 @@ dependencies = [ "bitflags 2.11.0", "block", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1929,6 +2254,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -1939,6 +2275,23 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "naga" version = "24.0.0" @@ -1961,6 +2314,23 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2019,6 +2389,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-traits" version = "0.2.19" @@ -2343,43 +2728,87 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "orbclient" -version = "0.3.50" +name = "openssl" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", "libc", - "libredox", + "once_cell", + "openssl-macros", + "openssl-sys", ] [[package]] -name = "ordered-float" -version = "4.6.0" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "num-traits", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "ordered-stream" -version = "0.2.0" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "owned_ttf_parser" -version = "0.25.1" +name = "openssl-sys" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ - "ttf-parser", -] - + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "orbclient" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "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 = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "parking" version = "2.2.1" @@ -2447,6 +2876,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.5" @@ -2506,6 +2941,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2664,12 +3105,85 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "renderdoc-sys" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower 0.5.3", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2714,7 +3228,40 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2727,11 +3274,30 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" name = "rustwave" version = "0.1.0" dependencies = [ + "anyhow", + "axum", + "bytes", "clap", "eframe", "hound", + "reqwest", + "serde", + "serde_json", + "tokio", + "tower 0.4.13", + "tower-http 0.5.2", + "tracing", + "tracing-appender", + "tracing-subscriber", + "uuid", ] +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2741,6 +3307,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2766,6 +3341,29 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -2815,6 +3413,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -2826,6 +3435,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2837,6 +3458,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2968,6 +3598,22 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -3023,6 +3669,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -3034,6 +3686,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3045,6 +3706,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -3052,10 +3734,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3107,6 +3789,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.11.3" @@ -3121,6 +3812,37 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3156,6 +3878,67 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -3186,6 +3969,79 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -3198,6 +4054,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -3216,8 +4084,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.25.1" @@ -3241,15 +4158,21 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -3274,6 +4197,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -3298,6 +4227,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3314,6 +4267,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3733,7 +4695,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3760,8 +4722,8 @@ checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ "windows-implement", "windows-interface", - "windows-result", - "windows-strings", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] @@ -3793,6 +4755,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -3802,16 +4775,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -4434,6 +5425,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index a073447..0d2c088 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,36 @@ name = "rustwave-cli" path = "src/main.rs" [dependencies] +# — existing — clap = { version = "4", features = ["derive"] } hound = "3" eframe = "0.31" + +# — NEW: async runtime (required for axum) — +tokio = { version = "1", features = ["full"] } + +# — NEW: HTTP server — +axum = { version = "0.7", features = ["multipart"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "limit"] } + +# — NEW: HTTP client (forward WAV to Broadcaster) — +reqwest = { version = "0.12", features = ["multipart", "json"] } + +# — NEW: serialisation — +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# — NEW: UUID generation for tx_id / queued_id — +uuid = { version = "1", features = ["v4", "serde"] } + +# — NEW: logging / tracing — +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-appender = "0.2" + +# — NEW: in-memory byte buffers (WAV bytes without temp files) — +bytes = "1" + +# — NEW: error propagation in run_server — +anyhow = "1" diff --git a/dev-check-strict.sh b/dev-check-strict.sh index 4856127..ba5cb19 100755 --- a/dev-check-strict.sh +++ b/dev-check-strict.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# dev-check.sh — full Rust quality gate +# dev-check.sh — full Rust quality gate (macOS) # Runs: fmt · fix · clippy (pedantic+nursery) · tests · audit · deny · dupes # Produces per-file clustered clippy reports in clippy_reports/ @@ -8,10 +8,21 @@ set -Eeuo pipefail # ─── Colours ────────────────────────────────────────────────────────────────── if [[ -t 1 ]]; then - RED='\033[0;31m'; YELLOW='\033[0;33m'; GREEN='\033[0;32m' - CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m' + RED='\033[0;31m' + YELLOW='\033[0;33m' + GREEN='\033[0;32m' + CYAN='\033[0;36m' + BOLD='\033[1m' + DIM='\033[2m' + RESET='\033[0m' else - RED=''; YELLOW=''; GREEN=''; CYAN=''; BOLD=''; DIM=''; RESET='' + RED='' + YELLOW='' + GREEN='' + CYAN='' + BOLD='' + DIM='' + RESET='' fi # ─── Globals ────────────────────────────────────────────────────────────────── @@ -29,7 +40,9 @@ SUMMARY_FILE="$REPORT_DIR/summary.txt" # ─── Helpers ────────────────────────────────────────────────────────────────── -command_exists() { command -v "$1" >/dev/null 2>&1; } +has_cmd() { + type -P "$1" >/dev/null 2>&1 +} step() { echo "" @@ -38,37 +51,28 @@ step() { pass() { echo -e " ${GREEN}✓${RESET} $1" - (( PASS_COUNT++ )) || true + PASS_COUNT=$(( PASS_COUNT + 1 )) } fail() { echo -e " ${RED}✗${RESET} $1" - (( FAIL_COUNT++ )) || true + FAIL_COUNT=$(( FAIL_COUNT + 1 )) FAILED_STEPS+=("$1") } skip() { echo -e " ${DIM}–${RESET} $1 ${DIM}(skipped — tool not installed)${RESET}" - (( SKIP_COUNT++ )) || true + SKIP_COUNT=$(( SKIP_COUNT + 1 )) } -warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } - -require_tool() { - local tool="$1" install_hint="$2" - if ! command_exists "$tool"; then - echo -e "${RED}Error:${RESET} required tool '${BOLD}$tool${RESET}' is not installed." - echo -e " Install with: ${DIM}$install_hint${RESET}" - exit 1 - fi +warn() { + echo -e " ${YELLOW}⚠${RESET} $1" } -optional_tool() { - local tool="$1" install_hint="$2" - if ! command_exists "$tool"; then - warn "'$tool' not installed — step will be skipped." - warn "Install with: $install_hint" - fi +die() { + echo -e "${RED}Error:${RESET} $1" + echo -e " Install with: ${DIM}$2${RESET}" + exit 1 } elapsed() { @@ -76,6 +80,23 @@ elapsed() { printf '%dm%02ds' $(( secs / 60 )) $(( secs % 60 )) } +# ─── Strip cargo noise from clippy output ───────────────────────────────────── + +filter_clippy() { + grep -Ev \ + '^[[:space:]]*(Compiling|Checking|Downloading|Updating|Fresh|Finished|Blocking|Locking|Dirty|Scraping|Running|Doctest)[[:space:]]' \ + | grep -Ev \ + '^[[:space:]]*= note: `#\[' \ + | grep -Ev \ + '^warning: [0-9]+ warning(s)? emitted' \ + | grep -Ev \ + '^error: aborting due to' \ + | grep -Ev \ + '^[[:space:]]*= note: for more information' \ + | sed '/^[[:space:]]*$/d' \ + || true +} + # ─── Header ─────────────────────────────────────────────────────────────────── echo -e "${BOLD}" @@ -84,30 +105,44 @@ echo "║ Rust Full Quality Gate Check ║" echo "╚══════════════════════════════════════════════════╝" echo -e "${RESET}" -# ─── CPU cores ──────────────────────────────────────────────────────────────── +# ─── CPU cores (macOS-aware) ────────────────────────────────────────────────── -if command_exists sysctl; then - export CARGO_BUILD_JOBS; CARGO_BUILD_JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 4) -elif command_exists nproc; then - export CARGO_BUILD_JOBS; CARGO_BUILD_JOBS=$(nproc) -else - export CARGO_BUILD_JOBS=4 -fi +export CARGO_BUILD_JOBS +CARGO_BUILD_JOBS=$(sysctl -n hw.logicalcpu 2>/dev/null || echo 4) echo -e " ${DIM}Using ${BOLD}${CARGO_BUILD_JOBS}${RESET}${DIM} CPU cores${RESET}" # ─── Required tools ─────────────────────────────────────────────────────────── step "Verifying required tools" -require_tool "cargo" "https://rustup.rs" -require_tool "rustfmt" "rustup component add rustfmt" -require_tool "clippy-driver" "rustup component add clippy" +if ! has_cmd cargo; then + die "required tool 'cargo' is not installed." "https://rustup.rs" +fi + +if ! has_cmd rustfmt; then + die "required tool 'rustfmt' is not installed." "rustup component add rustfmt" +fi + +# clippy-driver is not always on PATH on macOS — check via cargo subcommand instead +if ! cargo clippy --version >/dev/null 2>&1; then + die "required tool 'clippy' is not installed." "rustup component add clippy" +fi + pass "cargo · rustfmt · clippy all present" -optional_tool "cargo-audit" "cargo install cargo-audit" -optional_tool "cargo-deny" "cargo install cargo-deny" -optional_tool "cargo-udeps" "cargo install cargo-udeps" -optional_tool "cargo-msrv" "cargo install cargo-msrv" +# Optional tools — warn but do not exit +if ! has_cmd cargo-audit; then + warn "'cargo-audit' not installed — step will be skipped. cargo install cargo-audit" +fi +if ! has_cmd cargo-deny; then + warn "'cargo-deny' not installed — step will be skipped. cargo install cargo-deny" +fi +if ! has_cmd cargo-udeps; then + warn "'cargo-udeps' not installed — step will be skipped. cargo install cargo-udeps" +fi +if ! has_cmd cargo-msrv; then + warn "'cargo-msrv' not installed — step will be skipped. cargo install cargo-msrv" +fi # ─── Prepare report directory ───────────────────────────────────────────────── @@ -118,7 +153,11 @@ mkdir -p "$CLUSTER_DIR" if [[ "${1:-}" == "--update" ]]; then step "Updating dependency index" - if cargo update 2>&1; then pass "cargo update"; else fail "cargo update"; fi + if cargo update 2>&1; then + pass "cargo update" + else + fail "cargo update" + fi fi # ─── 1. Format ──────────────────────────────────────────────────────────────── @@ -131,7 +170,6 @@ else fail "cargo fmt --all" fi -# Verify nothing was left dirty (useful in CI) if git diff --quiet 2>/dev/null; then pass "No unstaged format changes" else @@ -153,42 +191,25 @@ fi step "3 · Lint (cargo clippy — pedantic + nursery)" CLIPPY_FLAGS=( - # Hard errors "-D" "warnings" - - # Pedantic: correctness, performance, style improvements "-W" "clippy::pedantic" - - # Nursery: newer lints, some may be noisy — catches subtle bugs early "-W" "clippy::nursery" - - # Catch common correctness bugs missed by the default set "-W" "clippy::correctness" "-W" "clippy::suspicious" "-W" "clippy::complexity" "-W" "clippy::perf" - - # Panic/unwrap hygiene — forces explicit error handling "-W" "clippy::unwrap_used" "-W" "clippy::expect_used" "-W" "clippy::panic" "-W" "clippy::todo" "-W" "clippy::unimplemented" "-W" "clippy::unreachable" - - # Index panic risk "-W" "clippy::indexing_slicing" - - # Integer overflow in casts "-W" "clippy::cast_possible_truncation" "-W" "clippy::cast_possible_wrap" "-W" "clippy::cast_sign_loss" "-W" "clippy::cast_precision_loss" - - # Arithmetic that can panic "-W" "clippy::arithmetic_side_effects" - - # Formatting / style discipline "-W" "clippy::format_collect" "-W" "clippy::uninlined_format_args" "-W" "clippy::redundant_closure_for_method_calls" @@ -209,6 +230,7 @@ CLIPPY_CMD=( cargo clippy --all-targets --all-features + --message-format=short -- "${CLIPPY_FLAGS[@]}" ) @@ -217,14 +239,17 @@ echo -e " ${DIM}Running: ${CLIPPY_CMD[*]}${RESET}" echo "" CLIPPY_EXIT=0 -"${CLIPPY_CMD[@]}" 2>&1 | tee "$RAW_FILE" || CLIPPY_EXIT=$? +"${CLIPPY_CMD[@]}" 2>&1 \ + | filter_clippy \ + | tee "$RAW_FILE" \ + || CLIPPY_EXIT=${PIPESTATUS[0]} -# ── Cluster clippy output by source file ───────────────────────────────────── +# ── Cluster clippy output by source file ────────────────────────────────────── echo "" echo -e " ${DIM}Clustering clippy output by file...${RESET}" -OUTFILE="" +CURRENT_OUTFILE="" while IFS= read -r line; do if [[ $line =~ ([a-zA-Z0-9_/.-]+\.rs):[0-9]+:[0-9]+ ]]; then FILE="${BASH_REMATCH[1]}" @@ -234,16 +259,16 @@ while IFS= read -r line; do else CLUSTER=$(echo "$DIR" | tr '/' '_') fi - OUTFILE="$CLUSTER_DIR/${CLUSTER}.txt" + CURRENT_OUTFILE="$CLUSTER_DIR/${CLUSTER}.txt" { echo "" echo "----------------------------------------" echo "FILE: $FILE" echo "----------------------------------------" - } >> "$OUTFILE" + } >> "$CURRENT_OUTFILE" fi - if [[ -n "$OUTFILE" ]]; then - echo "$line" >> "$OUTFILE" + if [[ -n "$CURRENT_OUTFILE" ]]; then + echo "$line" >> "$CURRENT_OUTFILE" fi done < "$RAW_FILE" @@ -273,7 +298,6 @@ TEST_EXIT=0 cargo test --all --all-features 2>&1 || TEST_EXIT=$? if [[ $TEST_EXIT -eq 0 ]]; then - PASSED=$(grep -oP '\d+(?= passed)' <<< "$(cargo test --all --all-features 2>&1)" | tail -1 || echo "?") pass "All tests passed" else fail "Test suite failed (exit $TEST_EXIT)" @@ -283,7 +307,7 @@ fi step "5 · Security audit (cargo audit)" -if command_exists cargo-audit; then +if has_cmd cargo-audit; then AUDIT_EXIT=0 cargo audit 2>&1 || AUDIT_EXIT=$? if [[ $AUDIT_EXIT -eq 0 ]]; then @@ -299,7 +323,7 @@ fi step "6 · Dependency policy (cargo deny)" -if command_exists cargo-deny; then +if has_cmd cargo-deny; then DENY_EXIT=0 cargo deny check 2>&1 || DENY_EXIT=$? if [[ $DENY_EXIT -eq 0 ]]; then @@ -315,7 +339,7 @@ fi step "7 · Unused dependencies (cargo udeps)" -if command_exists cargo-udeps; then +if has_cmd cargo-udeps; then UDEPS_EXIT=0 cargo +nightly udeps --all-targets 2>&1 || UDEPS_EXIT=$? if [[ $UDEPS_EXIT -eq 0 ]]; then @@ -331,7 +355,7 @@ fi step "8 · Minimum supported Rust version (cargo msrv)" -if command_exists cargo-msrv; then +if has_cmd cargo-msrv; then MSRV_EXIT=0 cargo msrv verify 2>&1 || MSRV_EXIT=$? if [[ $MSRV_EXIT -eq 0 ]]; then @@ -350,8 +374,8 @@ step "9 · Duplicate dependencies (cargo tree -d)" DUPES=$(cargo tree -d 2>&1 || true) if echo "$DUPES" | grep -q '\['; then warn "Duplicate crate versions detected:" - echo "$DUPES" | grep '^\[' | sort -u | while read -r line; do - echo -e " ${YELLOW}$line${RESET}" + echo "$DUPES" | grep '^\[' | sort -u | while IFS= read -r line; do + echo -e " ${YELLOW}${line}${RESET}" done else pass "No duplicate crate versions" @@ -382,7 +406,9 @@ TOTAL_SECS=$(( SECONDS - SCRIPT_START )) if [[ ${#FAILED_STEPS[@]} -gt 0 ]]; then echo "" echo "Failed steps:" - for s in "${FAILED_STEPS[@]}"; do echo " - $s"; done + for s in "${FAILED_STEPS[@]}"; do + echo " - $s" + done fi } | tee "$SUMMARY_FILE" @@ -407,4 +433,4 @@ else echo "" echo -e " ${DIM}Reports saved to: $REPORT_DIR/${RESET}" exit 1 -fi +fi \ No newline at end of file diff --git a/docs/RustWave-tree-annotated.txt b/docs/RustWave-tree-annotated.txt new file mode 100644 index 0000000..0d2a8b8 --- /dev/null +++ b/docs/RustWave-tree-annotated.txt @@ -0,0 +1,54 @@ +├── Cargo.lock +├── Cargo.toml +├── changelog.txt +├── deny.toml +├── dev-check-strict.sh +├── docs +│ ├── hamnet-relay-build-roadmap.md +│ └── rustwave_impl_plan.md +├── LICENSE +├── README.md +├── rustwave_api_implementation_plan.md +├── src +│ ├── api +│ │ ├── mod.rs # Router builder: full_router (wave + broadcast + chan routes), +│ │ │ # gui_router (broadcast + chan routes), run_server; 10 MB body limit +│ │ ├── models.rs # All JSON request/response structs; ChanCommand tagged enum +│ │ │ # (full_export, board_export, thread_export, archive_export, +│ │ │ # force_refresh, reply_push) matching ChanNet /chan/command API +│ │ ├── errors.rs # ApiError enum with IntoResponse impl +│ │ ├── state.rs # AppState (broadcaster_url, channet_url, wave_routes_enabled, +│ │ │ # incoming_queue); IncomingQueue; QueuedFile +│ │ ├── wave.rs # Handlers: GET /wave/status, POST /wave/encode, POST /wave/decode +│ │ │ # (serve mode only) +│ │ ├── broadcast.rs # Handlers: GET /broadcast/status, POST /broadcast/transmit, +│ │ │ # POST /broadcast/receive, GET /broadcast/incoming; +│ │ │ # pub forward_to_broadcaster() reused by chan.rs; +│ │ │ # ChanNet calls /broadcast/transmit to push ZIP snapshots and +│ │ │ # GET /broadcast/incoming to pull decoded inbound payloads +│ │ ├── chan.rs # ChanNet client + /chan/request proxy handler: +│ │ │ # check_channet_reachable() → GET /chan/status probe; +│ │ │ # send_chan_command() → POST /chan/command, returns raw ZIP bytes; +│ │ │ # POST /chan/request handler: receives ChanCommand JSON → +│ │ │ # forwards to ChanNet /chan/command → AFSK-encodes ZIP into WAV +│ │ │ # → calls forward_to_broadcaster() for over-the-air transmission +│ │ └── tests.rs # Unit test: queue enqueue/dequeue round-trip +│ ├── config.rs # Shared constants: sample rate (44 100 Hz), baud rate (1 200), +│ │ # MARK/SPACE frequencies, amplitude, preamble length & sync word +│ ├── decoder.rs # PCM → bytes: Goertzel-filter bit detection, clock-phase search, +│ │ # sync-word alignment, and frame extraction (with progress callback) +│ ├── encoder.rs # bytes → PCM: AFSK sine-wave synthesis at MARK/SPACE freqs, +│ │ # 50 ms silence padding, optional progress callback +│ ├── framer.rs # Wire-frame builder/parser: preamble + sync word + u16 filename +│ │ # length + filename + u32 payload length + payload + CRC-16/CCITT +│ ├── gui.rs # egui drag-and-drop front-end: auto-detects WAV vs. other file, +│ │ # runs encode/decode on a background thread, shows progress bar; +│ │ # spawns broadcast + chan API server thread before launching eframe +│ ├── logging.rs # Logging init: stderr INFO+ (human-readable) + rolling JSON file +│ │ # DEBUG+; respects RUSTWAVE_LOG env var +│ ├── main.rs # CLI entry point (clap): `gui`, `serve`, `encode -i … -o …`, +│ │ # `decode -i … [-o …]` +│ └── wav.rs # WAV I/O via hound: write normalised f64→i16 PCM; read i16 PCM→f64 +│ # (stereo accepted; only left channel used); +│ # write_to_bytes / read_from_bytes for in-memory API use +└── todo.txt \ No newline at end of file diff --git a/docs/RustWave_API_Reference.docx b/docs/RustWave_API_Reference.docx new file mode 100644 index 0000000..23fa67a Binary files /dev/null and b/docs/RustWave_API_Reference.docx differ diff --git a/src/api/broadcast.rs b/src/api/broadcast.rs new file mode 100644 index 0000000..a0d8b2d --- /dev/null +++ b/src/api/broadcast.rs @@ -0,0 +1,229 @@ +//! Handlers for the /broadcast/* channel network endpoints. +//! Exposed in both `serve` mode and `gui` mode. + +use axum::{ + extract::{Multipart, State}, + http::{header, StatusCode}, + response::{IntoResponse, Response}, + Json, +}; +use bytes::Bytes; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +use crate::{ + api::{ + errors::ApiError, + models::{BroadcastStatusResponse, QueueEmptyResponse, ReceiveResponse, TransmitResponse}, + state::{AppState, QueuedFile}, + }, + decoder, encoder, framer, wav, +}; + +// ── GET /broadcast/status ────────────────────────────────────────────────── + +pub async fn broadcast_status(State(state): State) -> Json { + let queue_depth = state.queue_depth().await; + let broadcaster_connected = check_broadcaster_reachable(&state.broadcaster_url).await; + let channet_connected = crate::api::chan::check_channet_reachable(&state.channet_url).await; + + info!( + queue_depth, + broadcaster_connected, + channet_connected, + broadcaster_url = %state.broadcaster_url, + channet_url = %state.channet_url, + "GET /broadcast/status" + ); + + Json(BroadcastStatusResponse { + service: "rustwave", + broadcaster_connected, + channet_connected, + broadcaster_url: state.broadcaster_url.clone(), + queue_depth, + }) +} + +async fn check_broadcaster_reachable(url: &str) -> bool { + reqwest::Client::new() + .get(url) + .timeout(std::time::Duration::from_secs(2)) + .send() + .await + .is_ok_and(|r| r.status().is_success()) +} + +// ── POST /broadcast/transmit ─────────────────────────────────────────────── + +pub async fn broadcast_transmit( + State(state): State, + mut multipart: Multipart, +) -> Result, ApiError> { + let (filename, file_bytes) = extract_file_field(&mut multipart).await?; + + info!(filename = %filename, input_bytes = file_bytes.len(), "POST /broadcast/transmit received file"); + + let wav_bytes: Vec = tokio::task::spawn_blocking({ + let filename = filename.clone(); + move || { + let framed = framer::frame(&file_bytes, &filename); + let samples = encoder::encode(&framed); + wav::write_to_bytes(&samples) + } + }) + .await + .map_err(|e| ApiError::Internal(format!("task panic: {e}")))? + .map_err(ApiError::EncodeFailed)?; + + let wav_size = wav_bytes.len(); + let tx_id = Uuid::new_v4(); + + info!(%tx_id, wav_bytes = wav_size, broadcaster_url = %state.broadcaster_url, + "POST /broadcast/transmit forwarding to Broadcaster"); + + forward_to_broadcaster(&state.broadcaster_url, &filename, wav_bytes, tx_id).await?; + + Ok(Json(TransmitResponse { + status: "ok", + tx_id, + wav_bytes: wav_size, + })) +} + +pub async fn forward_to_broadcaster( + broadcaster_url: &str, + original_filename: &str, + wav_bytes: Vec, + tx_id: Uuid, +) -> Result<(), ApiError> { + let stem = std::path::Path::new(original_filename) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + let wav_filename = format!("{stem}_encoded.wav"); + + let part = reqwest::multipart::Part::bytes(wav_bytes) + .file_name(wav_filename) + .mime_str("audio/wav") + .map_err(|e| ApiError::Internal(format!("mime error: {e}")))?; + let form = reqwest::multipart::Form::new().part("file", part); + + let resp = reqwest::Client::new() + .post(broadcaster_url) + .multipart(form) + .send() + .await + .map_err(|e| { + ApiError::BroadcasterUnavailable(format!( + "could not reach Broadcaster at {broadcaster_url}: {e}" + )) + })?; + + if !resp.status().is_success() { + return Err(ApiError::BroadcasterUnavailable(format!( + "Broadcaster returned HTTP {} for tx_id {tx_id}", + resp.status() + ))); + } + + debug!(%tx_id, "Broadcaster accepted WAV"); + Ok(()) +} + +// ── POST /broadcast/receive ──────────────────────────────────────────────── + +pub async fn broadcast_receive( + State(state): State, + mut multipart: Multipart, +) -> Result, ApiError> { + let (_filename, wav_bytes) = extract_file_field(&mut multipart).await?; + + info!( + wav_bytes = wav_bytes.len(), + "POST /broadcast/receive decoding WAV" + ); + + let decoded = tokio::task::spawn_blocking(move || { + let samples = wav::read_from_bytes(&wav_bytes)?; + decoder::decode(&samples) + }) + .await + .map_err(|e| ApiError::Internal(format!("task panic: {e}")))? + .map_err(ApiError::DecodeFailed)?; + + let decoded_size = decoded.data.len(); + let queued_id = Uuid::new_v4(); + + info!( + %queued_id, + original_filename = %decoded.filename, + decoded_bytes = decoded_size, + "POST /broadcast/receive queuing decoded file" + ); + + state + .enqueue(QueuedFile { + queued_id, + bytes: Bytes::from(decoded.data), + }) + .await; + + Ok(Json(ReceiveResponse { + status: "ok", + queued_id, + decoded_bytes: decoded_size, + })) +} + +// ── GET /broadcast/incoming ──────────────────────────────────────────────── + +pub async fn broadcast_incoming(State(state): State) -> Response { + if let Some(file) = state.dequeue().await { + info!(queued_id = %file.queued_id, bytes = file.bytes.len(), + "GET /broadcast/incoming dequeuing file"); + ( + StatusCode::OK, + [ + (header::CONTENT_TYPE, "application/octet-stream"), + ( + header::CONTENT_DISPOSITION, + "attachment; filename=\"snapshot.zip\"", + ), + ], + file.bytes, + ) + .into_response() + } else { + debug!("GET /broadcast/incoming queue is empty"); + (StatusCode::OK, Json(QueueEmptyResponse { status: "empty" })).into_response() + } +} + +// ── Shared helper ────────────────────────────────────────────────────────── + +async fn extract_file_field(multipart: &mut Multipart) -> Result<(String, Vec), ApiError> { + let Some(field) = multipart + .next_field() + .await + .map_err(|e| ApiError::BadRequest(format!("multipart error: {e}")))? + else { + return Err(ApiError::BadRequest( + "no file field found in multipart body".into(), + )); + }; + + let filename = field.file_name().unwrap_or("upload").to_string(); + let data = field + .bytes() + .await + .map_err(|e| ApiError::BadRequest(format!("could not read field bytes: {e}")))?; + + if data.is_empty() { + warn!(filename = %filename, "received empty file field"); + return Err(ApiError::BadRequest("file field is empty".into())); + } + + Ok((filename, data.to_vec())) +} diff --git a/src/api/chan.rs b/src/api/chan.rs new file mode 100644 index 0000000..d140891 --- /dev/null +++ b/src/api/chan.rs @@ -0,0 +1,105 @@ +//! `ChanNet` HTTP client and /chan/request proxy handler. +//! +//! `ChanNet` already calls `RustWave` on: +//! POST /broadcast/transmit — pushes a ZIP snapshot for AFSK encoding & over-air transmission +//! GET /broadcast/incoming — pulls decoded ZIP snapshots that arrived over radio +//! +//! This module adds the outbound direction: +//! POST /chan/request — operator sends a typed `ChanCommand`; `RustWave` forwards it to +//! `ChanNet`'s /chan/command, receives the ZIP response, AFSK-encodes +//! it into WAV, and calls `forward_to_broadcaster()` for transmission. + +use axum::{extract::State, Json}; +use tracing::info; +use uuid::Uuid; + +use crate::api::{ + errors::ApiError, + models::{ChanCommand, ChanRequestResponse}, + state::AppState, +}; +use crate::{encoder, framer, wav}; + +// ── Reachability probe ───────────────────────────────────────────────────── + +/// Hits `ChanNet`'s GET /chan/status. Used by `broadcast_status` to report +/// whether the paired `ChanNet` node is reachable. +pub async fn check_channet_reachable(channet_url: &str) -> bool { + reqwest::Client::new() + .get(format!("{channet_url}/chan/status")) + .timeout(std::time::Duration::from_secs(2)) + .send() + .await + .is_ok_and(|r| r.status().is_success()) +} + +// ── ChanNet command client ───────────────────────────────────────────────── + +/// POST a typed `ChanCommand` to `ChanNet`'s /chan/command endpoint. +/// Returns the raw ZIP bytes from the response body. +pub async fn send_chan_command( + channet_url: &str, + command: &ChanCommand, +) -> Result { + let resp = reqwest::Client::new() + .post(format!("{channet_url}/chan/command")) + .json(command) + .send() + .await + .map_err(|e| { + ApiError::BroadcasterUnavailable(format!("ChanNet unreachable at {channet_url}: {e}")) + })?; + + if !resp.status().is_success() { + return Err(ApiError::BroadcasterUnavailable(format!( + "ChanNet /chan/command returned HTTP {}", + resp.status() + ))); + } + + resp.bytes() + .await + .map_err(|e| ApiError::Internal(format!("reading ChanNet response body: {e}"))) +} + +// ── POST /chan/request ───────────────────────────────────────────────────── + +pub async fn chan_request( + State(state): State, + Json(command): Json, +) -> Result, ApiError> { + info!(?command, channet_url = %state.channet_url, "POST /chan/request"); + + // 1. Fetch ZIP from ChanNet. + let zip_bytes = send_chan_command(&state.channet_url, &command).await?; + let zip_len = zip_bytes.len(); + + // 2. AFSK-encode the ZIP into WAV bytes (CPU-bound; run on blocking thread). + let wav_bytes: Vec = tokio::task::spawn_blocking(move || { + let framed = framer::frame(&zip_bytes, "channet_payload.zip"); + let samples = encoder::encode(&framed); + wav::write_to_bytes(&samples) + }) + .await + .map_err(|e| ApiError::Internal(format!("task panic: {e}")))? + .map_err(ApiError::EncodeFailed)?; + + // 3. Forward the WAV to the external Broadcaster for over-air transmission. + // Reuses the same helper as /broadcast/transmit. + let tx_id = Uuid::new_v4(); + crate::api::broadcast::forward_to_broadcaster( + &state.broadcaster_url, + "channet_payload.zip", + wav_bytes, + tx_id, + ) + .await?; + + info!(%tx_id, zip_bytes = zip_len, "POST /chan/request transmitted successfully"); + + Ok(Json(ChanRequestResponse { + status: "transmitted", + tx_id, + zip_bytes: zip_len, + })) +} diff --git a/src/api/errors.rs b/src/api/errors.rs new file mode 100644 index 0000000..181ff7c --- /dev/null +++ b/src/api/errors.rs @@ -0,0 +1,71 @@ +//! API error type for `RustWave`. +//! +//! Every handler returns `Result<_, ApiError>`. axum automatically calls +//! `IntoResponse` on the error path. + +use crate::api::models::{ErrorDetail, ErrorEnvelope}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; + +#[derive(Debug)] +pub enum ApiError { + BadRequest(String), + EncodeFailed(String), + DecodeFailed(String), + BroadcasterUnavailable(String), + Internal(String), +} + +impl ApiError { + pub const fn code(&self) -> &'static str { + match self { + Self::BadRequest(_) => "BAD_REQUEST", + Self::EncodeFailed(_) => "ENCODE_FAILED", + Self::DecodeFailed(_) => "DECODE_FAILED", + Self::BroadcasterUnavailable(_) => "BROADCASTER_UNAVAILABLE", + Self::Internal(_) => "INTERNAL_ERROR", + } + } + + pub const fn status_code(&self) -> StatusCode { + match self { + Self::BadRequest(_) => StatusCode::BAD_REQUEST, + Self::EncodeFailed(_) | Self::DecodeFailed(_) => StatusCode::UNPROCESSABLE_ENTITY, + Self::BroadcasterUnavailable(_) => StatusCode::BAD_GATEWAY, + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = self.status_code(); + let message = match &self { + Self::BadRequest(m) + | Self::EncodeFailed(m) + | Self::DecodeFailed(m) + | Self::BroadcasterUnavailable(m) + | Self::Internal(m) => m.clone(), + }; + + tracing::error!( + code = self.code(), + http_status = status.as_u16(), + %message, + "api error" + ); + + let body = ErrorEnvelope { + error: ErrorDetail { + code: self.code().into(), + message, + status: status.as_u16(), + }, + }; + + (status, Json(body)).into_response() + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..986db9c --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,64 @@ +//! `RustWave` HTTP API server. +//! +//! `full_router()` — /wave/* + /broadcast/* + /chan/* (serve subcommand) +//! `gui_router()` — /broadcast/* + /chan/* (gui subcommand) + +pub mod broadcast; +pub mod chan; +pub mod errors; +pub mod models; +pub mod state; +pub mod wave; + +use axum::{routing::get, routing::post, Router}; +use std::net::SocketAddr; +use tower_http::limit::RequestBodyLimitLayer; +use tracing::info; + +use state::AppState; + +const BODY_LIMIT: usize = 10 * 1024 * 1024; // 10 MB + +pub fn full_router(state: AppState) -> Router { + let wave_routes = Router::new() + .route("/wave/status", get(wave::wave_status)) + .route("/wave/encode", post(wave::wave_encode)) + .route("/wave/decode", post(wave::wave_decode)); + + Router::new() + .merge(wave_routes) + .merge(broadcast_routes()) + .merge(chan_routes()) + .layer(RequestBodyLimitLayer::new(BODY_LIMIT)) + .with_state(state) +} + +pub fn gui_router(state: AppState) -> Router { + Router::new() + .merge(broadcast_routes()) + .merge(chan_routes()) + .layer(RequestBodyLimitLayer::new(BODY_LIMIT)) + .with_state(state) +} + +fn broadcast_routes() -> Router { + Router::new() + .route("/broadcast/status", get(broadcast::broadcast_status)) + .route("/broadcast/transmit", post(broadcast::broadcast_transmit)) + .route("/broadcast/receive", post(broadcast::broadcast_receive)) + .route("/broadcast/incoming", get(broadcast::broadcast_incoming)) +} + +fn chan_routes() -> Router { + Router::new().route("/chan/request", post(chan::chan_request)) +} + +pub async fn run_server(router: Router, bind_addr: SocketAddr) -> anyhow::Result<()> { + let listener = tokio::net::TcpListener::bind(bind_addr).await?; + info!(addr = %bind_addr, "RustWave API server listening"); + axum::serve(listener, router).await?; + Ok(()) +} + +#[cfg(test)] +pub mod tests; diff --git a/src/api/models.rs b/src/api/models.rs new file mode 100644 index 0000000..d1ad9c1 --- /dev/null +++ b/src/api/models.rs @@ -0,0 +1,96 @@ +//! JSON request and response types for the `RustWave` API. + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// ── /wave/* responses ────────────────────────────────────────────────────── + +#[derive(Serialize)] +pub struct WaveStatusResponse { + pub service: &'static str, + pub codec: &'static str, + pub version: &'static str, +} + +// ── /broadcast/* responses ───────────────────────────────────────────────── + +#[derive(Serialize)] +pub struct BroadcastStatusResponse { + pub service: &'static str, + pub broadcaster_connected: bool, + pub channet_connected: bool, + pub broadcaster_url: String, + pub queue_depth: usize, +} + +#[derive(Serialize)] +pub struct TransmitResponse { + pub status: &'static str, + pub tx_id: Uuid, + pub wav_bytes: usize, +} + +#[derive(Serialize)] +pub struct ReceiveResponse { + pub status: &'static str, + pub queued_id: Uuid, + pub decoded_bytes: usize, +} + +/// Returned by GET /broadcast/incoming when the queue is empty. +#[derive(Serialize, Deserialize)] +pub struct QueueEmptyResponse { + pub status: &'static str, +} + +// ── ChanNet /chan/command request types ──────────────────────────────────── +// +// Mirrors the six commands defined in the ChanNet API reference exactly. +// The `type` field is serialised as the serde tag so the JSON sent to +// /chan/command matches the format ChanNet expects. + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ChanCommand { + /// All boards + all active (non-archived) posts. Optional delta via `since`. + FullExport { since: Option }, + /// All active posts on a single board. Optional delta via `since`. + BoardExport { board: String, since: Option }, + /// All posts in a single thread. Optional delta via `since`. + ThreadExport { thread_id: u64, since: Option }, + /// All archived threads + posts for a single board. Always a full export. + ArchiveExport { board: String }, + /// Entire database — all boards, threads, archives, posts. Use for initial + /// sync or recovery only; `RustChan` logs a warning when this is received. + ForceRefresh, + /// Post a new reply to an existing thread (the only write command). + ReplyPush { + board: String, + thread_id: u64, + author: String, + content: String, + timestamp: u64, + }, +} + +/// Returned by POST /chan/request on success. +#[derive(Serialize)] +pub struct ChanRequestResponse { + pub status: &'static str, // "transmitted" + pub tx_id: uuid::Uuid, + pub zip_bytes: usize, +} + +// ── Error envelope ───────────────────────────────────────────────────────── + +#[derive(Serialize)] +pub struct ErrorDetail { + pub code: String, + pub message: String, + pub status: u16, +} + +#[derive(Serialize)] +pub struct ErrorEnvelope { + pub error: ErrorDetail, +} diff --git a/src/api/state.rs b/src/api/state.rs new file mode 100644 index 0000000..189b71e --- /dev/null +++ b/src/api/state.rs @@ -0,0 +1,52 @@ +//! Shared state for the `RustWave` API server. + +use bytes::Bytes; +use std::{collections::VecDeque, sync::Arc}; +use tokio::sync::Mutex; +use uuid::Uuid; + +#[derive(Debug)] +pub struct QueuedFile { + pub queued_id: Uuid, + pub bytes: Bytes, +} + +pub type IncomingQueue = Arc>>; + +#[derive(Clone)] +pub struct AppState { + pub broadcaster_url: String, + pub channet_url: String, + #[allow(dead_code)] + pub wave_routes_enabled: bool, + pub incoming_queue: IncomingQueue, +} + +impl AppState { + pub fn new(wave_routes_enabled: bool) -> Self { + let broadcaster_url = std::env::var("RUSTWAVE_BROADCASTER_URL") + .unwrap_or_else(|_| "http://localhost:9090".to_string()); + + let channet_url = std::env::var("RUSTWAVE_CHANNET_URL") + .unwrap_or_else(|_| "http://localhost:7070".to_string()); + + Self { + broadcaster_url, + channet_url, + wave_routes_enabled, + incoming_queue: Arc::new(Mutex::new(VecDeque::new())), + } + } + + pub async fn queue_depth(&self) -> usize { + self.incoming_queue.lock().await.len() + } + + pub async fn enqueue(&self, file: QueuedFile) { + self.incoming_queue.lock().await.push_back(file); + } + + pub async fn dequeue(&self) -> Option { + self.incoming_queue.lock().await.pop_front() + } +} diff --git a/src/api/tests.rs b/src/api/tests.rs new file mode 100644 index 0000000..001ab02 --- /dev/null +++ b/src/api/tests.rs @@ -0,0 +1,50 @@ +//! Integration tests for the `RustWave` API layer. + +use crate::api::state::{AppState, QueuedFile}; +use bytes::Bytes; +use uuid::Uuid; + +#[tokio::test] +async fn queue_enqueue_dequeue_roundtrip() { + let state = AppState::new(false); + assert_eq!(state.queue_depth().await, 0); + + state + .enqueue(QueuedFile { + queued_id: Uuid::new_v4(), + bytes: Bytes::from_static(b"api-test"), + }) + .await; + + assert_eq!(state.queue_depth().await, 1); + assert!(state.dequeue().await.is_some()); + assert!(state.dequeue().await.is_none()); +} + +#[tokio::test] +async fn queue_preserves_fifo_order() { + let state = AppState::new(true); + + for i in 0u8..3 { + state + .enqueue(QueuedFile { + queued_id: Uuid::new_v4(), + bytes: Bytes::from(vec![i]), + }) + .await; + } + + assert_eq!(state.queue_depth().await, 3); + for expected in 0u8..3 { + let dequeued = state.dequeue().await; + assert!(dequeued.is_some(), "queue should have an item"); + if let Some(file) = dequeued { + assert_eq!( + file.bytes.first().copied(), + Some(expected), + "bytes non-empty" + ); + } + } + assert_eq!(state.queue_depth().await, 0); +} diff --git a/src/api/wave.rs b/src/api/wave.rs new file mode 100644 index 0000000..fc40035 --- /dev/null +++ b/src/api/wave.rs @@ -0,0 +1,128 @@ +//! Handlers for the /wave/* general-purpose codec endpoints. +//! Only registered in `serve` mode — NOT in GUI mode. + +use axum::{ + extract::Multipart, + http::header, + response::{IntoResponse, Response}, + Json, +}; +use tracing::{info, warn}; + +use crate::{ + api::{errors::ApiError, models::WaveStatusResponse}, + decoder, encoder, framer, wav, +}; + +// ── GET /wave/status ─────────────────────────────────────────────────────── + +pub async fn wave_status() -> Json { + info!("GET /wave/status"); + Json(WaveStatusResponse { + service: "rustwave", + codec: "afsk-1200", + version: env!("CARGO_PKG_VERSION"), + }) +} + +// ── POST /wave/encode ────────────────────────────────────────────────────── + +pub async fn wave_encode(mut multipart: Multipart) -> Result { + let (filename, file_bytes) = extract_file_field(&mut multipart).await?; + + info!(filename = %filename, input_bytes = file_bytes.len(), "POST /wave/encode starting"); + + let result = tokio::task::spawn_blocking(move || { + let framed = framer::frame(&file_bytes, &filename); + let samples = encoder::encode(&framed); + let wav_bytes = wav::write_to_bytes(&samples)?; + Ok::<(String, Vec), String>((filename, wav_bytes)) + }) + .await + .map_err(|e| ApiError::Internal(format!("task panic: {e}")))? + .map_err(ApiError::EncodeFailed)?; + + let (original_filename, wav_bytes) = result; + let stem = std::path::Path::new(&original_filename) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + let out_name = format!("{stem}_encoded.wav"); + + info!(output_filename = %out_name, wav_bytes = wav_bytes.len(), "POST /wave/encode complete"); + + Ok(( + [ + (header::CONTENT_TYPE, "audio/wav"), + ( + header::CONTENT_DISPOSITION, + &format!("attachment; filename=\"{out_name}\""), + ), + ], + wav_bytes, + ) + .into_response()) +} + +// ── POST /wave/decode ────────────────────────────────────────────────────── + +pub async fn wave_decode(mut multipart: Multipart) -> Result { + let (_field_name, wav_bytes) = extract_file_field(&mut multipart).await?; + + info!(wav_bytes = wav_bytes.len(), "POST /wave/decode starting"); + + let result = tokio::task::spawn_blocking(move || { + let samples = wav::read_from_bytes(&wav_bytes)?; + let decoded = decoder::decode(&samples)?; + Ok::(decoded) + }) + .await + .map_err(|e| ApiError::Internal(format!("task panic: {e}")))? + .map_err(ApiError::DecodeFailed)?; + + info!( + original_filename = %result.filename, + decoded_bytes = result.data.len(), + "POST /wave/decode complete" + ); + + Ok(( + [ + (header::CONTENT_TYPE, "application/octet-stream"), + ( + header::CONTENT_DISPOSITION, + &format!("attachment; filename=\"{}\"", result.filename), + ), + ], + result.data, + ) + .into_response()) +} + +// ── Shared helper ────────────────────────────────────────────────────────── + +async fn extract_file_field(multipart: &mut Multipart) -> Result<(String, Vec), ApiError> { + let Some(field) = multipart + .next_field() + .await + .map_err(|e| ApiError::BadRequest(format!("multipart error: {e}")))? + else { + return Err(ApiError::BadRequest( + "no file field found in multipart body".into(), + )); + }; + + let filename = field.file_name().unwrap_or("upload").to_string(); + let data = field + .bytes() + .await + .map_err(|e| ApiError::BadRequest(format!("could not read field bytes: {e}")))?; + + if data.is_empty() { + warn!(filename = %filename, "received empty file field"); + return Err(ApiError::BadRequest("file field is empty".into())); + } + + Ok((filename, data.to_vec())) +} diff --git a/src/gui.rs b/src/gui.rs index bd3aac1..1445172 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -451,6 +451,35 @@ fn dashed_border(painter: &egui::Painter, rect: Rect, stroke: Stroke) { // ─── Entry point ───────────────────────────────────────────────────────────── pub fn run() -> eframe::Result<()> { + // Spawn the /broadcast/* API server on a background OS thread. + std::thread::spawn(|| { + let addr: std::net::SocketAddr = std::env::var("RUSTWAVE_BIND") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| std::net::SocketAddr::from(([127, 0, 0, 1], 7071))); + + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + tracing::error!("failed to build Tokio runtime for GUI API server: {e}"); + return; + } + }; + + rt.block_on(async move { + let state = crate::api::state::AppState::new(false); + let router = crate::api::gui_router(state); + if let Err(e) = crate::api::run_server(router, addr).await { + tracing::error!("GUI API server error: {e}"); + } + }); + }); + + tracing::info!("GUI mode: /broadcast/* API started on 127.0.0.1:7071"); + let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([480.0, 340.0]) diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..2be3e8a --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,49 @@ +//! Logging initialisation for `RustWave`. +//! +//! Call `logging::init()` once at the start of `main()`. +//! +//! Log output: +//! - stderr: INFO and above, human-readable +//! - rustwave.log (file): DEBUG and above, JSON format, rolling daily +//! +//! The log file is written next to the binary. +//! Set `RUSTWAVE_LOG`=debug to see debug output on stderr too. + +use std::path::PathBuf; +use tracing_appender::rolling; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; + +/// Initialise logging. Must be called once before any tracing macros are used. +/// +/// Returns the `_guard` from `tracing_appender::non_blocking`. The caller MUST +/// hold this value for the lifetime of the process; dropping it flushes and +/// closes the log file. +pub fn init() -> tracing_appender::non_blocking::WorkerGuard { + let log_dir: PathBuf = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(PathBuf::from)) + .unwrap_or_else(|| PathBuf::from(".")); + + // Rolling daily log file: rustwave.YYYY-MM-DD + let file_appender = rolling::daily(&log_dir, "rustwave.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + + // stderr layer — human readable, INFO+ by default, respects RUSTWAVE_LOG + let stderr_filter = + EnvFilter::try_from_env("RUSTWAVE_LOG").unwrap_or_else(|_| EnvFilter::new("info")); + + let stderr_layer = fmt::layer().with_target(false).with_filter(stderr_filter); + + // file layer — JSON, DEBUG+ + let file_layer = fmt::layer() + .json() + .with_writer(non_blocking) + .with_filter(EnvFilter::new("debug")); + + tracing_subscriber::registry() + .with(stderr_layer) + .with(file_layer) + .init(); + + guard +} diff --git a/src/main.rs b/src/main.rs index 3483e84..01a1191 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ +mod api; mod config; mod decoder; mod encoder; mod framer; mod gui; +mod logging; mod wav; use clap::{Parser, Subcommand}; -use std::path::PathBuf; +use std::{net::SocketAddr, path::PathBuf}; #[derive(Parser)] #[command(name = "rustwave-cli", version, about = "RustWave audio codec", long_about = None)] @@ -17,27 +19,31 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Launch the drag-and-drop GUI + /// Launch the drag-and-drop GUI (also starts /broadcast/* API on 127.0.0.1:7071) Gui, - /// Encode a file into an AFSK WAV (original filename is stored in the signal) + /// Start the HTTP API server (both /wave/* and /broadcast/* on 127.0.0.1:7071) + Serve { + #[arg(short, long, value_name = "ADDR")] + bind: Option, + }, + /// Encode a file into an AFSK WAV Encode { #[arg(short, long, value_name = "FILE")] input: PathBuf, #[arg(short, long, value_name = "FILE")] output: PathBuf, }, - /// Decode an AFSK WAV — restores the original filename automatically. - /// If -o is omitted the file is written next to the WAV with its original name. + /// Decode an AFSK WAV — restores the original filename automatically Decode { #[arg(short, long, value_name = "FILE")] input: PathBuf, - /// Output path (optional — defaults to original filename next to the WAV) #[arg(short, long, value_name = "FILE")] output: Option, }, } fn main() { + let _log_guard = logging::init(); if let Err(e) = run() { eprintln!("error: {e}"); std::process::exit(1); @@ -52,22 +58,39 @@ fn run() -> Result<(), String> { gui::run().map_err(|e| format!("GUI error: {e}"))?; } + Command::Serve { bind } => { + let addr = bind + .or_else(|| { + std::env::var("RUSTWAVE_BIND") + .ok() + .and_then(|s| s.parse().ok()) + }) + .unwrap_or_else(|| std::net::SocketAddr::from(([127, 0, 0, 1], 7071))); + + let rt = tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to build Tokio runtime: {e}"))?; + + rt.block_on(async move { + let state = api::state::AppState::new(true); + let router = api::full_router(state); + api::run_server(router, addr) + .await + .map_err(|e| format!("server error: {e}")) + })?; + } + Command::Encode { input, output } => { let data = std::fs::read(&input) .map_err(|e| format!("cannot read '{}': {e}", input.display()))?; - let filename = input .file_name() .unwrap_or_default() .to_string_lossy() .into_owned(); - let framed = framer::frame(&data, &filename); let samples = encoder::encode(&framed); - wav::write(&output, &samples) .map_err(|e| format!("cannot write '{}': {e}", output.display()))?; - #[allow(clippy::cast_precision_loss)] let duration = samples.len() as f64 / f64::from(config::SAMPLE_RATE); eprintln!( @@ -75,32 +98,28 @@ fn run() -> Result<(), String> { filename, data.len(), plural(data.len()), - output.display(), + output.display() ); } Command::Decode { input, output } => { let samples = wav::read(&input).map_err(|e| format!("cannot read '{}': {e}", input.display()))?; - let decoded = decoder::decode(&samples).map_err(|e| format!("decode failed: {e}"))?; - let out_path = output.unwrap_or_else(|| { input .parent() .unwrap_or_else(|| std::path::Path::new(".")) .join(&decoded.filename) }); - std::fs::write(&out_path, &decoded.data) .map_err(|e| format!("cannot write '{}': {e}", out_path.display()))?; - eprintln!( "decoded {} byte{} -> '{}' (original filename: '{}')", decoded.data.len(), plural(decoded.data.len()), out_path.display(), - decoded.filename, + decoded.filename ); } } diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..bfb7b5e --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,24 @@ +#[cfg(test)] +mod tests { + use crate::api::state::AppState; + + #[tokio::test] + async fn queue_enqueue_dequeue() { + use crate::api::state::QueuedFile; + use bytes::Bytes; + use uuid::Uuid; + + let state = AppState::new(false); + assert_eq!(state.queue_depth().await, 0); + + state.enqueue(QueuedFile { + queued_id: Uuid::new_v4(), + bytes: Bytes::from_static(b"hello"), + }).await; + + assert_eq!(state.queue_depth().await, 1); + let file = state.dequeue().await.unwrap(); + assert_eq!(file.bytes.as_ref(), b"hello"); + assert!(state.dequeue().await.is_none()); + } +} diff --git a/src/wav.rs b/src/wav.rs index 67a6d2a..abd63b4 100644 --- a/src/wav.rs +++ b/src/wav.rs @@ -53,6 +53,58 @@ pub fn read(path: &Path) -> Result, String> { } } +// ── In-memory variants used by the HTTP API ────────────────────────────── + +pub fn write_to_bytes(samples: &[f64]) -> Result, String> { + use std::io::Cursor; + let spec = hound::WavSpec { + channels: 1, + sample_rate: SAMPLE_RATE, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + let mut buf: Vec = Vec::new(); + let cursor = Cursor::new(&mut buf); + let mut writer = hound::WavWriter::new(cursor, spec).map_err(|e| e.to_string())?; + + for &s in samples { + #[allow(clippy::cast_possible_truncation)] + let v = (s.clamp(-1.0, 1.0) * 32_767.0) as i16; + writer.write_sample(v).map_err(|e| e.to_string())?; + } + + writer.finalize().map_err(|e| e.to_string())?; + Ok(buf) +} + +pub fn read_from_bytes(data: &[u8]) -> Result, String> { + use std::io::Cursor; + let cursor = Cursor::new(data); + let mut reader = hound::WavReader::new(cursor).map_err(|e| e.to_string())?; + let spec = reader.spec(); + + match (spec.bits_per_sample, spec.sample_format) { + (16, hound::SampleFormat::Int) => { + let channels = usize::from(spec.channels); + if channels == 0 { + return Err("invalid WAV: 0 channels".into()); + } + reader + .samples::() + .step_by(channels) + .map(|s| { + s.map(|v| f64::from(v) / 32_768.0) + .map_err(|e| e.to_string()) + }) + .collect() + } + (bits, fmt) => Err(format!( + "unsupported WAV format: {bits}-bit {fmt:?} (rustwave-cli expects 16-bit integer PCM)" + )), + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -112,4 +164,20 @@ mod tests { } Ok(()) } + + #[test] + fn memory_round_trip() -> Result<(), String> { + use std::f64::consts::TAU; + #[allow(clippy::cast_precision_loss)] + let original: Vec = (0..4_410_i32) + .map(|i| 0.5 * (TAU * 440.0 * f64::from(i) / 44_100.0).sin()) + .collect(); + let bytes = write_to_bytes(&original)?; + let recovered = read_from_bytes(&bytes)?; + assert_eq!(original.len(), recovered.len()); + for (a, b) in original.iter().zip(recovered.iter()) { + assert!((a - b).abs() < 5e-5, "quantisation error: {a} vs {b}"); + } + Ok(()) + } }