Improve protocol implementation.

This commit is contained in:
Michael Mikovsky
2026-04-24 14:10:03 -06:00
parent 49901b6370
commit 3f1116c26a
19 changed files with 1265 additions and 683 deletions
Generated
+524 -12
View File
@@ -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"
+2 -5
View File
@@ -1,10 +1,7 @@
[workspace]
members = [
# "ush-router",
# "ush-payload",
# "ush-cli",
# "ush-obfuscate",
# "base62", "no-alloc-network-test",
"ush-obfuscate",
"base62",
]
resolver = "2"
+34
View File
@@ -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`.
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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::<aes::Aes256>::new(&key_salted.into(), &iv.into())
.encrypt_padded_mut::<Pkcs7>(&mut buf, pt_len)
.encrypt_padded::<Pkcs7>(&mut buf, pt_len)
.unwrap()
.to_vec();
@@ -73,7 +73,7 @@ pub fn decrypt_aes(input: &str, key_str: &str, iv: [u8; 16]) -> Result<String, S
buf[..cipher_bytes.len()].copy_from_slice(&cipher_bytes);
let pt = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.decrypt_padded::<Pkcs7>(&mut buf)
.map_err(|_| "decryption failed".to_string())?;
Ok(String::from_utf8_lossy(pt).to_string())
+4
View File
@@ -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
+8 -8
View File
@@ -25,9 +25,9 @@ impl<T> 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<String>) -> Option<()>;
fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook>;
fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook>;
@@ -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<String>) -> Option<()> {
+70 -30
View File
@@ -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::<RkyvError>(&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<Vec<String>> = 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<EndpointOutcome, EndpointError> {
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()
}),
+27 -14
View File
@@ -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<HookKey, PendingHook>,
active: BTreeMap<HookKey, ActiveHook>,
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<String>) -> Option<()> {
+33 -18
View File
@@ -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<I>(
&self,
local_path: &[String],
child_paths: &[Vec<String>],
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<I>(
&self,
local_path: &[String],
child_paths: &[Vec<String>],
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<I>(
local_path: &[String],
child_paths: &[Vec<String>],
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::<String>::new(),
&children,
children,
false,
&[String::from("a"), String::from("b"), String::from("c")]
),
+8 -12
View File
@@ -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);
}
+7 -31
View File
@@ -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 {
if procedure_id.is_empty() {
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() {
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",
));
}
+6 -7
View File
@@ -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"
+3 -3
View File
@@ -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;
-518
View File
@@ -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<TcpTransport>,
tree: Tree,
current_path: String,
request_id: u64,
#[allow(dead_code)]
stream_id: u16,
streams: Vec<StreamState>,
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<Vec<String>, 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<Vec<crate::protocol::EndpointInfo>, 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<String> {
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<crate::protocol::NodeInfo, String> {
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<TreeResponse, String> {
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<TreeResponse, String> {
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<u16, String> {
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<String, String> {
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 <path>".to_string());
}
let info = cli.get_info(parts[1])?;
Ok(format!("{:?}", info))
}
"exec" => {
if parts.len() < 3 {
return Err("usage: exec <path> <command>".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 <path>".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 <host:port>".to_string());
}
cli.connect(parts[1])?;
Ok(format!("connected to {}", parts[1]))
}
"stream" => {
if parts.len() < 2 {
return Err("usage: stream <path>".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 <stream_id>".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 <stream_id> <data>".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<String, String> {
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 <path> Get node info
exec <path> <cmd> Execute command at path
cd <path> Change current path
pwd Print working path
connect <host> Connect to remote server
stream <path> Open stream to path
send <id> <data> Send data on stream
close <id> Close stream
help Show this help
"#;
+513 -2
View File
@@ -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};
/// 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<TcpTransport>,
tree: Tree,
current_path: String,
request_id: u64,
#[allow(dead_code)]
stream_id: u16,
streams: Vec<StreamState>,
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<Vec<String>, 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<Vec<crate::protocol::EndpointInfo>, 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<String> {
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<crate::protocol::NodeInfo, String> {
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<TreeResponse, String> {
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<TreeResponse, String> {
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<u16, String> {
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<String, String> {
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 <path>".to_string());
}
let info = cli.get_info(parts[1])?;
Ok(format!("{:?}", info))
}
"exec" => {
if parts.len() < 3 {
return Err("usage: exec <path> <command>".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 <path>".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 <host:port>".to_string());
}
cli.connect(parts[1])?;
Ok(format!("connected to {}", parts[1]))
}
"stream" => {
if parts.len() < 2 {
return Err("usage: stream <path>".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 <stream_id>".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 <stream_id> <data>".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<String, String> {
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 <path> Get node info
exec <path> <cmd> Execute command at path
cd <path> Change current path
pwd Print working path
connect <host> Connect to remote server
stream <path> Open stream to path
send <id> <data> Send data on stream
close <id> Close stream
help Show this help
"#;
+7 -7
View File
@@ -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<u16, Box<PtySession>>,
#[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
@@ -119,8 +119,8 @@ impl TTY {
unsafe {
libc::execl(
"/bin/sh\0".as_ptr() as *const libc::c_char,
"sh\0".as_ptr() as *const libc::c_char,
c"/bin/sh\0".as_ptr() as *const libc::c_char,
c"sh\0".as_ptr() as *const libc::c_char,
std::ptr::null::<libc::c_char>(),
);
}
@@ -175,7 +175,7 @@ impl TTY {
}
}
impl Endpoint for TTY {
impl Endpoint for Tty {
fn handle_request(
&mut self,
request: &TreeRequest,
+1 -1
View File
@@ -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 =
+7 -4
View File
@@ -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<Mutex<Box<dyn Endpoint>>>;
/// A node in the tree - contains an optional endpoint and child nodes.
pub struct Node {
endpoint: Option<Arc<Mutex<Box<dyn Endpoint + 'static>>>>,
endpoint: Option<SharedEndpoint>,
children: BTreeMap<String, Node>,
streams: BTreeMap<u16, Stream>,
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<Mutex<Box<dyn Endpoint>>>, &str)> {
pub fn find_handler(&self, path: &str) -> Option<(SharedEndpoint, &str)> {
if path == "/" {
return self.root.endpoint.as_ref().map(|e| (e.clone(), "/"));
}
@@ -443,8 +446,8 @@ 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<Arc<Mutex<Box<dyn Endpoint>>>> {
if node.streams.get(&sid).is_some() {
fn find_stream_handler(node: &mut Node, sid: u16) -> Option<SharedEndpoint> {
if node.streams.contains_key(&sid) {
return node.endpoint.clone();
}