Compare commits

...

2 Commits

Author SHA1 Message Date
d8356ecb6c Wrote b3270 arbiter 2023-05-11 01:24:11 +02:00
2b180d9a95 Lots of changes 2023-05-10 16:04:43 +02:00
16 changed files with 1159 additions and 81 deletions

338
Cargo.lock generated
View File

@@ -56,6 +56,15 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "aho-corasick"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -65,6 +74,15 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.71"
@@ -318,6 +336,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bincode"
version = "1.3.3"
@@ -380,9 +404,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.12.1"
version = "3.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
[[package]]
name = "byteorder"
@@ -601,17 +625,33 @@ dependencies = [
]
[[package]]
name = "d3270"
name = "d3270-common"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 2.2.1",
"serde",
"serde_cbor",
"serde_json",
]
[[package]]
name = "d3270d"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.21.0",
"bytes",
"d3270-common",
"futures",
"rand 0.8.5",
"serde",
"serde_json",
"tide",
"tide-websockets",
"tokio",
"tokio-stream",
"tracing",
"tracing-fmt",
]
[[package]]
@@ -705,6 +745,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.28"
@@ -712,6 +767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -720,6 +776,17 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.28"
@@ -770,10 +837,13 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite 0.2.9",
"pin-utils",
"slab",
@@ -834,10 +904,13 @@ dependencies = [
]
[[package]]
name = "half"
version = "1.8.2"
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
@@ -990,7 +1063,7 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.48.0",
]
@@ -1003,9 +1076,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "js-sys"
version = "0.3.61"
version = "0.3.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5"
dependencies = [
"wasm-bindgen",
]
@@ -1020,10 +1093,16 @@ dependencies = [
]
[[package]]
name = "libc"
version = "0.2.143"
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "link-cplusplus"
@@ -1051,6 +1130,21 @@ dependencies = [
"value-bag",
]
[[package]]
name = "matchers"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
dependencies = [
"regex-automata",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.5.0"
@@ -1088,6 +1182,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi 0.2.6",
"libc",
]
[[package]]
name = "once_cell"
version = "1.17.1"
@@ -1100,6 +1204,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "owning_ref"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "parking"
version = "2.1.0"
@@ -1198,6 +1311,10 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "qt3270"
version = "0.1.0"
[[package]]
name = "quote"
version = "1.0.26"
@@ -1278,6 +1395,38 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "regex"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.1",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "route-recognizer"
version = "0.2.0"
@@ -1343,16 +1492,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.162"
@@ -1485,6 +1624,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [
"maybe-uninit",
]
[[package]]
name = "socket2"
version = "0.4.9"
@@ -1495,6 +1643,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "standback"
version = "0.2.17"
@@ -1726,14 +1880,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.28.0"
version = "1.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105"
dependencies = [
"autocfg",
"bytes",
"libc",
"mio",
"num_cpus",
"pin-project-lite 0.2.9",
"signal-hook-registry",
"tokio-macros",
@@ -1751,6 +1906,101 @@ dependencies = [
"syn 2.0.15",
]
[[package]]
name = "tokio-stream"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [
"futures-core",
"pin-project-lite 0.2.9",
"tokio",
"tokio-util",
]
[[package]]
name = "tokio-util"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite 0.2.9",
"tokio",
]
[[package]]
name = "tracing"
version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if 1.0.0",
"pin-project-lite 0.2.9",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "tracing-core"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-fmt"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "880547feb88739526e322a366be2411c41c797f0dabcddfe99a3216e5a664f71"
dependencies = [
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "192ca16595cdd0661ce319e8eede9c975f227cdaabc4faaefdc256f43d852e45"
dependencies = [
"ansi_term",
"chrono",
"lazy_static",
"matchers",
"owning_ref",
"regex",
"smallvec",
"tracing-core",
"tracing-log",
]
[[package]]
name = "tungstenite"
version = "0.13.0"
@@ -1832,6 +2082,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.0.0-alpha.9"
@@ -1878,9 +2134,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.84"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4"
dependencies = [
"cfg-if 1.0.0",
"serde",
@@ -1890,24 +2146,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.84"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.15",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.34"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
@@ -1917,9 +2173,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.84"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1927,28 +2183,28 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.84"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.15",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.84"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb"
[[package]]
name = "web-sys"
version = "0.3.61"
version = "0.3.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@@ -1,16 +1,16 @@
[package]
name = "d3270"
name = "d3270-common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.28.0", features = ["rt", "macros", "sync", "process"] }
tide = "0.16.0"
tide-websockets = "0.4.0"
serde = { version = "1.0.162", features = ["derive"]}
serde_json = "1.0.96"
serde_cbor = "0.11.2"
#serde_cbor = "0.11.2"
anyhow = "1.0.71"
bitflags = "2.2.1"
bitflags = "2.2.1"
# deps for bins
#structopt = "0.3.26"

View File

@@ -87,18 +87,22 @@ pub enum InitializeIndication {
Models(Vec<Model>),
/// Change in the state of the Operator Information Area
Oia(Oia),
/// List of supported proxies
Proxies(Vec<Proxy>),
/// Set of supported prefixes
Prefixes{value: String},
/// List of supported proxies
Proxies(Vec<Proxy>),
/// Screen dimensions/characteristics changed
ScreenMode(ScreenMode),
/// Setting changed
Setting(Setting),
/// Scroll thumb position
Thumb(Thumb),
/// Indicates build-time TLS config
TlsHello(TlsHello),
/// TLS state changed
Tls(Tls),
/// Trace file
TraceFile(TraceFile),
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]

View File

@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use crate::b3270::types::GraphicRendition;
use crate::b3270::types::{Color, GraphicRendition};
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename_all="kebab-case")]
@@ -69,7 +69,7 @@ pub enum ConnectionState {
ConnectedETn3270e,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
#[serde(rename_all="kebab-case")]
pub struct Erase {
#[serde(default, skip_serializing_if="Option::is_none")]
@@ -147,18 +147,54 @@ pub enum OiaField {
value: bool,
},
/// Screen trace count
Screentrace {
ScreenTrace {
value: String,
},
/// Host command timer (minutes:seconds)
Script {
value: String,
},
Timing {
#[serde(default, skip_serializing_if="Option::is_none")]
value: Option<String>,
},
Typeahead {
value: bool,
}
},
}
#[derive(Copy, Clone, Debug, Eq, Ord, PartialOrd, PartialEq, Hash)]
pub enum OiaFieldName {
Compose,
Insert,
Lock,
Lu,
NotUndera,
PrinterSession,
ReverseInput,
ScreenTrace,
Script,
Timing,
Typeahead,
}
impl OiaField {
pub fn field_name(&self) -> OiaFieldName {
match self {
OiaField::Compose {..} => OiaFieldName::Compose,
OiaField::Insert {..} => OiaFieldName::Insert,
OiaField::Lock {..} => OiaFieldName::Lock,
OiaField::Lu {..} => OiaFieldName::Lu,
OiaField::NotUndera {..} => OiaFieldName::NotUndera,
OiaField::PrinterSession {..} => OiaFieldName::PrinterSession,
OiaField::ReverseInput {..} => OiaFieldName::ReverseInput,
OiaField::ScreenTrace {..} => OiaFieldName::ScreenTrace,
OiaField::Script {..} => OiaFieldName::Script,
OiaField::Timing {..} => OiaFieldName::Timing,
OiaField::Typeahead {..} => OiaFieldName::Typeahead,
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Proxy {
pub name: String,
@@ -211,7 +247,7 @@ pub struct ConnectAttempt {
pub port: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
// TODO: change this to an enum
pub struct Cursor {
pub enabled: bool,
@@ -342,27 +378,6 @@ pub struct Scroll {
pub bg: Option<Color>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename="camelCase")]
pub enum Color {
NeutralBlack,
Blue,
Red,
Pink,
Green,
Turquoise,
Yellow,
NeutralWhite,
Black,
DeepBlue,
Orange,
Purple,
PaleGreen,
PaleTurquoise,
Gray,
White,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename="kebab-case")]
pub struct Stats {
@@ -412,3 +427,13 @@ pub struct UiError {
#[serde(default, skip_serializing_if="Option::is_none")]
pub column: Option<usize>,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
pub fn connection_state_serializes_as_expected() {
assert_eq!(serde_json::to_string(&ConnectionState::ConnectedETn3270e).unwrap(),r#""connected-e-tn3270e""#);
assert_eq!(serde_json::to_string(&ConnectionState::ConnectedESscp).unwrap(),r#""connected-e-sscp""#);
}
}

View File

@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
// {"run":{"actions":[{"action":"Connect","args":["10.24.74.37:3270"]}]}}
// {"run":{"actions":"Key(a)"}}
// Operations
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]

View File

@@ -17,6 +17,7 @@ bitflags! {
const PRIVATE_USE = 0x080;
const NO_COPY = 0x100;
const WRAP = 0x200;
}
}
@@ -113,4 +114,115 @@ mod test {
fn from_str_1() {
assert_eq!(GraphicRendition::from_str("underline,blink"), Ok(GraphicRendition::BLINK | GraphicRendition::UNDERLINE))
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename="camelCase")]
#[repr(u8)]
pub enum Color {
NeutralBlack,
Blue,
Red,
Pink,
Green,
Turquoise,
Yellow,
NeutralWhite,
Black,
DeepBlue,
Orange,
Purple,
PaleGreen,
PaleTurquoise,
Gray,
White,
}
impl From<Color> for u8 {
fn from(value: Color) -> Self {
use Color::*;
match value {
NeutralBlack => 0,
Blue => 1,
Red => 2,
Pink => 3,
Green => 4,
Turquoise => 5,
Yellow => 6,
NeutralWhite => 7,
Black => 8,
DeepBlue => 9,
Orange => 10,
Purple => 11,
PaleGreen => 12,
PaleTurquoise => 13,
Gray => 14,
White => 15,
}
}
}
impl From<u8> for Color {
fn from(value: u8) -> Self {
use Color::*;
match value & 0xF {
0 => NeutralBlack,
1 => Blue,
2 => Red,
3 => Pink,
4 => Green,
5 => Turquoise,
6 => Yellow,
7 => NeutralWhite,
8 => Black,
9 => DeepBlue,
10 => Orange,
11 => Purple,
12 => PaleGreen,
13 => PaleTurquoise,
14 => Gray,
15 => White,
_ => unreachable!(),
}
}
}
pub trait PackedAttr {
fn c_gr(self) -> GraphicRendition;
fn c_fg(self) -> Color;
fn c_bg(self) -> Color;
fn c_setgr(self, gr: GraphicRendition) -> Self;
fn c_setfg(self, fg: Color) -> Self;
fn c_setbg(self, bg: Color) -> Self;
fn c_pack(fg: Color, bg: Color, gr: GraphicRendition) -> Self;
}
impl PackedAttr for u32 {
fn c_gr(self) -> GraphicRendition {
GraphicRendition::from_bits_truncate((self & 0xFFFF) as u16)
}
fn c_fg(self) -> Color {
((self >> 16 & 0xF) as u8).into()
}
fn c_bg(self) -> Color {
((self >> 20 & 0xF) as u8).into()
}
fn c_setgr(self, gr: GraphicRendition) -> Self {
self & !0xFFFF | gr.bits() as u32
}
fn c_setfg(self, fg: Color) -> Self {
self & !0xF0000 | (u8::from(fg) as u32) << 16
}
fn c_setbg(self, bg: Color) -> Self {
self & !0xF0000 | (u8::from(bg) as u32) << 20
}
fn c_pack(fg: Color, bg: Color, gr: GraphicRendition) -> Self {
0.c_setfg(fg).c_setbg(bg).c_setgr(gr)
}
}

View File

@@ -1,2 +1,4 @@
pub mod b3270;
pub mod tracker;
pub mod executor;

View File

331
d3270-common/src/tracker.rs Normal file
View File

@@ -0,0 +1,331 @@
use std::collections::HashMap;
use crate::b3270::indication::{Change, Connection, ConnectionState, CountOrText, Cursor, Erase, Oia, OiaFieldName, Row, RunResult, Screen, ScreenMode, Scroll, Setting, TerminalName, Thumb, Tls, TraceFile};
use crate::b3270::{Indication, InitializeIndication};
use crate::b3270::types::{Color, GraphicRendition, PackedAttr};
#[derive(Copy, Clone, Debug)]
struct CharCell {
pub ch: char,
pub attr: u32,
}
pub struct Tracker {
screen: Vec<Vec<CharCell>>,
oia: HashMap<OiaFieldName, Oia>,
screen_mode: ScreenMode,
erase: Erase,
thumb: Thumb,
settings: HashMap<String, Setting>,
// These are not init indications, but need to be tracked anyways
cursor: Cursor,
connection: Connection,
formatted: bool,
terminal_name: Option<TerminalName>,
trace_file: Option<String>,
tls: Option<Tls>,
// These never change, but need to be represented in an initialize message
static_init: Vec<InitializeIndication>,
}
#[derive(Clone, Debug)]
pub enum Disposition {
// Deliver this indication to every connected client
Broadcast,
// Ignore this message
Drop,
// Send this message to one particular client
Direct(String),
}
impl Tracker {
pub fn handle_indication(&mut self, indication: &mut Indication) -> Disposition {
match indication {
Indication::Bell { .. }
| Indication::ConnectAttempt(_)
| Indication::Flipped { .. }
| Indication::Font { .. }
| Indication::Icon { .. }
| Indication::Popup(_)
| Indication::Stats(_)
| Indication::WindowTitle { .. }
=> (),
Indication::Connection(conn) => {
self.connection = conn.clone();
}
Indication::Erase(erase) => {
self.erase.logical_cols = erase.logical_cols.or(self.erase.logical_cols);
self.erase.logical_rows = erase.logical_rows.or(self.erase.logical_rows);
self.erase.fg = erase.fg.or(self.erase.fg);
self.erase.bg = erase.bg.or(self.erase.bg);
let rows = self.erase.logical_rows.unwrap_or(self.screen_mode.rows) as usize;
let cols = self.erase.logical_cols.unwrap_or(self.screen_mode.cols) as usize;
self.screen = vec![
vec![CharCell{
attr: u32::c_pack(
erase.fg.unwrap_or(Color::NeutralBlack),
erase.bg.unwrap_or(Color::Blue),
GraphicRendition::empty(),
),
ch: ' ',
};cols]
; rows
]
}
Indication::Formatted { state } => {self.formatted = *state; }
Indication::Initialize(init) => {
let mut static_init = Vec::with_capacity(init.len());
for indicator in init.clone() {
match indicator {
InitializeIndication::CodePages(_) |
InitializeIndication::Hello(_) |
InitializeIndication::Models(_) |
InitializeIndication::Prefixes { .. } |
InitializeIndication::Proxies(_) |
InitializeIndication::TlsHello(_) |
InitializeIndication::Tls(_) |
InitializeIndication::TraceFile(_) =>
static_init.push(indicator),
// The rest are passed through to normal processing.
InitializeIndication::Thumb(thumb) => {
self.handle_indication(&mut Indication::Thumb(thumb));
},
InitializeIndication::Setting(setting) => {
self.handle_indication(&mut Indication::Setting(setting));
}
InitializeIndication::ScreenMode(mode) => {
self.handle_indication(&mut Indication::ScreenMode(mode));
},
InitializeIndication::Oia(oia) => {
self.handle_indication(&mut Indication::Oia(oia));
}
InitializeIndication::Erase(erase) => {
self.handle_indication(&mut Indication::Erase(erase));
}
InitializeIndication::Connection(conn) => {
self.handle_indication(&mut Indication::Connection(conn));
}
}
}
}
Indication::Oia(oia) => {
self.oia.insert(oia.field.field_name(), oia.clone());
}
Indication::Screen(screen) => {
if let Some(cursor) = screen.cursor {
self.cursor = cursor;
}
for row in screen.rows.iter() {
let row_idx = row.row as usize - 1;
for change in row.changes.iter() {
let col_idx = change.column as usize - 1;
// update screen contents
let cols = self.screen[row_idx].iter_mut().skip(col_idx);
match change.change {
CountOrText::Count(n) => {
cols.take(n).for_each(|cell| {
let mut attr = cell.attr;
if let Some(fg) = change.fg {
attr = attr.c_setfg(fg);
}
if let Some(bg) = change.bg {
attr = attr.c_setbg(bg);
}
if let Some(gr) = change.gr {
attr = attr.c_setgr(gr);
}
cell.attr = attr;
})
}
CountOrText::Text(ref text) => {
cols.zip(text.chars()).for_each(|(cell, ch)| {
let mut attr = cell.attr;
if let Some(fg) = change.fg {
attr = attr.c_setfg(fg);
}
if let Some(bg) = change.bg {
attr = attr.c_setbg(bg);
}
if let Some(gr) = change.gr {
attr = attr.c_setgr(gr);
}
cell.attr = attr;
cell.ch = ch;
});
}
}
}
}
}
Indication::ScreenMode(mode) => {
self.screen_mode = *mode;
self.handle_indication(&mut Indication::Erase(Erase{
logical_rows: Some(self.screen_mode.rows),
logical_cols: Some(self.screen_mode.cols),
fg: None,
bg: None,
}));
}
Indication::Scroll(Scroll{ fg, bg }) => {
let fg = fg.or(self.erase.fg).unwrap_or(Color::Blue);
let bg = bg.or(self.erase.bg).unwrap_or(Color::NeutralBlack);
let mut row = self.screen.remove(0);
row.fill(CharCell{
attr: u32::c_pack(fg, bg, GraphicRendition::empty()),
ch: ' ',
});
self.screen.push(row);
}
Indication::Setting(setting) => {
self.settings.insert(setting.name.clone(), setting.clone());
}
Indication::TerminalName(term) => {
self.terminal_name = Some(term.clone());
}
Indication::Thumb(thumb) => {
self.thumb = thumb.clone();
}
Indication::TraceFile(TraceFile{name}) => {
self.trace_file = name.clone();
}
Indication::Tls(tls) => {
self.tls = Some(tls.clone());
}
// These need direction
Indication::UiError(_) => {} // we can assume that this came from the last sent command
Indication::Passthru(_) => {} // dunno how to handle this one
Indication::FileTransfer(_) => {}
Indication::RunResult(RunResult{r_tag, ..}) => {
if let Some(dest) = r_tag {
return Disposition::Direct(dest.clone());
} else {
return Disposition::Drop;
}
}
}
return Disposition::Broadcast
}
pub fn get_init_indication(&self) -> Vec<Indication> {
let mut contents = self.static_init.clone();
contents.push(InitializeIndication::ScreenMode(self.screen_mode));
contents.push(InitializeIndication::Erase(self.erase));
contents.push(InitializeIndication::Thumb(self.thumb));
contents.extend(self.oia.values()
.cloned()
.map(InitializeIndication::Oia));
contents.extend(self.settings.values()
.cloned()
.map(InitializeIndication::Setting));
contents.extend(self.tls.clone().map(InitializeIndication::Tls));
// Construct a screen snapshot
let mut result = vec![
Indication::Initialize(contents),
Indication::Connection(self.connection.clone()),
Indication::Screen(self.screen_snapshot()),
Indication::Formatted {state: self.formatted},
];
if let Some(terminal_name) = self.terminal_name.clone() {
result.push(Indication::TerminalName(terminal_name));
}
if let Some(trace_file) = self.trace_file.clone() {
result.push(Indication::TraceFile(TraceFile {
name: Some(trace_file),
}))
}
result
}
fn format_row(mut row: &[CharCell]) -> Vec<Change> {
let mut result = vec![];
let mut column = 1;
while !row.is_empty() {
let cur_gr = row[0].attr;
let split_pt = row.iter().take_while(|ch| ch.attr == cur_gr).count();
let (first, rest) = row.split_at(split_pt);
row = rest;
let content = first.iter().map(|cell| cell.ch).collect();
result.push(Change{
column,
fg: Some(cur_gr.c_fg()),
bg: Some(cur_gr.c_bg()),
gr: Some(cur_gr.c_gr()),
change: CountOrText::Text(content),
});
column += first.len() as u8;
}
result
}
fn screen_snapshot(&self) -> Screen {
Screen{
cursor: Some(self.cursor),
rows: self.screen.iter()
.map(Vec::as_slice).map(Self::format_row)
.enumerate()
.map(|(row_id, changes)| Row{
row: row_id as u8 - 1,
changes,
})
.collect()
}
}
}
impl Default for Tracker {
fn default() -> Self {
let ret = Self {
screen: vec![],
oia: Default::default(),
screen_mode: ScreenMode {
cols: 80,
rows: 43,
color: true,
model: 4,
extended: true,
oversize: false,
},
erase: Erase {
logical_rows: None,
logical_cols: None,
fg: None,
bg: None,
},
thumb: Thumb {
top: 0.0,
shown: 0.0,
saved: 0,
screen: 0,
back: 0,
},
settings: Default::default(),
cursor: Cursor {
enabled: false,
row: None,
column: None
},
connection: Connection {
state: ConnectionState::NotConnected,
host: None,
cause: None,
},
formatted: false,
terminal_name: None,
trace_file: None,
tls: None,
static_init: vec![],
};
ret
}
}

22
d3270d/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "d3270d"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0.162", features = ["derive"]}
serde_json = "1.0.96"
anyhow = "1.0.71"
tokio = { version = "1.28.0", features = ["rt", "macros", "sync", "process", "rt-multi-thread", "bytes", "io-util"] }
tide = "0.16.0"
tide-websockets = "0.4.0"
d3270-common = {path = "../d3270-common"}
bytes = "1.4.0"
tracing = "0.1.37"
tracing-fmt = "0.1.1"
futures = "0.3.28"
tokio-stream = { version = "0.1.14", features = ["sync"] }
rand = "0.8.5"
base64 = "0.21.0"

263
d3270d/src/connection.rs Normal file
View File

@@ -0,0 +1,263 @@
use std::collections::{HashMap, VecDeque};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use anyhow::anyhow;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64_STANDARD;
use bytes::Buf;
use futures::{FutureExt, Stream, StreamExt, TryFutureExt};
use futures::future::BoxFuture;
use rand::RngCore;
use tokio::io::{BufReader, AsyncBufReadExt, Lines, AsyncWrite};
use tokio::process::{Child, ChildStdout};
use tokio::sync::{mpsc, oneshot, broadcast};
use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
use tracing::{error, info, warn};
use d3270_common::b3270::{Indication, Operation, operation};
use d3270_common::b3270::indication::RunResult;
use d3270_common::b3270::operation::Action;
use d3270_common::tracker::{Disposition, Tracker};
pub struct B3270 {
tracker: Tracker, //
child: Child, //
comm: mpsc::Receiver<B3270Request>, //
ind_chan: broadcast::Sender<Indication>, //
child_reader: Lines<BufReader<ChildStdout>>, //
write_buf: VecDeque<u8>,
action_response_map: HashMap<String, oneshot::Sender<RunResult>>, //
}
pub enum B3270Request {
Action(Vec<Action>, oneshot::Sender<RunResult>),
Resync(oneshot::Sender<(Vec<Indication>, broadcast::Receiver<Indication>)>),
}
enum HandleReceiveState {
Steady(BroadcastStream<Indication>),
Wait(oneshot::Receiver<(Vec<Indication>, broadcast::Receiver<Indication>)>),
Resume(std::vec::IntoIter<Indication>, broadcast::Receiver<Indication>),
TryRestart(BoxFuture<'static, Result<(), ()>>, oneshot::Receiver<(Vec<Indication>, broadcast::Receiver<Indication>)>),
}
pub struct Handle {
sender: mpsc::Sender<B3270Request>,
receiver: Option<HandleReceiveState>,
}
impl Stream for Handle {
type Item = Indication;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// iter tracks whether any progress has been made
loop {
match self.receiver.take() {
Some(HandleReceiveState::TryRestart(mut fut, receiver)) => {
if fut.poll_unpin(cx).is_pending() {
self.receiver = Some(HandleReceiveState::TryRestart(fut, receiver));
return Poll::Pending;
}
// The option is only there to solve a lifetime issue, so this unwrap is safe
self.receiver = Some(HandleReceiveState::Wait(receiver));
}
Some(HandleReceiveState::Wait(mut rcvr)) => {
match rcvr.poll_unpin(cx) {
Poll::Ready(Ok((inds, rcvr))) => {
// reverse the indicators so that they can be popped.
self.receiver = Some(HandleReceiveState::Resume(inds.into_iter(), rcvr));
}
Poll::Ready(Err(error)) => {
warn!(%error, "unable to reconnect to b3270 server");
return Poll::Ready(None)
}
Poll::Pending => {
self.receiver = Some(HandleReceiveState::Wait(rcvr));
return Poll::Pending;
}
}
}
Some(HandleReceiveState::Resume(mut inds, rcvr)) => {
match inds.next() {
Some(next) => {
self.receiver = Some(HandleReceiveState::Resume(inds, rcvr));
return Poll::Ready(Some(next));
}
None => {
self.receiver = Some(HandleReceiveState::Steady(BroadcastStream::new(rcvr)));
}
}
}
Some(HandleReceiveState::Steady(mut rcvr)) => {
match rcvr.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(msg))) => {
self.receiver = Some(HandleReceiveState::Steady(rcvr));
return Poll::Ready(Some(msg))
}
Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(_)))) => {
warn!("Dropped messages from b3270 server; starting resync");
let (os_snd, os_rcv) = oneshot::channel();
let fut = self.sender.clone().reserve_owned()
.map_ok(move |permit| {
permit.send(B3270Request::Resync(os_snd));
})
.map_err(|_| ())
.boxed();
self.receiver = Some(HandleReceiveState::TryRestart(fut, os_rcv));
}
Poll::Ready(None) => {
warn!("Failed to receive from b3270 server");
return Poll::Ready(None)
},
Poll::Pending => return Poll::Pending
}
}
None => {
return Poll::Ready(None);
}
}
}
}
}
impl B3270 {
pub fn spawn(mut child: Child) -> (tokio::task::JoinHandle<anyhow::Error>, mpsc::Sender<B3270Request>) {
let (subproc_snd, subproc_rcv) = mpsc::channel(10);
let child_reader = child.stdout.take().expect("Should always be given a child that has stdout captured");
let child_reader = BufReader::new(child_reader).lines();
// A single connect can result in a flurry of messages, so we need a big buffer
let (ind_chan, _) = broadcast::channel(100);
let proc = B3270 {
child,
child_reader,
tracker: Tracker::default(),
comm: subproc_rcv,
ind_chan,
write_buf: VecDeque::new(),
action_response_map: Default::default(),
};
(tokio::task::spawn(proc), subproc_snd)
}
}
impl Future for B3270 {
type Output = anyhow::Error;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// try to read data from the child
let mut indications = vec![];
// handle new indications first, so that new subscribers get the results in the sync state.
while let Poll::Ready(buf) = Pin::new(&mut self.child_reader).poll_next_line(cx) {
match buf {
Ok(Some(line)) => match serde_json::from_str(&line) {
Ok(ind) => {
indications.push(ind)
},
Err(error) => {
warn!(%error, msg=line, "Failed to parse indication");
}
},
// EOF on stdin; this is a big problem
Ok(None) => return Poll::Ready(anyhow!("Child exited unexpectedly")),
Err(err) => return Poll::Ready(anyhow!(err).context("Failed to read from child")),
}
}
for mut ind in indications {
match self.tracker.handle_indication(&mut ind) {
Disposition::Broadcast => {
// It's OK to drop these, as anybody who cares will resync
self.ind_chan.send(ind).ok();
}
Disposition::Drop => {
// do nothing
}
Disposition::Direct(dst) => {
// TODO: handle this once we have a map of destinations.
if let Indication::RunResult(run_res) = ind {
if let Some(dest) = self.action_response_map.remove(&dst) {
// If this fails, whoever sent the request must not have cared.
dest.send(run_res).ok();
}
}
}
}
}
// check if the server has exited; if so, no sense looking at new connections
match self.child.try_wait() {
Ok(Some(status)) => {
info!(%status, "b3270 process exited");
return Poll::Ready(anyhow!("b3270 process exited"));
}
Ok(None) => {}
Err(error) => {
warn!(%error, "Failed to check status of b3270");
// TODO: should we end now?
}
}
// Only now do we handle connection requests. This way new connections
// cache the sync state in case we have multiple requests for it at once
let mut sync_state = None;
while let Poll::Ready(cmd) = self.comm.poll_recv(cx) {
match cmd {
None => {},
Some(B3270Request::Resync(sender)) => {
if sync_state.is_none() {
sync_state = Some(self.tracker.get_init_indication());
}
// it's OK for this to fail; we just don't get a new client
sender.send((sync_state.clone().unwrap(), self.ind_chan.subscribe())).ok();
}
Some(B3270Request::Action(actions, response_chan)) => {
let tag = 'find_tag: loop {
let tag = rand::thread_rng().next_u64().to_le_bytes();
let tag = B64_STANDARD.encode(tag);
if !self.action_response_map.contains_key(&tag) {
break 'find_tag tag;
}
};
let op = Operation::Run(operation::Run {
r_tag: Some(tag.clone()),
type_: Some("keymap".to_owned()),
actions,
});
let result = serde_json::to_writer(
&mut self.write_buf,
&op
);
match result {
Ok(()) => {
self.write_buf.push_back(b'\n');
self.action_response_map.insert(tag, response_chan);
},
Err(error) => error!(?op, %error, "Failed to serialize op"),
}
}
}
}
// Now, check if there's anything to be written
'write: while !self.write_buf.is_empty() {
let myself = &mut *self;
let chunk = myself.write_buf.chunk();
let stdin = Pin::new(myself.child.stdin.as_mut().expect("Should always have child stdin"));
match stdin.poll_write(cx, chunk) {
Poll::Pending | Poll::Ready(Ok(0)) => {
break 'write;
}
Poll::Ready(Ok(n)) => {
myself.write_buf.advance(n);
}
Poll::Ready(Err(error)) => {
warn!(%error, "Failed to write to b3270");
}
}
}
// We only complete when the child dies, which we catch above
Poll::Pending
}
}

55
d3270d/src/main.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::ffi::OsString;
use std::process::Stdio;
use std::str::FromStr;
use anyhow::anyhow;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut subprocess_args = vec![
OsString::from_str("-json").unwrap(),
];
let mut args_iter = std::env::args_os().peekable();
let mut connect_str = None;
while let Some(arg) = args_iter.next() {
// we default to one of the ignored args
match arg.to_str().unwrap_or("-json") {
"-json" | "-xml" | "-indent" | "--" |
"-scriptportonce" | "-nowrapperdoc" |
"-socket" | "-v" | "--version" => {}
"-scriptport" | "-httpd" => {
args_iter.next();
}
"-connect" => {
connect_str = args_iter.next()
.ok_or_else(|| anyhow!("Arg required for -connect"))
.and_then(|arg| arg.into_string().map_err(|_| anyhow!("Invalid connect string")))
.map(Some)?;
}
"-e" => {
'skip: while let Some(arg) = args_iter.peek() {
if arg.to_str().unwrap_or("").starts_with("-") {
break 'skip;
}
args_iter.next();
}
}
_ => subprocess_args.push(arg),
}
}
let _connect_str = connect_str.ok_or_else(||anyhow!("No connect string given"))?;
let subproc = tokio::process::Command::new("b3270")
.args(&subprocess_args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let (_server, _server_req) = connection::B3270::spawn(subproc);
// TODO: make connection before starting listeners
Ok(())
}
pub mod connection;

8
qt3270/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "qt3270"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

3
qt3270/src/main.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
eprintln!("Hello from d3270c");
}

View File

@@ -1,5 +0,0 @@
pub struct Tracker {
}