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.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
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]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -11,12 +31,28 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base62"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"cbc",
|
||||||
|
"regex",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -29,6 +65,24 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.1"
|
version = "3.19.1"
|
||||||
@@ -55,7 +109,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -64,6 +118,15 @@ version = "1.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cbc"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.54"
|
version = "1.2.54"
|
||||||
@@ -92,6 +155,17 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
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]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
@@ -105,12 +179,63 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
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]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -123,6 +248,35 @@ version = "0.1.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -135,6 +289,33 @@ version = "0.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
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]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.64"
|
version = "0.1.64"
|
||||||
@@ -159,6 +340,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -167,8 +354,26 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"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]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.85"
|
version = "0.3.85"
|
||||||
@@ -179,6 +384,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.185"
|
version = "0.2.185"
|
||||||
@@ -223,7 +434,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -264,6 +475,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -290,18 +511,24 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.44"
|
version = "1.0.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rancor"
|
name = "rancor"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -311,6 +538,23 @@ dependencies = [
|
|||||||
"ptr_meta",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -320,6 +564,35 @@ dependencies = [
|
|||||||
"bitflags 2.11.1",
|
"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]]
|
[[package]]
|
||||||
name = "rend"
|
name = "rend"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -356,7 +629,7 @@ checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -371,6 +644,65 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
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]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -430,9 +762,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.114"
|
version = "2.0.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -456,7 +788,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -474,12 +806,24 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unshell"
|
name = "unshell"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -490,6 +834,22 @@ dependencies = [
|
|||||||
"thiserror",
|
"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]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
@@ -500,6 +860,24 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.108"
|
version = "0.2.108"
|
||||||
@@ -532,7 +910,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -545,6 +923,40 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -588,7 +1000,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -599,7 +1011,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -625,3 +1037,103 @@ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"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]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
# "ush-router",
|
"ush-obfuscate",
|
||||||
# "ush-payload",
|
"base62",
|
||||||
# "ush-cli",
|
|
||||||
# "ush-obfuscate",
|
|
||||||
# "base62", "no-alloc-network-test",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
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"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "0.8.4"
|
aes = "0.9.0"
|
||||||
cbc = "0.1.2"
|
cbc = "0.2.0"
|
||||||
regex = "1.12.3"
|
regex = "1.12.3"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.11.0"
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
use crate::{base62::Base62, hash};
|
use crate::{base62::Base62, hash};
|
||||||
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit};
|
||||||
use cbc::cipher::block_padding::Pkcs7;
|
use cbc::cipher::block_padding::Pkcs7;
|
||||||
use regex::Regex;
|
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);
|
buf[..pt_len].copy_from_slice(&plaintext);
|
||||||
|
|
||||||
let mut ct = cbc::Encryptor::<aes::Aes256>::new(&key_salted.into(), &iv.into())
|
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()
|
.unwrap()
|
||||||
.to_vec();
|
.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);
|
buf[..cipher_bytes.len()].copy_from_slice(&cipher_bytes);
|
||||||
|
|
||||||
let pt = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
|
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())?;
|
.map_err(|_| "decryption failed".to_string())?;
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(pt).to_string())
|
Ok(String::from_utf8_lossy(pt).to_string())
|
||||||
|
|||||||
@@ -211,6 +211,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn align_section(bytes: &[u8]) -> AlignedVec {
|
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());
|
let mut aligned = AlignedVec::with_capacity(bytes.len());
|
||||||
aligned.extend_from_slice(bytes);
|
aligned.extend_from_slice(bytes);
|
||||||
aligned
|
aligned
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ impl<T> RouteResolution for T where T: RouteProvider + ?Sized {}
|
|||||||
|
|
||||||
/// Hook storage contract for pending and active protocol flows.
|
/// Hook storage contract for pending and active protocol flows.
|
||||||
pub trait HookStore {
|
pub trait HookStore {
|
||||||
fn allocate_hook_id(&self, return_path: &[String]) -> u64;
|
fn allocate_hook_id(&mut self, return_path: &[String]) -> u64;
|
||||||
fn insert_pending(&mut self, pending: PendingHook);
|
fn insert_pending(&mut self, pending: PendingHook) -> Result<(), ()>;
|
||||||
fn insert_active(&mut self, active: ActiveHook);
|
fn insert_active(&mut self, active: ActiveHook) -> Result<(), ()>;
|
||||||
fn activate_pending(&mut self, key: &HookKey, peer_path: Vec<String>) -> Option<()>;
|
fn activate_pending(&mut self, key: &HookKey, peer_path: Vec<String>) -> Option<()>;
|
||||||
fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook>;
|
fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook>;
|
||||||
fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook>;
|
fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook>;
|
||||||
@@ -37,16 +37,16 @@ pub trait HookStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HookStore for HookTable {
|
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)
|
HookTable::allocate_hook_id(self, return_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_pending(&mut self, pending: PendingHook) {
|
fn insert_pending(&mut self, pending: PendingHook) -> Result<(), ()> {
|
||||||
HookTable::insert_pending(self, pending);
|
HookTable::insert_pending(self, pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_active(&mut self, active: ActiveHook) {
|
fn insert_active(&mut self, active: ActiveHook) -> Result<(), ()> {
|
||||||
HookTable::insert_active(self, active);
|
HookTable::insert_active(self, active)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn activate_pending(&mut self, key: &HookKey, peer_path: Vec<String>) -> Option<()> {
|
fn activate_pending(&mut self, key: &HookKey, peer_path: Vec<String>) -> Option<()> {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ impl ProtocolEndpoint {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allocate_hook_id(&self) -> u64 {
|
pub fn allocate_hook_id(&mut self) -> u64 {
|
||||||
self.hooks.allocate_hook_id(&self.path)
|
self.hooks.allocate_hook_id(&self.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,14 +204,16 @@ impl ProtocolEndpoint {
|
|||||||
validate_call(&header, &call)?;
|
validate_call(&header, &call)?;
|
||||||
|
|
||||||
if let Some(hook) = &call.response_hook {
|
if let Some(hook) = &call.response_hook {
|
||||||
self.hooks.insert_active(ActiveHook {
|
if self.hooks.insert_active(ActiveHook {
|
||||||
return_path: hook.return_path.clone(),
|
return_path: hook.return_path.clone(),
|
||||||
hook_id: hook.hook_id,
|
hook_id: hook.hook_id,
|
||||||
peer_path: dst_path,
|
peer_path: dst_path,
|
||||||
procedure_id,
|
procedure_id,
|
||||||
dst_leaf,
|
dst_leaf,
|
||||||
peer_finished: false,
|
peer_finished: false,
|
||||||
});
|
}).is_err() {
|
||||||
|
return Err(EndpointError::Validation(ValidationError::InvalidHookId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(encode_packet(&header, &call)?)
|
Ok(encode_packet(&header, &call)?)
|
||||||
@@ -254,13 +256,15 @@ impl ProtocolEndpoint {
|
|||||||
.map(|hook| HookKey::new(hook.return_path.clone(), hook.hook_id));
|
.map(|hook| HookKey::new(hook.return_path.clone(), hook.hook_id));
|
||||||
|
|
||||||
if let Some(hook) = &message.response_hook {
|
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(),
|
caller_src_path: header.src_path.clone(),
|
||||||
return_path: hook.return_path.clone(),
|
return_path: hook.return_path.clone(),
|
||||||
hook_id: hook.hook_id,
|
hook_id: hook.hook_id,
|
||||||
procedure_id: message.procedure_id.clone(),
|
procedure_id: message.procedure_id.clone(),
|
||||||
dst_leaf: header.dst_leaf.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 {
|
if message.procedure_id == INTROSPECTION_PROCEDURE_ID {
|
||||||
@@ -282,9 +286,9 @@ impl ProtocolEndpoint {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|name| !self.leaves.contains_key(name))
|
.is_some_and(|name| !self.leaves.contains_key(name))
|
||||||
{
|
{
|
||||||
ProtocolFault::UnknownLeaf
|
ProtocolFault::UNKNOWN_LEAF
|
||||||
} else {
|
} else {
|
||||||
ProtocolFault::UnknownProcedure
|
ProtocolFault::UNKNOWN_PROCEDURE
|
||||||
};
|
};
|
||||||
return self.emit_fault_if_possible(key, fault);
|
return self.emit_fault_if_possible(key, fault);
|
||||||
}
|
}
|
||||||
@@ -313,10 +317,11 @@ impl ProtocolEndpoint {
|
|||||||
hook_id: Some(hook.hook_id),
|
hook_id: Some(hook.hook_id),
|
||||||
};
|
};
|
||||||
let frame = encode_packet(&response_header, &response)?;
|
let frame = encode_packet(&response_header, &response)?;
|
||||||
|
let route = self.decide_route(&hook.return_path);
|
||||||
self.hooks
|
self.hooks
|
||||||
.remove_active(&HookKey::new(hook.return_path, hook.hook_id));
|
.remove_active(&HookKey::new(hook.return_path, hook.hook_id));
|
||||||
Ok(EndpointOutcome {
|
Ok(EndpointOutcome {
|
||||||
forwards: vec![(RouteDecision::Parent, frame)],
|
forwards: vec![(route, frame)],
|
||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -342,7 +347,7 @@ impl ProtocolEndpoint {
|
|||||||
|
|
||||||
let payload = if let Some(leaf_name) = &header.dst_leaf {
|
let payload = if let Some(leaf_name) = &header.dst_leaf {
|
||||||
let Some(leaf) = self.leaves.get(leaf_name) else {
|
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 {
|
to_bytes::<RkyvError>(&LeafIntrospection {
|
||||||
leaf_name: leaf_name.clone(),
|
leaf_name: leaf_name.clone(),
|
||||||
@@ -372,6 +377,7 @@ impl ProtocolEndpoint {
|
|||||||
dst_leaf: None,
|
dst_leaf: None,
|
||||||
hook_id: Some(key.hook_id),
|
hook_id: Some(key.hook_id),
|
||||||
};
|
};
|
||||||
|
let route = self.decide_route(&key.return_path);
|
||||||
let response = DataMessage {
|
let response = DataMessage {
|
||||||
procedure_id: String::new(),
|
procedure_id: String::new(),
|
||||||
data: payload,
|
data: payload,
|
||||||
@@ -380,7 +386,7 @@ impl ProtocolEndpoint {
|
|||||||
let frame = encode_packet(&response_header, &response)?;
|
let frame = encode_packet(&response_header, &response)?;
|
||||||
self.hooks.remove_active(&key);
|
self.hooks.remove_active(&key);
|
||||||
Ok(EndpointOutcome {
|
Ok(EndpointOutcome {
|
||||||
forwards: vec![(RouteDecision::Parent, frame)],
|
forwards: vec![(route, frame)],
|
||||||
..EndpointOutcome::default()
|
..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_active(&key);
|
||||||
self.hooks.remove_pending(&key);
|
self.hooks.remove_pending(&key);
|
||||||
return Ok(EndpointOutcome {
|
return Ok(EndpointOutcome {
|
||||||
@@ -421,13 +427,20 @@ impl ProtocolEndpoint {
|
|||||||
hook_id: Some(key.hook_id),
|
hook_id: Some(key.hook_id),
|
||||||
},
|
},
|
||||||
message: FaultMessage {
|
message: FaultMessage {
|
||||||
fault: ProtocolFault::InvalidHookPeer,
|
fault: ProtocolFault::INVALID_HOOK_PEER,
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if active.procedure_id != message.procedure_id {
|
||||||
|
return Ok(EndpointOutcome {
|
||||||
|
dropped: true,
|
||||||
|
..EndpointOutcome::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if message.end_hook {
|
if message.end_hook {
|
||||||
self.hooks.remove_active(&key);
|
self.hooks.remove_active(&key);
|
||||||
}
|
}
|
||||||
@@ -478,6 +491,7 @@ impl ProtocolEndpoint {
|
|||||||
};
|
};
|
||||||
self.hooks.remove_pending(&key);
|
self.hooks.remove_pending(&key);
|
||||||
self.hooks.remove_active(&key);
|
self.hooks.remove_active(&key);
|
||||||
|
let route = self.decide_route(&key.return_path);
|
||||||
let header = PacketHeader {
|
let header = PacketHeader {
|
||||||
packet_type: PacketType::Fault,
|
packet_type: PacketType::Fault,
|
||||||
src_path: self.path.clone(),
|
src_path: self.path.clone(),
|
||||||
@@ -487,21 +501,20 @@ impl ProtocolEndpoint {
|
|||||||
};
|
};
|
||||||
let frame = encode_packet(&header, &FaultMessage { fault })?;
|
let frame = encode_packet(&header, &FaultMessage { fault })?;
|
||||||
Ok(EndpointOutcome {
|
Ok(EndpointOutcome {
|
||||||
forwards: vec![(RouteDecision::Parent, frame)],
|
forwards: vec![(route, frame)],
|
||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decide_route(&self, dst_path: &[String]) -> RouteDecision {
|
fn decide_route(&self, dst_path: &[String]) -> RouteDecision {
|
||||||
let child_paths: Vec<Vec<String>> = self
|
let child_paths = self
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.state == ConnectionState::Registered)
|
.filter(|c| c.state == ConnectionState::Registered)
|
||||||
.map(|c| c.path.clone())
|
.map(|c| &c.path);
|
||||||
.collect();
|
|
||||||
route_destination(
|
route_destination(
|
||||||
&self.path,
|
&self.path,
|
||||||
&child_paths,
|
child_paths,
|
||||||
self.parent_path.is_some(),
|
self.parent_path.is_some(),
|
||||||
dst_path,
|
dst_path,
|
||||||
)
|
)
|
||||||
@@ -509,11 +522,22 @@ impl ProtocolEndpoint {
|
|||||||
|
|
||||||
fn valid_source_for_ingress(&self, ingress: &Ingress, src_path: &[String]) -> bool {
|
fn valid_source_for_ingress(&self, ingress: &Ingress, src_path: &[String]) -> bool {
|
||||||
match ingress {
|
match ingress {
|
||||||
Ingress::Parent => self
|
Ingress::Parent => {
|
||||||
.parent_path
|
// Valid if src_path is an ancestor, sibling, or the current node itself.
|
||||||
.as_ref()
|
// Invalid if it's a descendant of the current node.
|
||||||
.map_or(self.path.is_empty(), |p| p == src_path),
|
if src_path.len() < self.path.len() {
|
||||||
Ingress::Child(path) => path == src_path,
|
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,
|
Ingress::Local => src_path == self.path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,8 +554,8 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
frame: FrameBytes,
|
frame: FrameBytes,
|
||||||
) -> Result<EndpointOutcome, EndpointError> {
|
) -> Result<EndpointOutcome, EndpointError> {
|
||||||
let parsed = decode_frame(&frame)?;
|
let parsed = decode_frame(&frame)?;
|
||||||
let header = parsed.deserialize_header();
|
let header = parsed.header();
|
||||||
validate_header(&header)?;
|
validate_header(header)?;
|
||||||
if !self.valid_source_for_ingress(ingress, &header.src_path) {
|
if !self.valid_source_for_ingress(ingress, &header.src_path) {
|
||||||
return Ok(EndpointOutcome {
|
return Ok(EndpointOutcome {
|
||||||
dropped: true,
|
dropped: true,
|
||||||
@@ -548,7 +572,7 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
validate_call(&header, &message)?;
|
validate_call(header, &message)?;
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Child(idx) => Ok(EndpointOutcome {
|
RouteDecision::Child(idx) => Ok(EndpointOutcome {
|
||||||
forwards: vec![(RouteDecision::Child(idx), frame)],
|
forwards: vec![(RouteDecision::Child(idx), frame)],
|
||||||
@@ -562,14 +586,22 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
dropped: true,
|
dropped: true,
|
||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
}),
|
}),
|
||||||
RouteDecision::Local => self.handle_local_call(header, message),
|
RouteDecision::Local => self.handle_local_call(header.clone(), message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PacketType::Data => {
|
PacketType::Data => {
|
||||||
let message = parsed.deserialize_data()?;
|
let message = parsed.deserialize_data()?;
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Local => self.handle_local_data(header, message),
|
RouteDecision::Local => self.handle_local_data(header.clone(), message),
|
||||||
_ => Ok(EndpointOutcome {
|
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,
|
dropped: true,
|
||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
}),
|
}),
|
||||||
@@ -578,8 +610,16 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
PacketType::Fault => {
|
PacketType::Fault => {
|
||||||
let message = parsed.deserialize_fault()?;
|
let message = parsed.deserialize_fault()?;
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Local => self.handle_local_fault(header, message),
|
RouteDecision::Local => self.handle_local_fault(header.clone(), message),
|
||||||
_ => Ok(EndpointOutcome {
|
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,
|
dropped: true,
|
||||||
..EndpointOutcome::default()
|
..EndpointOutcome::default()
|
||||||
}),
|
}),
|
||||||
|
|||||||
+27
-14
@@ -42,34 +42,47 @@ pub struct ActiveHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Durable hook state tables.
|
/// Durable hook state tables.
|
||||||
#[derive(Debug, Default)]
|
/// Durable hook state tables.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct HookTable {
|
pub struct HookTable {
|
||||||
pending: BTreeMap<HookKey, PendingHook>,
|
pending: BTreeMap<HookKey, PendingHook>,
|
||||||
active: BTreeMap<HookKey, ActiveHook>,
|
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 {
|
impl HookTable {
|
||||||
pub fn allocate_hook_id(&self, return_path: &[String]) -> u64 {
|
pub fn allocate_hook_id(&mut self, _return_path: &[String]) -> u64 {
|
||||||
let mut hook_id = 0u64;
|
let id = self.next_id;
|
||||||
loop {
|
self.next_id = self.next_id.wrapping_add(1);
|
||||||
let key = HookKey::new(return_path.to_vec(), hook_id);
|
id
|
||||||
if !self.pending.contains_key(&key) && !self.active.contains_key(&key) {
|
|
||||||
return hook_id;
|
|
||||||
}
|
|
||||||
hook_id = hook_id.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_pending(&mut self, pending: PendingHook) {
|
pub fn insert_pending(&mut self, pending: PendingHook) -> Result<(), ()> {
|
||||||
// WARNING: hook tables intentionally own their path and procedure strings.
|
|
||||||
// Hook state must outlive any individual frame buffer.
|
|
||||||
let key = HookKey::new(pending.return_path.clone(), pending.hook_id);
|
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);
|
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);
|
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);
|
self.active.insert(key, active);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate_pending(&mut self, key: &HookKey, peer_path: Vec<String>) -> Option<()> {
|
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.
|
/// Trait for resolving a destination path to a routing decision.
|
||||||
pub trait RouteProvider {
|
pub trait RouteProvider {
|
||||||
/// Computes the routing decision for a destination path.
|
fn route_destination<I>(
|
||||||
fn route_destination(
|
|
||||||
&self,
|
&self,
|
||||||
local_path: &[String],
|
local_path: &[String],
|
||||||
child_paths: &[Vec<String>],
|
child_paths: I,
|
||||||
has_parent: bool,
|
has_parent: bool,
|
||||||
dst_path: &[String],
|
dst_path: &[String],
|
||||||
) -> RouteDecision;
|
) -> RouteDecision
|
||||||
|
where
|
||||||
|
I: IntoIterator,
|
||||||
|
I::Item: AsRef<[String]>,
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default routing implementation using the protocol's longest-prefix rule.
|
/// Default routing implementation using the protocol's longest-prefix rule.
|
||||||
pub struct DefaultRouteProvider;
|
pub struct DefaultRouteProvider;
|
||||||
|
|
||||||
impl RouteProvider for DefaultRouteProvider {
|
impl RouteProvider for DefaultRouteProvider {
|
||||||
fn route_destination(
|
fn route_destination<I>(
|
||||||
&self,
|
&self,
|
||||||
local_path: &[String],
|
local_path: &[String],
|
||||||
child_paths: &[Vec<String>],
|
child_paths: I,
|
||||||
has_parent: bool,
|
has_parent: bool,
|
||||||
dst_path: &[String],
|
dst_path: &[String],
|
||||||
) -> RouteDecision {
|
) -> RouteDecision
|
||||||
let child = child_paths
|
where
|
||||||
.iter()
|
I: IntoIterator,
|
||||||
.enumerate()
|
I::Item: AsRef<[String]>,
|
||||||
.filter(|(_, child_path)| is_prefix(child_path, dst_path))
|
{
|
||||||
.max_by_key(|(_, child_path)| child_path.len())
|
let mut best_index = None;
|
||||||
.map(|(index, _)| index);
|
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);
|
return RouteDecision::Child(index);
|
||||||
}
|
}
|
||||||
if local_path == dst_path {
|
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],
|
local_path: &[String],
|
||||||
child_paths: &[Vec<String>],
|
child_paths: I,
|
||||||
has_parent: bool,
|
has_parent: bool,
|
||||||
dst_path: &[String],
|
dst_path: &[String],
|
||||||
) -> RouteDecision {
|
) -> RouteDecision
|
||||||
|
where
|
||||||
|
I: IntoIterator,
|
||||||
|
I::Item: AsRef<[String]>,
|
||||||
|
{
|
||||||
DefaultRouteProvider.route_destination(local_path, child_paths, has_parent, dst_path)
|
DefaultRouteProvider.route_destination(local_path, child_paths, has_parent, dst_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +158,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
provider.route_destination(
|
provider.route_destination(
|
||||||
&Vec::<String>::new(),
|
&Vec::<String>::new(),
|
||||||
&children,
|
children,
|
||||||
false,
|
false,
|
||||||
&[String::from("a"), String::from("b"), String::from("c")]
|
&[String::from("a"), String::from("b"), String::from("c")]
|
||||||
),
|
),
|
||||||
|
|||||||
+8
-12
@@ -72,17 +72,13 @@ pub struct FaultMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stable protocol fault set.
|
/// Stable protocol fault set.
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ProtocolFault {
|
pub struct ProtocolFault(pub u8);
|
||||||
/// The destination leaf does not exist.
|
|
||||||
UnknownLeaf = 0x01,
|
impl ProtocolFault {
|
||||||
/// The destination does not support the requested procedure.
|
pub const UNKNOWN_LEAF: Self = Self(0x01);
|
||||||
UnknownProcedure = 0x02,
|
pub const UNKNOWN_PROCEDURE: Self = Self(0x02);
|
||||||
/// The source path was invalid for the receiving connection.
|
pub const INVALID_SOURCE_PATH: Self = Self(0x03);
|
||||||
InvalidSourcePath = 0x03,
|
pub const INVALID_HOOK_PEER: Self = Self(0x04);
|
||||||
/// The sender did not match the expected hook peer.
|
pub const INTERNAL_ERROR: Self = Self(0x05);
|
||||||
InvalidHookPeer = 0x04,
|
|
||||||
/// The endpoint encountered an internal processing failure.
|
|
||||||
InternalError = 0x05,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ pub enum ValidationError {
|
|||||||
ProcedureId(&'static str),
|
ProcedureId(&'static str),
|
||||||
/// Call-specific invariants were violated.
|
/// Call-specific invariants were violated.
|
||||||
CallInvariant(&'static str),
|
CallInvariant(&'static str),
|
||||||
|
/// The hook identifier is already in use.
|
||||||
|
InvalidHookId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ValidationError {
|
impl fmt::Display for ValidationError {
|
||||||
@@ -22,6 +24,7 @@ impl fmt::Display for ValidationError {
|
|||||||
Self::HeaderInvariant(message) => write!(f, "invalid header: {message}"),
|
Self::HeaderInvariant(message) => write!(f, "invalid header: {message}"),
|
||||||
Self::ProcedureId(message) => write!(f, "invalid procedure id: {message}"),
|
Self::ProcedureId(message) => write!(f, "invalid procedure id: {message}"),
|
||||||
Self::CallInvariant(message) => write!(f, "invalid call: {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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut segments = procedure_id.split('.');
|
if procedure_id.is_empty() {
|
||||||
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() {
|
|
||||||
return Err(ValidationError::ProcedureId(
|
return Err(ValidationError::ProcedureId(
|
||||||
"must contain exactly 5 segments",
|
"procedure identifier cannot be empty except for introspection",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = collected[2];
|
if !procedure_id.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '.') {
|
||||||
let Some(suffix) = version.strip_prefix('v') else {
|
|
||||||
return Err(ValidationError::ProcedureId(
|
return Err(ValidationError::ProcedureId(
|
||||||
"third segment must be a version like v1",
|
"procedure identifier should use alphanumeric characters, dots, and underscores",
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ proc-macro = true
|
|||||||
base62 = {path = "../base62"}
|
base62 = {path = "../base62"}
|
||||||
|
|
||||||
# aes = "0.8.4"
|
# aes = "0.8.4"
|
||||||
block-padding = "0.4.1"
|
block-padding = "0.4.2"
|
||||||
# cbc = "0.1.2"
|
getrandom = "0.4.2"
|
||||||
getrandom = "0.3.4"
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hex-literal = "1.1.0"
|
hex-literal = "1.1.0"
|
||||||
# regex = "1.12.2"
|
# regex = "1.12.2"
|
||||||
@@ -29,7 +28,7 @@ hex-literal = "1.1.0"
|
|||||||
static_init = { workspace = true }
|
static_init = { workspace = true }
|
||||||
|
|
||||||
# Specific
|
# Specific
|
||||||
quote = "1.0.42"
|
quote = "1.0.45"
|
||||||
syn = {version = "2.0.109", features = ["full"]}
|
syn = {version = "2.0.117", features = ["full"]}
|
||||||
proc-macro2 = "1.0.103"
|
proc-macro2 = "1.0.106"
|
||||||
rand = "0.9.2"
|
rand = "0.10.1"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use rand::rngs::SmallRng;
|
use rand::rngs::{SmallRng, StdRng};
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, RngExt, SeedableRng};
|
||||||
use syn::{LitFloat, parse_macro_input};
|
use syn::{LitFloat, parse_macro_input};
|
||||||
|
|
||||||
// const MIN_TAGS: u32 = 1; // Maximum instructions per recursive block
|
// 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);
|
// let final_weight = input_weight.unwrap_or(WEIGHT);
|
||||||
|
|
||||||
// 2. Setup
|
// 2. Setup
|
||||||
let mut rng = SmallRng::from_os_rng();
|
let mut rng = SmallRng::from_seed(rand::random());
|
||||||
|
|
||||||
let count = {
|
let count = {
|
||||||
let mut n = 1;
|
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);
|
//! 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.
|
/// TTY endpoint - provides PTY streaming functionality.
|
||||||
pub struct TTY {
|
pub struct Tty {
|
||||||
name: String,
|
name: String,
|
||||||
sessions: HashMap<u16, Box<PtySession>>,
|
sessions: HashMap<u16, Box<PtySession>>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
next_id: u16,
|
next_id: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for TTY {
|
impl fmt::Debug for Tty {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("TTY")
|
f.debug_struct("Tty")
|
||||||
.field("name", &self.name)
|
.field("name", &self.name)
|
||||||
.field("sessions", &self.sessions.len())
|
.field("sessions", &self.sessions.len())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TTY {
|
impl Tty {
|
||||||
/// Create a new TTY endpoint.
|
/// Create a new TTY endpoint.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -118,11 +118,11 @@ impl TTY {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::execl(
|
libc::execl(
|
||||||
"/bin/sh\0".as_ptr() as *const libc::c_char,
|
c"/bin/sh\0".as_ptr() as *const libc::c_char,
|
||||||
"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>(),
|
std::ptr::null::<libc::c_char>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe { libc::exit(1) };
|
unsafe { libc::exit(1) };
|
||||||
@@ -175,7 +175,7 @@ impl TTY {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Endpoint for TTY {
|
impl Endpoint for Tty {
|
||||||
fn handle_request(
|
fn handle_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: &TreeRequest,
|
request: &TreeRequest,
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ impl Transport for TcpTransport {
|
|||||||
|
|
||||||
let header_bytes = self.read_exact(header_len)?;
|
let header_bytes = self.read_exact(header_len)?;
|
||||||
let header =
|
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_bytes = self.read_exact(4)?;
|
||||||
let payload_len =
|
let payload_len =
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ use std::result::Result;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::fmt;
|
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.
|
/// A node in the tree - contains an optional endpoint and child nodes.
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
endpoint: Option<Arc<Mutex<Box<dyn Endpoint + 'static>>>>,
|
endpoint: Option<SharedEndpoint>,
|
||||||
children: BTreeMap<String, Node>,
|
children: BTreeMap<String, Node>,
|
||||||
streams: BTreeMap<u16, Stream>,
|
streams: BTreeMap<u16, Stream>,
|
||||||
next_stream_id: u16,
|
next_stream_id: u16,
|
||||||
@@ -234,7 +237,7 @@ impl Tree {
|
|||||||
/// Find the handler for a given path using longest-prefix matching
|
/// Find the handler for a given path using longest-prefix matching
|
||||||
///
|
///
|
||||||
/// Returns the endpoint and the matched path
|
/// 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 == "/" {
|
if path == "/" {
|
||||||
return self.root.endpoint.as_ref().map(|e| (e.clone(), "/"));
|
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")?;
|
let stream_id = header.stream_id.ok_or("no stream_id")?;
|
||||||
|
|
||||||
// Find the node containing this stream
|
// Find the node containing this stream
|
||||||
fn find_stream_handler(node: &mut Node, sid: u16) -> Option<Arc<Mutex<Box<dyn Endpoint>>>> {
|
fn find_stream_handler(node: &mut Node, sid: u16) -> Option<SharedEndpoint> {
|
||||||
if node.streams.get(&sid).is_some() {
|
if node.streams.contains_key(&sid) {
|
||||||
return node.endpoint.clone();
|
return node.endpoint.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in node.children.values_mut() {
|
for child in node.children.values_mut() {
|
||||||
if let Some(h) = find_stream_handler(child, sid) {
|
if let Some(h) = find_stream_handler(child, sid) {
|
||||||
|
|||||||
Reference in New Issue
Block a user