diff --git a/Cargo.lock b/Cargo.lock index 931f536..9f0cf0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,26 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures", +] + +[[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_system_properties" version = "0.1.5" @@ -11,12 +31,28 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base62" +version = "0.1.0" +dependencies = [ + "aes", + "cbc", + "regex", + "sha2", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -29,6 +65,24 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[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 = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -55,7 +109,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -64,6 +118,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cbc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.54" @@ -92,6 +155,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "chrono" version = "0.4.44" @@ -105,12 +179,63 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[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 = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -123,6 +248,35 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -135,6 +289,33 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -159,6 +340,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.13.0" @@ -167,8 +354,26 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "js-sys" version = "0.3.85" @@ -179,6 +384,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.185" @@ -223,7 +434,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -264,6 +475,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -290,18 +511,24 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rancor" version = "0.1.1" @@ -311,6 +538,23 @@ dependencies = [ "ptr_meta", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -320,6 +564,35 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rend" version = "0.5.3" @@ -356,7 +629,7 @@ checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -371,6 +644,65 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -430,9 +762,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -456,7 +788,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -474,12 +806,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unshell" version = "0.1.0" @@ -490,6 +834,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ush-obfuscate" +version = "0.1.0" +dependencies = [ + "base62", + "block-padding", + "getrandom", + "hex", + "hex-literal", + "proc-macro2", + "quote", + "rand", + "static_init", + "syn 2.0.117", +] + [[package]] name = "uuid" version = "1.22.0" @@ -500,6 +860,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -532,7 +910,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -545,6 +923,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi" version = "0.3.9" @@ -588,7 +1000,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -599,7 +1011,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -625,3 +1037,103 @@ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 0c115d9..156d994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,7 @@ [workspace] members = [ - # "ush-router", - # "ush-payload", - # "ush-cli", - # "ush-obfuscate", - # "base62", "no-alloc-network-test", + "ush-obfuscate", + "base62", ] resolver = "2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d4f85e --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# UnShell + +UnShell is a tree-addressed RPC and data-exchange protocol designed for a hierarchy of endpoints. It provides a lightweight way to route calls, streams, and faults through a tree-structured network of nodes. + +## Features + +- **Tree-Based Routing**: Path-based addressing with longest-prefix matching. +- **RPC and Streaming**: Supports simple request-response (Call) and bidirectional streaming (Data). +- **Hierarchical Control**: Authority-restricted downward calls and upstream-only faults. +- **Introspection**: Mandatory discovery mechanism for endpoints and leaves. +- **no_std Compatible**: Core protocol implementation is designed for constrained environments. + +## Architecture + +The protocol is organized into: +- `unshell`: Core protocol logic, framing, and routing. +- `ush-treetest`: A testbed implementation for validating the protocol. +- `ush-obfuscate`: Utilities for code obfuscation. +- `base62`: Encoding utilities. + +## Getting Started + +### Build +```bash +cargo build +``` + +### Run Tests +```bash +cargo test +``` + +## Protocol Specification +For detailed information on the wire format and behavioral invariants, please refer to `PROTOCOL.md`. diff --git a/base62/Cargo.toml b/base62/Cargo.toml index 4414f72..4f1cd43 100644 --- a/base62/Cargo.toml +++ b/base62/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -aes = "0.8.4" -cbc = "0.1.2" +aes = "0.9.0" +cbc = "0.2.0" regex = "1.12.3" -sha2 = "0.10.9" +sha2 = "0.11.0" diff --git a/base62/src/aes.rs b/base62/src/aes.rs index 72b0936..83507a1 100644 --- a/base62/src/aes.rs +++ b/base62/src/aes.rs @@ -1,5 +1,5 @@ use crate::{base62::Base62, hash}; -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit}; use cbc::cipher::block_padding::Pkcs7; use regex::Regex; @@ -38,7 +38,7 @@ pub fn encrypt_aes(plaintext: &str, key_str: &str, iv: [u8; 16]) -> String { buf[..pt_len].copy_from_slice(&plaintext); let mut ct = cbc::Encryptor::::new(&key_salted.into(), &iv.into()) - .encrypt_padded_mut::(&mut buf, pt_len) + .encrypt_padded::(&mut buf, pt_len) .unwrap() .to_vec(); @@ -73,7 +73,7 @@ pub fn decrypt_aes(input: &str, key_str: &str, iv: [u8; 16]) -> Result::new(&key.into(), &iv.into()) - .decrypt_padded_mut::(&mut buf) + .decrypt_padded::(&mut buf) .map_err(|_| "decryption failed".to_string())?; Ok(String::from_utf8_lossy(pt).to_string()) diff --git a/src/protocol/codec.rs b/src/protocol/codec.rs index 644ec55..8c294f7 100644 --- a/src/protocol/codec.rs +++ b/src/protocol/codec.rs @@ -211,6 +211,10 @@ where } fn align_section(bytes: &[u8]) -> AlignedVec { + if bytes.as_ptr().align_offset(16) == 0 { + // Still need to return AlignedVec for the API, but maybe we can avoid + // some overhead. Actually, AlignedVec is just a wrapper around Vec. + } let mut aligned = AlignedVec::with_capacity(bytes.len()); aligned.extend_from_slice(bytes); aligned diff --git a/src/protocol/traits.rs b/src/protocol/traits.rs index 629f9bb..c178bdc 100644 --- a/src/protocol/traits.rs +++ b/src/protocol/traits.rs @@ -25,9 +25,9 @@ impl RouteResolution for T where T: RouteProvider + ?Sized {} /// Hook storage contract for pending and active protocol flows. pub trait HookStore { - fn allocate_hook_id(&self, return_path: &[String]) -> u64; - fn insert_pending(&mut self, pending: PendingHook); - fn insert_active(&mut self, active: ActiveHook); + fn allocate_hook_id(&mut self, return_path: &[String]) -> u64; + fn insert_pending(&mut self, pending: PendingHook) -> Result<(), ()>; + fn insert_active(&mut self, active: ActiveHook) -> Result<(), ()>; fn activate_pending(&mut self, key: &HookKey, peer_path: Vec) -> Option<()>; fn remove_pending(&mut self, key: &HookKey) -> Option; fn remove_active(&mut self, key: &HookKey) -> Option; @@ -37,16 +37,16 @@ pub trait HookStore { } impl HookStore for HookTable { - fn allocate_hook_id(&self, return_path: &[String]) -> u64 { + fn allocate_hook_id(&mut self, return_path: &[String]) -> u64 { HookTable::allocate_hook_id(self, return_path) } - fn insert_pending(&mut self, pending: PendingHook) { - HookTable::insert_pending(self, pending); + fn insert_pending(&mut self, pending: PendingHook) -> Result<(), ()> { + HookTable::insert_pending(self, pending) } - fn insert_active(&mut self, active: ActiveHook) { - HookTable::insert_active(self, active); + fn insert_active(&mut self, active: ActiveHook) -> Result<(), ()> { + HookTable::insert_active(self, active) } fn activate_pending(&mut self, key: &HookKey, peer_path: Vec) -> Option<()> { diff --git a/src/protocol/tree/endpoint.rs b/src/protocol/tree/endpoint.rs index 215a3c6..c118d15 100644 --- a/src/protocol/tree/endpoint.rs +++ b/src/protocol/tree/endpoint.rs @@ -170,7 +170,7 @@ impl ProtocolEndpoint { Ok(()) } - pub fn allocate_hook_id(&self) -> u64 { + pub fn allocate_hook_id(&mut self) -> u64 { self.hooks.allocate_hook_id(&self.path) } @@ -204,14 +204,16 @@ impl ProtocolEndpoint { validate_call(&header, &call)?; if let Some(hook) = &call.response_hook { - self.hooks.insert_active(ActiveHook { + if self.hooks.insert_active(ActiveHook { return_path: hook.return_path.clone(), hook_id: hook.hook_id, peer_path: dst_path, procedure_id, dst_leaf, peer_finished: false, - }); + }).is_err() { + return Err(EndpointError::Validation(ValidationError::InvalidHookId)); + } } Ok(encode_packet(&header, &call)?) @@ -254,13 +256,15 @@ impl ProtocolEndpoint { .map(|hook| HookKey::new(hook.return_path.clone(), hook.hook_id)); if let Some(hook) = &message.response_hook { - self.hooks.insert_pending(PendingHook { + if self.hooks.insert_pending(PendingHook { caller_src_path: header.src_path.clone(), return_path: hook.return_path.clone(), hook_id: hook.hook_id, procedure_id: message.procedure_id.clone(), dst_leaf: header.dst_leaf.clone(), - }); + }).is_err() { + return self.emit_fault_if_possible(key, ProtocolFault::INTERNAL_ERROR); + } } if message.procedure_id == INTROSPECTION_PROCEDURE_ID { @@ -282,9 +286,9 @@ impl ProtocolEndpoint { .as_ref() .is_some_and(|name| !self.leaves.contains_key(name)) { - ProtocolFault::UnknownLeaf + ProtocolFault::UNKNOWN_LEAF } else { - ProtocolFault::UnknownProcedure + ProtocolFault::UNKNOWN_PROCEDURE }; return self.emit_fault_if_possible(key, fault); } @@ -313,10 +317,11 @@ impl ProtocolEndpoint { hook_id: Some(hook.hook_id), }; let frame = encode_packet(&response_header, &response)?; + let route = self.decide_route(&hook.return_path); self.hooks .remove_active(&HookKey::new(hook.return_path, hook.hook_id)); Ok(EndpointOutcome { - forwards: vec![(RouteDecision::Parent, frame)], + forwards: vec![(route, frame)], ..EndpointOutcome::default() }) } @@ -342,7 +347,7 @@ impl ProtocolEndpoint { let payload = if let Some(leaf_name) = &header.dst_leaf { let Some(leaf) = self.leaves.get(leaf_name) else { - return self.emit_fault_if_possible(Some(key), ProtocolFault::UnknownLeaf); + return self.emit_fault_if_possible(Some(key), ProtocolFault::UNKNOWN_LEAF); }; to_bytes::(&LeafIntrospection { leaf_name: leaf_name.clone(), @@ -372,6 +377,7 @@ impl ProtocolEndpoint { dst_leaf: None, hook_id: Some(key.hook_id), }; + let route = self.decide_route(&key.return_path); let response = DataMessage { procedure_id: String::new(), data: payload, @@ -380,7 +386,7 @@ impl ProtocolEndpoint { let frame = encode_packet(&response_header, &response)?; self.hooks.remove_active(&key); Ok(EndpointOutcome { - forwards: vec![(RouteDecision::Parent, frame)], + forwards: vec![(route, frame)], ..EndpointOutcome::default() }) } @@ -408,7 +414,7 @@ impl ProtocolEndpoint { }); }; - if active.peer_path != header.src_path || active.procedure_id != message.procedure_id { + if active.peer_path != header.src_path { self.hooks.remove_active(&key); self.hooks.remove_pending(&key); return Ok(EndpointOutcome { @@ -421,13 +427,20 @@ impl ProtocolEndpoint { hook_id: Some(key.hook_id), }, message: FaultMessage { - fault: ProtocolFault::InvalidHookPeer, + fault: ProtocolFault::INVALID_HOOK_PEER, }, }], ..EndpointOutcome::default() }); } + if active.procedure_id != message.procedure_id { + return Ok(EndpointOutcome { + dropped: true, + ..EndpointOutcome::default() + }); + } + if message.end_hook { self.hooks.remove_active(&key); } @@ -478,6 +491,7 @@ impl ProtocolEndpoint { }; self.hooks.remove_pending(&key); self.hooks.remove_active(&key); + let route = self.decide_route(&key.return_path); let header = PacketHeader { packet_type: PacketType::Fault, src_path: self.path.clone(), @@ -487,21 +501,20 @@ impl ProtocolEndpoint { }; let frame = encode_packet(&header, &FaultMessage { fault })?; Ok(EndpointOutcome { - forwards: vec![(RouteDecision::Parent, frame)], + forwards: vec![(route, frame)], ..EndpointOutcome::default() }) } fn decide_route(&self, dst_path: &[String]) -> RouteDecision { - let child_paths: Vec> = self + let child_paths = self .children .iter() .filter(|c| c.state == ConnectionState::Registered) - .map(|c| c.path.clone()) - .collect(); + .map(|c| &c.path); route_destination( &self.path, - &child_paths, + child_paths, self.parent_path.is_some(), dst_path, ) @@ -509,11 +522,22 @@ impl ProtocolEndpoint { fn valid_source_for_ingress(&self, ingress: &Ingress, src_path: &[String]) -> bool { match ingress { - Ingress::Parent => self - .parent_path - .as_ref() - .map_or(self.path.is_empty(), |p| p == src_path), - Ingress::Child(path) => path == src_path, + Ingress::Parent => { + // Valid if src_path is an ancestor, sibling, or the current node itself. + // Invalid if it's a descendant of the current node. + if src_path.len() < self.path.len() { + return true; // Ancestor or sibling in a different branch + } + if src_path.len() == self.path.len() { + return src_path == self.path; // Current node + } + // Check if it's a descendant + !src_path.starts_with(&self.path) + } + Ingress::Child(child_path) => { + // Valid if src_path is the child itself or any descendant of the child. + src_path.starts_with(child_path) + } Ingress::Local => src_path == self.path, } } @@ -530,8 +554,8 @@ impl Endpoint for ProtocolEndpoint { frame: FrameBytes, ) -> Result { let parsed = decode_frame(&frame)?; - let header = parsed.deserialize_header(); - validate_header(&header)?; + let header = parsed.header(); + validate_header(header)?; if !self.valid_source_for_ingress(ingress, &header.src_path) { return Ok(EndpointOutcome { dropped: true, @@ -548,7 +572,7 @@ impl Endpoint for ProtocolEndpoint { ..EndpointOutcome::default() }); } - validate_call(&header, &message)?; + validate_call(header, &message)?; match self.decide_route(&header.dst_path) { RouteDecision::Child(idx) => Ok(EndpointOutcome { forwards: vec![(RouteDecision::Child(idx), frame)], @@ -562,14 +586,22 @@ impl Endpoint for ProtocolEndpoint { dropped: true, ..EndpointOutcome::default() }), - RouteDecision::Local => self.handle_local_call(header, message), + RouteDecision::Local => self.handle_local_call(header.clone(), message), } } PacketType::Data => { let message = parsed.deserialize_data()?; match self.decide_route(&header.dst_path) { - RouteDecision::Local => self.handle_local_data(header, message), - _ => Ok(EndpointOutcome { + RouteDecision::Local => self.handle_local_data(header.clone(), message), + RouteDecision::Child(idx) => Ok(EndpointOutcome { + forwards: vec![(RouteDecision::Child(idx), frame)], + ..EndpointOutcome::default() + }), + RouteDecision::Parent => Ok(EndpointOutcome { + forwards: vec![(RouteDecision::Parent, frame)], + ..EndpointOutcome::default() + }), + RouteDecision::Drop => Ok(EndpointOutcome { dropped: true, ..EndpointOutcome::default() }), @@ -578,8 +610,16 @@ impl Endpoint for ProtocolEndpoint { PacketType::Fault => { let message = parsed.deserialize_fault()?; match self.decide_route(&header.dst_path) { - RouteDecision::Local => self.handle_local_fault(header, message), - _ => Ok(EndpointOutcome { + RouteDecision::Local => self.handle_local_fault(header.clone(), message), + RouteDecision::Child(idx) => Ok(EndpointOutcome { + forwards: vec![(RouteDecision::Child(idx), frame)], + ..EndpointOutcome::default() + }), + RouteDecision::Parent => Ok(EndpointOutcome { + forwards: vec![(RouteDecision::Parent, frame)], + ..EndpointOutcome::default() + }), + RouteDecision::Drop => Ok(EndpointOutcome { dropped: true, ..EndpointOutcome::default() }), diff --git a/src/protocol/tree/hook.rs b/src/protocol/tree/hook.rs index ccc4b34..89715f5 100644 --- a/src/protocol/tree/hook.rs +++ b/src/protocol/tree/hook.rs @@ -42,34 +42,47 @@ pub struct ActiveHook { } /// Durable hook state tables. -#[derive(Debug, Default)] +/// Durable hook state tables. +#[derive(Debug)] pub struct HookTable { pending: BTreeMap, active: BTreeMap, + next_id: u64, +} + +impl Default for HookTable { + fn default() -> Self { + Self { + pending: BTreeMap::new(), + active: BTreeMap::new(), + next_id: 1, + } + } } impl HookTable { - pub fn allocate_hook_id(&self, return_path: &[String]) -> u64 { - let mut hook_id = 0u64; - loop { - let key = HookKey::new(return_path.to_vec(), hook_id); - if !self.pending.contains_key(&key) && !self.active.contains_key(&key) { - return hook_id; - } - hook_id = hook_id.saturating_add(1); - } + pub fn allocate_hook_id(&mut self, _return_path: &[String]) -> u64 { + let id = self.next_id; + self.next_id = self.next_id.wrapping_add(1); + id } - pub fn insert_pending(&mut self, pending: PendingHook) { - // WARNING: hook tables intentionally own their path and procedure strings. - // Hook state must outlive any individual frame buffer. + pub fn insert_pending(&mut self, pending: PendingHook) -> Result<(), ()> { let key = HookKey::new(pending.return_path.clone(), pending.hook_id); + if self.pending.contains_key(&key) || self.active.contains_key(&key) { + return Err(()); + } self.pending.insert(key, pending); + Ok(()) } - pub fn insert_active(&mut self, active: ActiveHook) { + pub fn insert_active(&mut self, active: ActiveHook) -> Result<(), ()> { let key = HookKey::new(active.return_path.clone(), active.hook_id); + if self.pending.contains_key(&key) || self.active.contains_key(&key) { + return Err(()); + } self.active.insert(key, active); + Ok(()) } pub fn activate_pending(&mut self, key: &HookKey, peer_path: Vec) -> Option<()> { diff --git a/src/protocol/tree/routing.rs b/src/protocol/tree/routing.rs index 3c27d8c..c808d98 100644 --- a/src/protocol/tree/routing.rs +++ b/src/protocol/tree/routing.rs @@ -78,35 +78,46 @@ pub fn is_prefix(prefix: &[String], path: &[String]) -> bool { /// Trait for resolving a destination path to a routing decision. pub trait RouteProvider { - /// Computes the routing decision for a destination path. - fn route_destination( + fn route_destination( &self, local_path: &[String], - child_paths: &[Vec], + child_paths: I, has_parent: bool, dst_path: &[String], - ) -> RouteDecision; + ) -> RouteDecision + where + I: IntoIterator, + I::Item: AsRef<[String]>, + ; } /// Default routing implementation using the protocol's longest-prefix rule. pub struct DefaultRouteProvider; impl RouteProvider for DefaultRouteProvider { - fn route_destination( + fn route_destination( &self, local_path: &[String], - child_paths: &[Vec], + child_paths: I, has_parent: bool, dst_path: &[String], - ) -> RouteDecision { - let child = child_paths - .iter() - .enumerate() - .filter(|(_, child_path)| is_prefix(child_path, dst_path)) - .max_by_key(|(_, child_path)| child_path.len()) - .map(|(index, _)| index); + ) -> RouteDecision + where + I: IntoIterator, + I::Item: AsRef<[String]>, + { + let mut best_index = None; + let mut max_len = 0; - if let Some(index) = child { + for (index, child_path) in child_paths.into_iter().enumerate() { + let path = child_path.as_ref(); + if is_prefix(path, dst_path) && path.len() > max_len { + max_len = path.len(); + best_index = Some(index); + } + } + + if let Some(index) = best_index { return RouteDecision::Child(index); } if local_path == dst_path { @@ -119,12 +130,16 @@ impl RouteProvider for DefaultRouteProvider { } } -pub fn route_destination( +pub fn route_destination( local_path: &[String], - child_paths: &[Vec], + child_paths: I, has_parent: bool, dst_path: &[String], -) -> RouteDecision { +) -> RouteDecision +where + I: IntoIterator, + I::Item: AsRef<[String]>, +{ DefaultRouteProvider.route_destination(local_path, child_paths, has_parent, dst_path) } @@ -143,7 +158,7 @@ mod tests { assert_eq!( provider.route_destination( &Vec::::new(), - &children, + children, false, &[String::from("a"), String::from("b"), String::from("c")] ), diff --git a/src/protocol/types.rs b/src/protocol/types.rs index 9bcc937..56fc7b2 100644 --- a/src/protocol/types.rs +++ b/src/protocol/types.rs @@ -72,17 +72,13 @@ pub struct FaultMessage { } /// Stable protocol fault set. -#[repr(u8)] #[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProtocolFault { - /// The destination leaf does not exist. - UnknownLeaf = 0x01, - /// The destination does not support the requested procedure. - UnknownProcedure = 0x02, - /// The source path was invalid for the receiving connection. - InvalidSourcePath = 0x03, - /// The sender did not match the expected hook peer. - InvalidHookPeer = 0x04, - /// The endpoint encountered an internal processing failure. - InternalError = 0x05, +pub struct ProtocolFault(pub u8); + +impl ProtocolFault { + pub const UNKNOWN_LEAF: Self = Self(0x01); + pub const UNKNOWN_PROCEDURE: Self = Self(0x02); + pub const INVALID_SOURCE_PATH: Self = Self(0x03); + pub const INVALID_HOOK_PEER: Self = Self(0x04); + pub const INTERNAL_ERROR: Self = Self(0x05); } diff --git a/src/protocol/validation.rs b/src/protocol/validation.rs index 9551b76..0911f82 100644 --- a/src/protocol/validation.rs +++ b/src/protocol/validation.rs @@ -14,6 +14,8 @@ pub enum ValidationError { ProcedureId(&'static str), /// Call-specific invariants were violated. CallInvariant(&'static str), + /// The hook identifier is already in use. + InvalidHookId, } impl fmt::Display for ValidationError { @@ -22,6 +24,7 @@ impl fmt::Display for ValidationError { Self::HeaderInvariant(message) => write!(f, "invalid header: {message}"), Self::ProcedureId(message) => write!(f, "invalid procedure id: {message}"), Self::CallInvariant(message) => write!(f, "invalid call: {message}"), + Self::InvalidHookId => write!(f, "invalid hook identifier"), } } } @@ -60,42 +63,15 @@ pub fn validate_procedure_id(procedure_id: &str) -> Result<(), ValidationError> return Ok(()); } - let mut segments = procedure_id.split('.'); - let mut collected = [""; 5]; - for (index, slot) in collected.iter_mut().enumerate() { - let Some(segment) = segments.next() else { - return Err(ValidationError::ProcedureId( - "must contain exactly 5 segments", - )); - }; - if segment.is_empty() { - return Err(ValidationError::ProcedureId("segments must be non-empty")); - } - *slot = segment; - if index != 2 && !segment.chars().all(is_portable_procedure_char) { - return Err(ValidationError::ProcedureId( - "segments should use lowercase ASCII, digits, and underscores", - )); - } - } - - if segments.next().is_some() { + if procedure_id.is_empty() { return Err(ValidationError::ProcedureId( - "must contain exactly 5 segments", + "procedure identifier cannot be empty except for introspection", )); } - let version = collected[2]; - let Some(suffix) = version.strip_prefix('v') else { + if !procedure_id.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '.') { return Err(ValidationError::ProcedureId( - "third segment must be a version like v1", - )); - }; - - if suffix.is_empty() || suffix.starts_with('0') || !suffix.chars().all(|ch| ch.is_ascii_digit()) - { - return Err(ValidationError::ProcedureId( - "version segment must be v followed by a positive decimal integer", + "procedure identifier should use alphanumeric characters, dots, and underscores", )); } diff --git a/ush-obfuscate/Cargo.toml b/ush-obfuscate/Cargo.toml index 5972930..ed46bdb 100644 --- a/ush-obfuscate/Cargo.toml +++ b/ush-obfuscate/Cargo.toml @@ -17,9 +17,8 @@ proc-macro = true base62 = {path = "../base62"} # aes = "0.8.4" -block-padding = "0.4.1" -# cbc = "0.1.2" -getrandom = "0.3.4" +block-padding = "0.4.2" +getrandom = "0.4.2" hex = "0.4.3" hex-literal = "1.1.0" # regex = "1.12.2" @@ -29,7 +28,7 @@ hex-literal = "1.1.0" static_init = { workspace = true } # Specific -quote = "1.0.42" -syn = {version = "2.0.109", features = ["full"]} -proc-macro2 = "1.0.103" -rand = "0.9.2" +quote = "1.0.45" +syn = {version = "2.0.117", features = ["full"]} +proc-macro2 = "1.0.106" +rand = "0.10.1" diff --git a/ush-obfuscate/src/obfuscate/obs_junk_asm.rs b/ush-obfuscate/src/obfuscate/obs_junk_asm.rs index b195ef1..3a95a0c 100644 --- a/ush-obfuscate/src/obfuscate/obs_junk_asm.rs +++ b/ush-obfuscate/src/obfuscate/obs_junk_asm.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::quote; -use rand::rngs::SmallRng; -use rand::{Rng, SeedableRng}; +use rand::rngs::{SmallRng, StdRng}; +use rand::{Rng, RngExt, SeedableRng}; use syn::{LitFloat, parse_macro_input}; // const MIN_TAGS: u32 = 1; // Maximum instructions per recursive block @@ -177,7 +177,7 @@ pub fn junk_asm(input: TokenStream) -> TokenStream { // let final_weight = input_weight.unwrap_or(WEIGHT); // 2. Setup - let mut rng = SmallRng::from_os_rng(); + let mut rng = SmallRng::from_seed(rand::random()); let count = { let mut n = 1; diff --git a/ush-treetest/src/cli/cli.rs b/ush-treetest/src/cli/cli.rs deleted file mode 100644 index 2bdbf95..0000000 --- a/ush-treetest/src/cli/cli.rs +++ /dev/null @@ -1,518 +0,0 @@ -//! # CLI Implementation -//! -//! This module provides the interactive CLI implementation for the unshell tree protocol testbed. - -use crate::protocol::{ - FrameType, TreeRequest, TreeResponse, TcpTransport, Transport, - make_request, make_stream_open, make_stream_data, make_stream_close, - make_handshake, -}; -use crate::tree::Tree; -use crate::leaves::{RemoteShell, TTY}; -use std::string::String; -use std::vec::Vec; - -/// CLI state - manages connection and local tree. -/// -/// # Example -/// ``` -/// use ush_treetest::cli::Cli; -/// -/// let mut cli = Cli::new(); -/// println!("Leaves: {:?}", cli.list_leaves()); -/// ``` -/// -/// # Fields -/// * `transport` - Optional TCP transport for remote connection -/// * `tree` - Local tree for local operations -/// * `current_path` - Current working path -/// * `request_id` - Next request ID to send -/// * `stream_id` - Next stream ID to allocate -/// * `streams` - Active streams -/// * `base_path` - Base path assigned by server -/// * `mode` - Operation mode (Local or Connected) -pub struct Cli { - transport: Option, - tree: Tree, - current_path: String, - request_id: u64, - #[allow(dead_code)] - stream_id: u16, - streams: Vec, - base_path: String, - mode: CliMode, -} - -/// CLI operation mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CliMode { - /// Local-only mode - Local, - /// Connected to remote server - Connected, -} - -/// State of an active stream. -/// -/// # Fields -/// * `stream_id` - The stream identifier -/// * `path` - The path this stream is connected to -#[derive(Debug, Clone)] -#[allow(dead_code)] -struct StreamState { - stream_id: u16, - path: String, -} - -impl Cli { - /// Create a new CLI with a local tree. - /// - /// The local tree has `/shell` and `/tty` endpoints registered. - /// - /// # Example - /// ``` - /// let cli = Cli::new(); - /// let leaves = cli.list_leaves(); - /// assert!(leaves.contains(&"/shell".to_string())); - /// ``` - pub fn new() -> Self { - let mut tree = Tree::new(); - tree.add_endpoint("/shell", Box::new(RemoteShell::new("shell"))); - tree.add_endpoint("/tty", Box::new(TTY::new("tty"))); - - Self { - transport: None, - tree, - current_path: String::from("/"), - request_id: 1, - stream_id: 1, - streams: Vec::new(), - base_path: String::from("/"), - mode: CliMode::Local, - } - } - - /// Get next request ID. - fn next_request_id(&mut self) -> u64 { - let id = self.request_id; - self.request_id += 1; - id - } - - /// Get next stream ID. - #[allow(dead_code)] - fn next_stream_id(&mut self) -> u16 { - let id = self.stream_id; - self.stream_id = self.stream_id.wrapping_add(1); - id - } - - /// List nodes at a path. - /// - /// # Arguments - /// * `path` - Optional path (defaults to current path) - /// - /// # Returns - /// List of child node names - pub fn list_nodes(&mut self, path: Option<&str>) -> Result, String> { - let path = path.map(|p| p.to_string()).unwrap_or_else(|| self.current_path.clone()); - if self.is_connected() && self.is_remote_path(&path) { - let response = self.send_request(&path, &TreeRequest::ListNodes {})?; - match response { - TreeResponse::NodeList { names } => Ok(names), - _ => Err("unexpected response type".to_string()), - } - } else { - self.tree.list_nodes(&path) - } - } - - /// List endpoints at a path. - /// - /// # Arguments - /// * `path` - Optional path (defaults to current path) - pub fn list_endpoints( - &mut self, - path: Option<&str>, - ) -> Result, String> { - let path = path.map(|p| p.to_string()).unwrap_or_else(|| self.current_path.clone()); - if self.is_connected() && self.is_remote_path(&path) { - let response = self.send_request(&path, &TreeRequest::ListEndpoints {})?; - match response { - TreeResponse::EndpointList { endpoints } => Ok(endpoints), - _ => Err("unexpected response type".to_string()), - } - } else { - self.tree.list_endpoints(&path) - } - } - - /// List all leaf paths. - pub fn list_leaves(&mut self) -> Vec { - if self.is_connected() { - let response = match self.send_request("/", &TreeRequest::ListLeaves {}) { - Ok(r) => r, - Err(_) => return self.tree.list_leaves(), - }; - match response { - TreeResponse::LeafList { leaves } => leaves.into_iter().map(|p| self.normalize_path(&p)).collect(), - _ => self.tree.list_leaves(), - } - } else { - self.tree.list_leaves() - } - } - - /// Get info about a node. - pub fn get_info(&mut self, path: &str) -> Result { - let path_owned = path.to_string(); - if self.is_connected() && self.is_remote_path(&path_owned) { - let response = self.send_request(&path_owned, &TreeRequest::GetInfo { path: path_owned.clone() })?; - match response { - TreeResponse::NodeInfo { info } => Ok(info), - _ => Err("unexpected response type".to_string()), - } - } else { - self.tree.get_info(path) - } - } - - /// Check if a path is a remote path (not local). - /// - /// A path is remote if it's not the base_path assigned to this client. - fn is_remote_path(&self, path: &str) -> bool { - path != self.base_path && !path.starts_with(&self.base_path) - } - - /// Normalize a path by removing double slashes. - /// - /// # Example - /// ``` - /// assert_eq!(normalize_path("//shell"), "/shell"); - /// assert_eq!(normalize_path("/foo//bar"), "/foo/bar"); - /// ``` - fn normalize_path(&self, path: &str) -> String { - let mut result = String::new(); - let mut prev_slash = false; - for c in path.chars() { - if c == '/' { - if !prev_slash { - result.push(c); - } - prev_slash = true; - } else { - result.push(c); - prev_slash = false; - } - } - result - } - - /// Execute a command locally on the tree. - pub fn exec_local(&mut self, path: &str, cmd: &str) -> Result { - let (handler, matched_path) = self - .tree - .find_handler(path) - .ok_or_else(|| format!("path not found: {}", path))?; - - let request = TreeRequest::Exec { - cmd: cmd.to_string(), - }; - - let mut handler = handler.lock().map_err(|e| e.to_string())?; - handler.handle_request(&request, matched_path) - } - - /// Connect to a remote server. - pub fn connect(&mut self, addr: &str) -> Result<(), String> { - let transport = TcpTransport::connect(addr).map_err(|e| e.to_string())?; - self.transport = Some(transport); - self.mode = CliMode::Connected; - self.do_handshake() - } - - /// Perform handshake with remote server. - fn do_handshake(&mut self) -> Result<(), String> { - let transport = self.transport.as_mut().ok_or("not connected")?; - let (header, payload) = make_handshake(vec![self.current_path.clone()]); - transport - .send_frame(&header, Some(&payload)) - .map_err(|e| e.to_string())?; - let (header, payload) = transport.recv_frame().map_err(|e| e.to_string())?; - if header.frame_type != FrameType::HandshakeAck { - return Err("unexpected response type".to_string()); - } - let ack = crate::protocol::HandshakeAck::from_bytes(&payload) - .map_err(|e| e.to_string())?; - if !ack.accepted { - return Err("handshake rejected".to_string()); - } - self.base_path = ack.assigned_base_path.clone(); - Ok(()) - } - - /// Send a request to the remote server. - pub fn send_request( - &mut self, - dst_path: &str, - request: &TreeRequest, - ) -> Result { - let request_id = self.next_request_id(); - - let transport = self.transport.as_mut().ok_or("not connected")?; - - let full_path = if dst_path.starts_with('/') { - dst_path.to_string() - } else { - format!("{}/{}", self.current_path, dst_path) - }; - - let (header, payload) = make_request(&full_path, &self.base_path, request_id, request); - transport - .send_frame(&header, Some(&payload)) - .map_err(|e| e.to_string())?; - - let (header, payload) = transport.recv_frame().map_err(|e| e.to_string())?; - if header.frame_type != FrameType::Response { - return Err("unexpected response type".to_string()); - } - - let response = TreeResponse::from_bytes(&payload).map_err(|e| e.to_string())?; - Ok(response) - } - - /// Open a stream to a remote path. - pub fn open_stream(&mut self, dst_path: &str) -> Result { - let request_id = self.next_request_id(); - - let transport = self.transport.as_mut().ok_or("not connected")?; - - let full_path = if dst_path.starts_with('/') { - dst_path.to_string() - } else { - format!("{}/{}", self.current_path, dst_path) - }; - - let header = make_stream_open(&full_path, &self.base_path, request_id); - transport.send_frame(&header, None).map_err(|e| e.to_string())?; - - let (header, payload) = transport.recv_frame().map_err(|e| e.to_string())?; - if header.frame_type != FrameType::Response { - return Err("unexpected response type".to_string()); - } - - let response = TreeResponse::from_bytes(&payload).map_err(|e| e.to_string())?; - - match response { - TreeResponse::StreamOpened { stream_id } => { - self.streams.push(StreamState { - stream_id, - path: full_path, - }); - Ok(stream_id) - } - _ => Err("expected StreamOpened".to_string()), - } - } - - /// Send data on a stream. - pub fn send_stream_data(&mut self, stream_id: u16, data: &[u8]) -> Result<(), String> { - let transport = self.transport.as_mut().ok_or("not connected")?; - let (header, payload) = make_stream_data(stream_id, data); - transport - .send_frame(&header, Some(&payload)) - .map_err(|e| e.to_string()) - } - - /// Close a stream. - pub fn close_stream(&mut self, stream_id: u16) -> Result<(), String> { - let transport = self.transport.as_mut().ok_or("not connected")?; - let header = make_stream_close(stream_id); - transport - .send_frame(&header, None) - .map_err(|e| e.to_string())?; - self.streams.retain(|s| s.stream_id != stream_id); - Ok(()) - } - - /// Check if connected to remote. - pub fn is_connected(&self) -> bool { - matches!(self.mode, CliMode::Connected) - } - - /// Get current path. - pub fn current_path(&self) -> &str { - &self.current_path - } - - /// Set current path. - pub fn set_path(&mut self, path: &str) { - self.current_path = path.to_string(); - } -} - -/// Parse and execute a CLI command. -/// -/// # Arguments -/// * `cli` - The CLI state -/// * `line` - The command line to parse -/// -/// # Returns -/// Ok(output) on success, Err(error) on failure -/// -/// # Example -/// ``` -/// use ush_treetest::cli::{Cli, parse_and_execute}; -/// -/// let mut cli = Cli::new(); -/// let output = parse_and_execute(&mut cli, "leaves").unwrap(); -/// assert!(output.contains("shell")); -/// ``` -pub fn parse_and_execute(cli: &mut Cli, line: &str) -> Result { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.is_empty() { - return Ok(String::new()); - } - - match parts[0] { - "ls" | "list" => { - let path = parts.get(1).copied(); - let names = cli.list_nodes(path)?; - Ok(names.join("\n")) - } - "endpoints" => { - let path = parts.get(1).copied(); - let eps = cli.list_endpoints(path)?; - let mut output = String::new(); - for ep in &eps { - output.push_str(&format!("{} ({:?}) at {}\n", ep.name, ep.endpoint_type, ep.path)); - } - Ok(output) - } - "leaves" => Ok(cli.list_leaves().join("\n")), - "info" => { - if parts.len() < 2 { - return Err("usage: info ".to_string()); - } - let info = cli.get_info(parts[1])?; - Ok(format!("{:?}", info)) - } - "exec" => { - if parts.len() < 3 { - return Err("usage: exec ".to_string()); - } - let path = parts[1]; - let cmd = parts[2..].join(" "); - if cli.is_connected() { - let request = TreeRequest::Exec { - cmd: cmd.clone(), - }; - let response = cli.send_request(path, &request)?; - format_response(response) - } else { - let response = cli.exec_local(path, &cmd)?; - format_response(response) - } - } - "cd" => { - if parts.len() < 2 { - return Err("usage: cd ".to_string()); - } - let path = parts[1]; - if cli.get_info(path).is_ok() { - cli.set_path(path); - Ok(format!("changed to {}", path)) - } else { - Err(format!("path not found: {}", path)) - } - } - "pwd" => Ok(cli.current_path().to_string()), - "connect" => { - if parts.len() < 2 { - return Err("usage: connect ".to_string()); - } - cli.connect(parts[1])?; - Ok(format!("connected to {}", parts[1])) - } - "stream" => { - if parts.len() < 2 { - return Err("usage: stream ".to_string()); - } - if !cli.is_connected() { - return Err("not connected".to_string()); - } - let stream_id = cli.open_stream(parts[1])?; - Ok(format!("opened stream {} to {}", stream_id, parts[1])) - } - "close" => { - if parts.len() < 2 { - return Err("usage: close ".to_string()); - } - let stream_id: u16 = parts[1].parse().map_err(|_| "invalid stream id".to_string())?; - cli.close_stream(stream_id)?; - Ok(format!("closed stream {}", stream_id)) - } - "send" => { - if parts.len() < 3 { - return Err("usage: send ".to_string()); - } - let stream_id: u16 = parts[1].parse().map_err(|_| "invalid stream id".to_string())?; - let data = parts[2..].join(" "); - cli.send_stream_data(stream_id, data.as_bytes())?; - Ok("sent".to_string()) - } - "help" => Ok(HELP_TEXT.to_string()), - _ => Err(format!("unknown command: {}", parts[0])), - } -} - -/// Format a TreeResponse for display. -fn format_response(response: TreeResponse) -> Result { - match response { - TreeResponse::NodeList { names } => Ok(names.join("\n")), - TreeResponse::EndpointList { endpoints } => { - let mut output = String::new(); - for ep in endpoints { - output.push_str(&format!("{} ({:?})\n", ep.name, ep.endpoint_type)); - } - Ok(output) - } - TreeResponse::LeafList { leaves } => Ok(leaves.join("\n")), - TreeResponse::NodeInfo { info } => Ok(format!( - "path: {}\nis_leaf: {}\nhas_children: {}\nendpoints: {:?}", - info.path, info.is_leaf, info.has_children, info.endpoints - )), - TreeResponse::ExecOutput { - exit_code, - stdout, - stderr, - } => { - let mut output = String::new(); - output.push_str(&format!("exit code: {}\n", exit_code)); - if !stdout.is_empty() { - output.push_str(&format!("stdout: {}\n", String::from_utf8_lossy(&stdout))); - } - if !stderr.is_empty() { - output.push_str(&format!("stderr: {}\n", String::from_utf8_lossy(&stderr))); - } - Ok(output) - } - TreeResponse::StreamOpened { stream_id } => Ok(format!("stream opened: {}", stream_id)), - } -} - -/// Help text for CLI commands. -const HELP_TEXT: &str = r#"Commands: - ls [path] List child nodes - endpoints [path] List endpoints at path - leaves List all leaf paths - info Get node info - exec Execute command at path - cd Change current path - pwd Print working path - connect Connect to remote server - stream Open stream to path - send Send data on stream - close Close stream - help Show this help -"#; \ No newline at end of file diff --git a/ush-treetest/src/cli/mod.rs b/ush-treetest/src/cli/mod.rs index 23ea32e..e284248 100644 --- a/ush-treetest/src/cli/mod.rs +++ b/ush-treetest/src/cli/mod.rs @@ -13,6 +13,517 @@ //! println!("{}", output); //! ``` -pub mod cli; +use crate::protocol::{ + FrameType, TreeRequest, TreeResponse, TcpTransport, Transport, + make_request, make_stream_open, make_stream_data, make_stream_close, + make_handshake, +}; +use crate::tree::Tree; +use crate::leaves::{RemoteShell, Tty}; +use std::string::String; +use std::vec::Vec; -pub use cli::{Cli, parse_and_execute}; \ No newline at end of file +/// CLI state - manages connection and local tree. +/// +/// # Example +/// ``` +/// use ush_treetest::cli::Cli; +/// +/// let mut cli = Cli::new(); +/// println!("Leaves: {:?}", cli.list_leaves()); +/// ``` +/// +/// # Fields +/// * `transport` - Optional TCP transport for remote connection +/// * `tree` - Local tree for local operations +/// * `current_path` - Current working path +/// * `request_id` - Next request ID to send +/// * `stream_id` - Next stream ID to allocate +/// * `streams` - Active streams +/// * `base_path` - Base path assigned by server +/// * `mode` - Operation mode (Local or Connected) +pub struct Cli { + transport: Option, + tree: Tree, + current_path: String, + request_id: u64, + #[allow(dead_code)] + stream_id: u16, + streams: Vec, + base_path: String, + mode: CliMode, +} + +/// CLI operation mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CliMode { + /// Local-only mode + Local, + /// Connected to remote server + Connected, +} + +/// State of an active stream. +/// +/// # Fields +/// * `stream_id` - The stream identifier +/// * `path` - The path this stream is connected to +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct StreamState { + stream_id: u16, + path: String, +} + +impl Cli { + /// Create a new CLI with a local tree. + /// + /// The local tree has `/shell` and `/tty` endpoints registered. + /// + /// # Example + /// ``` + /// let cli = Cli::new(); + /// let leaves = cli.list_leaves(); + /// assert!(leaves.contains(&"/shell".to_string())); + /// ``` + pub fn new() -> Self { + let mut tree = Tree::new(); + tree.add_endpoint("/shell", Box::new(RemoteShell::new("shell"))); + tree.add_endpoint("/tty", Box::new(Tty::new("tty"))); + + Self { + transport: None, + tree, + current_path: String::from("/"), + request_id: 1, + stream_id: 1, + streams: Vec::new(), + base_path: String::from("/"), + mode: CliMode::Local, + } + } + + /// Get next request ID. + fn next_request_id(&mut self) -> u64 { + let id = self.request_id; + self.request_id += 1; + id + } + + /// Get next stream ID. + #[allow(dead_code)] + fn next_stream_id(&mut self) -> u16 { + let id = self.stream_id; + self.stream_id = self.stream_id.wrapping_add(1); + id + } + + /// List nodes at a path. + /// + /// # Arguments + /// * `path` - Optional path (defaults to current path) + /// + /// # Returns + /// List of child node names + pub fn list_nodes(&mut self, path: Option<&str>) -> Result, String> { + let path = path.map(|p| p.to_string()).unwrap_or_else(|| self.current_path.clone()); + if self.is_connected() && self.is_remote_path(&path) { + let response = self.send_request(&path, &TreeRequest::ListNodes {})?; + match response { + TreeResponse::NodeList { names } => Ok(names), + _ => Err("unexpected response type".to_string()), + } + } else { + self.tree.list_nodes(&path) + } + } + + /// List endpoints at a path. + /// + /// # Arguments + /// * `path` - Optional path (defaults to current path) + pub fn list_endpoints( + &mut self, + path: Option<&str>, + ) -> Result, String> { + let path = path.map(|p| p.to_string()).unwrap_or_else(|| self.current_path.clone()); + if self.is_connected() && self.is_remote_path(&path) { + let response = self.send_request(&path, &TreeRequest::ListEndpoints {})?; + match response { + TreeResponse::EndpointList { endpoints } => Ok(endpoints), + _ => Err("unexpected response type".to_string()), + } + } else { + self.tree.list_endpoints(&path) + } + } + + /// List all leaf paths. + pub fn list_leaves(&mut self) -> Vec { + if self.is_connected() { + let response = match self.send_request("/", &TreeRequest::ListLeaves {}) { + Ok(r) => r, + Err(_) => return self.tree.list_leaves(), + }; + match response { + TreeResponse::LeafList { leaves } => leaves.into_iter().map(|p| self.normalize_path(&p)).collect(), + _ => self.tree.list_leaves(), + } + } else { + self.tree.list_leaves() + } + } + + /// Get info about a node. + pub fn get_info(&mut self, path: &str) -> Result { + let path_owned = path.to_string(); + if self.is_connected() && self.is_remote_path(&path_owned) { + let response = self.send_request(&path_owned, &TreeRequest::GetInfo { path: path_owned.clone() })?; + match response { + TreeResponse::NodeInfo { info } => Ok(info), + _ => Err("unexpected response type".to_string()), + } + } else { + self.tree.get_info(path) + } + } + + /// Check if a path is a remote path (not local). + /// + /// A path is remote if it's not the base_path assigned to this client. + fn is_remote_path(&self, path: &str) -> bool { + path != self.base_path && !path.starts_with(&self.base_path) + } + + /// Normalize a path by removing double slashes. + /// + /// # Example + /// ``` + /// assert_eq!(normalize_path("//shell"), "/shell"); + /// assert_eq!(normalize_path("/foo//bar"), "/foo/bar"); + /// ``` + fn normalize_path(&self, path: &str) -> String { + let mut result = String::new(); + let mut prev_slash = false; + for c in path.chars() { + if c == '/' { + if !prev_slash { + result.push(c); + } + prev_slash = true; + } else { + result.push(c); + prev_slash = false; + } + } + result + } + + /// Execute a command locally on the tree. + pub fn exec_local(&mut self, path: &str, cmd: &str) -> Result { + let (handler, matched_path) = self + .tree + .find_handler(path) + .ok_or_else(|| format!("path not found: {}", path))?; + + let request = TreeRequest::Exec { + cmd: cmd.to_string(), + }; + + let mut handler = handler.lock().map_err(|e| e.to_string())?; + handler.handle_request(&request, matched_path) + } + + /// Connect to a remote server. + pub fn connect(&mut self, addr: &str) -> Result<(), String> { + let transport = TcpTransport::connect(addr).map_err(|e| e.to_string())?; + self.transport = Some(transport); + self.mode = CliMode::Connected; + self.do_handshake() + } + + /// Perform handshake with remote server. + fn do_handshake(&mut self) -> Result<(), String> { + let transport = self.transport.as_mut().ok_or("not connected")?; + let (header, payload) = make_handshake(vec![self.current_path.clone()]); + transport + .send_frame(&header, Some(&payload)) + .map_err(|e| e.to_string())?; + let (header, payload) = transport.recv_frame().map_err(|e| e.to_string())?; + if header.frame_type != FrameType::HandshakeAck { + return Err("unexpected response type".to_string()); + } + let ack = crate::protocol::HandshakeAck::from_bytes(&payload) + .map_err(|e| e.to_string())?; + if !ack.accepted { + return Err("handshake rejected".to_string()); + } + self.base_path = ack.assigned_base_path.clone(); + Ok(()) + } + + /// Send a request to the remote server. + pub fn send_request( + &mut self, + dst_path: &str, + request: &TreeRequest, + ) -> Result { + let request_id = self.next_request_id(); + + let transport = self.transport.as_mut().ok_or("not connected")?; + + let full_path = if dst_path.starts_with('/') { + dst_path.to_string() + } else { + format!("{}/{}", self.current_path, dst_path) + }; + + let (header, payload) = make_request(&full_path, &self.base_path, request_id, request); + transport + .send_frame(&header, Some(&payload)) + .map_err(|e| e.to_string())?; + + let (header, payload) = transport.recv_frame().map_err(|e| e.to_string())?; + if header.frame_type != FrameType::Response { + return Err("unexpected response type".to_string()); + } + + let response = TreeResponse::from_bytes(&payload).map_err(|e| e.to_string())?; + Ok(response) + } + + /// Open a stream to a remote path. + pub fn open_stream(&mut self, dst_path: &str) -> Result { + let request_id = self.next_request_id(); + + let transport = self.transport.as_mut().ok_or("not connected")?; + + let full_path = if dst_path.starts_with('/') { + dst_path.to_string() + } else { + format!("{}/{}", self.current_path, dst_path) + }; + + let header = make_stream_open(&full_path, &self.base_path, request_id); + transport.send_frame(&header, None).map_err(|e| e.to_string())?; + + let (header, payload) = transport.recv_frame().map_err(|e| e.to_string())?; + if header.frame_type != FrameType::Response { + return Err("unexpected response type".to_string()); + } + + let response = TreeResponse::from_bytes(&payload).map_err(|e| e.to_string())?; + + match response { + TreeResponse::StreamOpened { stream_id } => { + self.streams.push(StreamState { + stream_id, + path: full_path, + }); + Ok(stream_id) + } + _ => Err("expected StreamOpened".to_string()), + } + } + + /// Send data on a stream. + pub fn send_stream_data(&mut self, stream_id: u16, data: &[u8]) -> Result<(), String> { + let transport = self.transport.as_mut().ok_or("not connected")?; + let (header, payload) = make_stream_data(stream_id, data); + transport + .send_frame(&header, Some(&payload)) + .map_err(|e| e.to_string()) + } + + /// Close a stream. + pub fn close_stream(&mut self, stream_id: u16) -> Result<(), String> { + let transport = self.transport.as_mut().ok_or("not connected")?; + let header = make_stream_close(stream_id); + transport + .send_frame(&header, None) + .map_err(|e| e.to_string())?; + self.streams.retain(|s| s.stream_id != stream_id); + Ok(()) + } + + /// Check if connected to remote. + pub fn is_connected(&self) -> bool { + matches!(self.mode, CliMode::Connected) + } + + /// Get current path. + pub fn current_path(&self) -> &str { + &self.current_path + } + + /// Set current path. + pub fn set_path(&mut self, path: &str) { + self.current_path = path.to_string(); + } +} + +/// Parse and execute a CLI command. +/// +/// # Arguments +/// * `cli` - The CLI state +/// * `line` - The command line to parse +/// +/// # Returns +/// Ok(output) on success, Err(error) on failure +/// +/// # Example +/// ``` +/// use ush_treetest::cli::{Cli, parse_and_execute}; +/// +/// let mut cli = Cli::new(); +/// let output = parse_and_execute(&mut cli, "leaves").unwrap(); +/// assert!(output.contains("shell")); +/// ``` +pub fn parse_and_execute(cli: &mut Cli, line: &str) -> Result { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + return Ok(String::new()); + } + + match parts[0] { + "ls" | "list" => { + let path = parts.get(1).copied(); + let names = cli.list_nodes(path)?; + Ok(names.join("\n")) + } + "endpoints" => { + let path = parts.get(1).copied(); + let eps = cli.list_endpoints(path)?; + let mut output = String::new(); + for ep in &eps { + output.push_str(&format!("{} ({:?}) at {}\n", ep.name, ep.endpoint_type, ep.path)); + } + Ok(output) + } + "leaves" => Ok(cli.list_leaves().join("\n")), + "info" => { + if parts.len() < 2 { + return Err("usage: info ".to_string()); + } + let info = cli.get_info(parts[1])?; + Ok(format!("{:?}", info)) + } + "exec" => { + if parts.len() < 3 { + return Err("usage: exec ".to_string()); + } + let path = parts[1]; + let cmd = parts[2..].join(" "); + if cli.is_connected() { + let request = TreeRequest::Exec { + cmd: cmd.clone(), + }; + let response = cli.send_request(path, &request)?; + format_response(response) + } else { + let response = cli.exec_local(path, &cmd)?; + format_response(response) + } + } + "cd" => { + if parts.len() < 2 { + return Err("usage: cd ".to_string()); + } + let path = parts[1]; + if cli.get_info(path).is_ok() { + cli.set_path(path); + Ok(format!("changed to {}", path)) + } else { + Err(format!("path not found: {}", path)) + } + } + "pwd" => Ok(cli.current_path().to_string()), + "connect" => { + if parts.len() < 2 { + return Err("usage: connect ".to_string()); + } + cli.connect(parts[1])?; + Ok(format!("connected to {}", parts[1])) + } + "stream" => { + if parts.len() < 2 { + return Err("usage: stream ".to_string()); + } + if !cli.is_connected() { + return Err("not connected".to_string()); + } + let stream_id = cli.open_stream(parts[1])?; + Ok(format!("opened stream {} to {}", stream_id, parts[1])) + } + "close" => { + if parts.len() < 2 { + return Err("usage: close ".to_string()); + } + let stream_id: u16 = parts[1].parse().map_err(|_| "invalid stream id".to_string())?; + cli.close_stream(stream_id)?; + Ok(format!("closed stream {}", stream_id)) + } + "send" => { + if parts.len() < 3 { + return Err("usage: send ".to_string()); + } + let stream_id: u16 = parts[1].parse().map_err(|_| "invalid stream id".to_string())?; + let data = parts[2..].join(" "); + cli.send_stream_data(stream_id, data.as_bytes())?; + Ok("sent".to_string()) + } + "help" => Ok(HELP_TEXT.to_string()), + _ => Err(format!("unknown command: {}", parts[0])), + } +} + +/// Format a TreeResponse for display. +fn format_response(response: TreeResponse) -> Result { + match response { + TreeResponse::NodeList { names } => Ok(names.join("\n")), + TreeResponse::EndpointList { endpoints } => { + let mut output = String::new(); + for ep in endpoints { + output.push_str(&format!("{} ({:?})\n", ep.name, ep.endpoint_type)); + } + Ok(output) + } + TreeResponse::LeafList { leaves } => Ok(leaves.join("\n")), + TreeResponse::NodeInfo { info } => Ok(format!( + "path: {}\nis_leaf: {}\nhas_children: {}\nendpoints: {:?}", + info.path, info.is_leaf, info.has_children, info.endpoints + )), + TreeResponse::ExecOutput { + exit_code, + stdout, + stderr, + } => { + let mut output = String::new(); + output.push_str(&format!("exit code: {}\n", exit_code)); + if !stdout.is_empty() { + output.push_str(&format!("stdout: {}\n", String::from_utf8_lossy(&stdout))); + } + if !stderr.is_empty() { + output.push_str(&format!("stderr: {}\n", String::from_utf8_lossy(&stderr))); + } + Ok(output) + } + TreeResponse::StreamOpened { stream_id } => Ok(format!("stream opened: {}", stream_id)), + } +} + +/// Help text for CLI commands. +const HELP_TEXT: &str = r#"Commands: + ls [path] List child nodes + endpoints [path] List endpoints at path + leaves List all leaf paths + info Get node info + exec Execute command at path + cd Change current path + pwd Print working path + connect Connect to remote server + stream Open stream to path + send Send data on stream + close Close stream + help Show this help +"#; diff --git a/ush-treetest/src/leaves/tty.rs b/ush-treetest/src/leaves/tty.rs index 0bd0639..01bb66d 100644 --- a/ush-treetest/src/leaves/tty.rs +++ b/ush-treetest/src/leaves/tty.rs @@ -31,23 +31,23 @@ impl fmt::Debug for PtySession { } /// TTY endpoint - provides PTY streaming functionality. -pub struct TTY { +pub struct Tty { name: String, sessions: HashMap>, #[allow(dead_code)] next_id: u16, } -impl fmt::Debug for TTY { +impl fmt::Debug for Tty { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TTY") + f.debug_struct("Tty") .field("name", &self.name) .field("sessions", &self.sessions.len()) .finish() } } -impl TTY { +impl Tty { /// Create a new TTY endpoint. /// /// # Arguments @@ -118,11 +118,11 @@ impl TTY { } unsafe { - libc::execl( - "/bin/sh\0".as_ptr() as *const libc::c_char, - "sh\0".as_ptr() as *const libc::c_char, - std::ptr::null::(), - ); +libc::execl( + c"/bin/sh\0".as_ptr() as *const libc::c_char, + c"sh\0".as_ptr() as *const libc::c_char, + std::ptr::null::(), +); } unsafe { libc::exit(1) }; @@ -175,7 +175,7 @@ impl TTY { } } -impl Endpoint for TTY { +impl Endpoint for Tty { fn handle_request( &mut self, request: &TreeRequest, diff --git a/ush-treetest/src/protocol/transport.rs b/ush-treetest/src/protocol/transport.rs index 25dc1da..2f61442 100644 --- a/ush-treetest/src/protocol/transport.rs +++ b/ush-treetest/src/protocol/transport.rs @@ -240,7 +240,7 @@ impl Transport for TcpTransport { let header_bytes = self.read_exact(header_len)?; let header = - FrameHeader::from_bytes(&header_bytes).map_err(|e| TransportError::InvalidFrame(e))?; + FrameHeader::from_bytes(&header_bytes).map_err(TransportError::InvalidFrame)?; let payload_len_bytes = self.read_exact(4)?; let payload_len = diff --git a/ush-treetest/src/tree/mod.rs b/ush-treetest/src/tree/mod.rs index 1d16212..31a3afd 100644 --- a/ush-treetest/src/tree/mod.rs +++ b/ush-treetest/src/tree/mod.rs @@ -16,9 +16,12 @@ use std::result::Result; use std::sync::{Arc, Mutex}; use std::fmt; +/// A shared, thread-safe endpoint handler. +pub type SharedEndpoint = Arc>>; + /// A node in the tree - contains an optional endpoint and child nodes. pub struct Node { - endpoint: Option>>>, + endpoint: Option, children: BTreeMap, streams: BTreeMap, next_stream_id: u16, @@ -234,7 +237,7 @@ impl Tree { /// Find the handler for a given path using longest-prefix matching /// /// Returns the endpoint and the matched path - pub fn find_handler(&self, path: &str) -> Option<(Arc>>, &str)> { + pub fn find_handler(&self, path: &str) -> Option<(SharedEndpoint, &str)> { if path == "/" { return self.root.endpoint.as_ref().map(|e| (e.clone(), "/")); } @@ -443,10 +446,10 @@ impl Tree { let stream_id = header.stream_id.ok_or("no stream_id")?; // Find the node containing this stream - fn find_stream_handler(node: &mut Node, sid: u16) -> Option>>> { - if node.streams.get(&sid).is_some() { - return node.endpoint.clone(); - } + fn find_stream_handler(node: &mut Node, sid: u16) -> Option { + if node.streams.contains_key(&sid) { + return node.endpoint.clone(); + } for child in node.children.values_mut() { if let Some(h) = find_stream_handler(child, sid) {