mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Improve protocol implementation.
This commit is contained in:
Generated
+524
-12
@@ -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
@@ -1,10 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
# "ush-router",
|
||||
# "ush-payload",
|
||||
# "ush-cli",
|
||||
# "ush-obfuscate",
|
||||
# "base62", "no-alloc-network-test",
|
||||
"ush-obfuscate",
|
||||
"base62",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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
@@ -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<()> {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
"#;
|
||||
|
||||
@@ -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
|
||||
@@ -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::c_char>(),
|
||||
);
|
||||
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::<libc::c_char>(),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,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<Arc<Mutex<Box<dyn Endpoint>>>> {
|
||||
if node.streams.get(&sid).is_some() {
|
||||
return node.endpoint.clone();
|
||||
}
|
||||
fn find_stream_handler(node: &mut Node, sid: u16) -> Option<SharedEndpoint> {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user