Initial commit; wrote lots o' serializers

This commit is contained in:
2023-05-07 00:43:16 +02:00
commit 6b84b61878
12 changed files with 2923 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

2127
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "d3270"
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"
anyhow = "1.0.71"
bitflags = "2.2.1"

59
flake.lock generated Normal file
View File

@@ -0,0 +1,59 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1683353485,
"narHash": "sha256-Skp5El3egmoXPiINWjnoW0ktVfB7PR/xc4F4bhD+BJY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "caf436a52b25164b71e0d48b671127ac2e2a5b75",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

21
flake.nix Normal file
View File

@@ -0,0 +1,21 @@
{
description = "A very basic flake";
inputs.nixpkgs.url = "nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
clang
rustup
];
nativeBuildInputs = with pkgs; [
];
};
});
}

116
src/b3270.rs Normal file
View File

@@ -0,0 +1,116 @@
use serde::{Deserialize, Serialize};
use indication::{CodePage, ConnectAttempt, Connection, Erase, FileTransfer, Hello, Model, Oia, Passthru, Popup, Proxy, RunResult, Screen, ScreenMode, Scroll, Setting, Stats, TerminalName, Thumb, Tls, TlsHello, TraceFile, UiError};
use operation::{Fail, Register, Run, Succeed};
pub mod operation;
pub mod indication;
pub mod types;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub enum Indication {
Bell{}, // TODO: make sure this emits/parses {"bell": {}}
/// Indicates that the host connection has changed state.
Connection(Connection),
/// A new host connection is being attempted
ConnectAttempt(ConnectAttempt),
/// Indicates the screen size
Erase(Erase),
/// Display switch between LTR and RTL
Flipped {
/// True if display is now in RTL mode
value: bool,
},
/// An xterm escape sequence requested a new font
Font {
text: String,
},
/// The formatting state of the screen has changed.
/// A formatted string has at least one field displayed.
Formatted {
state: bool,
},
/// File transfer state change
#[serde(rename="ft")]
FileTransfer(FileTransfer),
/// An XTerm escape sequence requested a new icon name
Icon{
text: String,
},
/// The first message sent
Initialize(Vec<InitializeIndication>),
/// Change in the state of the Operator Information Area
Oia(Oia),
/// A passthru action has been invoked.
/// Clients must respond with a succeed or fail operation
Passthru(Passthru),
/// Display an asynchronous message
Popup(Popup),
/// Result of a run operation
RunResult(RunResult),
/// Change to screen contents
Screen(Screen),
/// Screen dimensions/characteristics changed
ScreenMode(ScreenMode),
/// Screen was scrolled up by one row
Scroll(Scroll),
/// Setting changed
Setting(Setting),
/// I/O statistics
Stats(Stats),
/// Reports the terminal name sent to the host during TELNET negotiation
TerminalName(TerminalName),
/// Change in the scrollbar thumb
Thumb(Thumb),
/// Indicates the name of the trace file
TraceFile(TraceFile),
/// TLS state changed
Tls(Tls),
/// Error in b3270's input
UiError(UiError),
/// Xterm escape sequence requested a change to the window title
WindowTitle{
text: String,
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub enum InitializeIndication {
CodePages(Vec<CodePage>),
/// Indicates that the host connection has changed state.
Connection(Connection),
/// Indicates the screen size
Erase(Erase),
/// The first element in the initialize array
Hello(Hello),
/// Indicates which 3270 models are supported
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},
/// Screen dimensions/characteristics changed
ScreenMode(ScreenMode),
/// Setting changed
Setting(Setting),
/// Indicates build-time TLS config
TlsHello(TlsHello),
/// TLS state changed
Tls(Tls),
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub enum Operation {
/// Run an action
Run(Run),
/// Register a pass-thru action
Register(Register),
/// Tell b3270 that a passthru action failed
Fail(Fail),
/// Tell b3270 that a passthru action succeeded
Succeed(Succeed),
}

414
src/b3270/indication.rs Normal file
View File

@@ -0,0 +1,414 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename_all="kebab-case")]
pub enum ActionCause {
Command,
Default,
FileTransfer,
Httpd,
Idle,
Keymap,
Macro,
None,
Password,
Paste,
Peek,
ScreenRedraw,
Script,
Typeahead,
Ui,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct CodePage{
/// The canonical name of the code page
pub name: String,
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub aliases: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Connection{
/// New connection state
pub state: ConnectionState,
/// Host name, if connected
#[serde(default, skip_serializing_if="Option::is_none")]
pub host: Option<String>,
/// Source of the connection
#[serde(default, skip_serializing_if="Option::is_none")]
pub cause: Option<ActionCause>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ComposeType {
Std,
Ge,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ConnectionState {
NotConnected,
Reconnecting,
Resolving,
TcpPending,
TlsPending,
TelnetPending,
ConnectedNvt,
ConnectedNvtCharmode,
Connected3270,
ConnectedUnbound,
ConnectedENvt,
ConnectedESscp,
#[serde(rename="connected-e-tn3270e")]
ConnectedETn3270e,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct Erase {
#[serde(default, skip_serializing_if="Option::is_none")]
pub logical_rows: Option<u8>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub logical_cols: Option<u8>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub fg: Option<Color>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub bg: Option<Color>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Hello {
pub version: String,
pub build: String,
pub copyright: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Model {
pub model: u8,
pub rows: u8,
pub columns: u8,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
// This could be more typesafe, probably ¯\_(ツ)_/¯
pub struct Oia {
#[serde(flatten)]
pub field: OiaField,
#[serde(default, skip_serializing_if="Option::is_none")]
pub lu: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case", tag="field")]
#[derive(Debug, PartialEq, Clone)]
pub enum OiaField {
/// Composite character in progress
Compose{
value: bool,
#[serde(default, skip_serializing_if="Option::is_none")]
char: Option<String>,
#[serde(default, skip_serializing_if="Option::is_none")]
type_: Option<ComposeType>,
},
/// Insert mode
Insert{value: bool},
/// Keyboard is locked
Lock{
#[serde(default, skip_serializing_if="Option::is_none")]
value: Option<String>
},
Lu{
/// Host session logical unit name
value: String,
/// Printer session LU name
#[serde(default, skip_serializing_if="Option::is_none")]
lu: Option<String>,
},
/// Communication pending
NotUndera {
value: bool,
},
PrinterSession {
value: bool,
/// Printer session LU name
// TODO: determine if this is sent with this message or with Lu
#[serde(default, skip_serializing_if="Option::is_none")]
lu: Option<String>,
},
/// Reverse input mode
ReverseInput {
value: bool,
},
/// Screen trace count
Screentrace {
value: String,
},
/// Host command timer (minutes:seconds)
Script {
value: String,
},
Typeahead {
value: bool,
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Proxy {
pub name: String,
pub username: String,
pub port: u16,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Setting {
pub name: String,
/// I'd love something other than depending on serde_json for this.
pub value: Option<serde_json::Value>,
pub cause: ActionCause,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
pub struct ScreenMode {
pub model: u8,
pub rows: u8,
pub cols: u8,
pub color: bool,
pub oversize: bool,
pub extended: bool,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct TlsHello {
pub supported: bool,
pub provider: String, // docs claim this is always set, but I'm not sure.
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub options: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct Tls {
pub secure: bool,
#[serde(default, skip_serializing_if="Option::is_none")]
pub verified: Option<bool>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub session: Option<String>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub host_cert: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct ConnectAttempt {
pub host_ip: String,
pub port: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
// TODO: change this to an enum
pub struct Cursor {
pub enabled: bool,
#[serde(default, skip_serializing_if="Option::is_none")]
pub row: Option<u8>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub column: Option<u8>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct FileTransfer {
#[serde(flatten)]
pub state: FileTransferState,
pub cause: ActionCause,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="lowercase", tag="state")]
pub enum FileTransferState {
Awaiting,
Running{
/// Number of bytes transferred
bytes: usize
},
Aborting,
Complete{
/// Completion message
text: String,
/// Transfer succeeded
success: bool,
},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename="kebab-case")]
pub struct Passthru {
pub p_tag: String,
#[serde(default, skip_serializing_if="Option::is_none")]
pub parent_r_tag: Option<String>,
pub action: String,
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub args: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Popup {
#[serde(rename="type")]
pub type_: PopupType,
pub text: String,
#[serde(default, skip_serializing_if="Option::is_none")]
pub error: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename="kebab-case")]
pub enum PopupType {
/// Error message from a connection attempt
ConnectError,
/// Error message
Error,
/// Informational message
Info,
/// Stray action output (should not happen)
Result,
/// Output from the pr3287 process
Printer,
/// Output from other child process
Child,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename="kebab-case")]
pub struct Row {
pub row: u8,
pub changes: Vec<Change>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename="kebab-case")]
pub enum CountOrText {
Count(usize),
Text(String),
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Change {
pub column: u8,
#[serde(flatten)]
pub change: CountOrText,
#[serde(default, skip_serializing_if="Option::is_none")]
pub fg: Option<Color>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub bg: Option<Color>,
#[serde(default, skip_serializing_if="Option::is_none")]
/// Graphic rendition
// TODO: parse comma-separated list of GR strings from https://x3270.miraheze.org/wiki/B3270/Graphic_rendition
pub gr: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Screen {
#[serde(default, skip_serializing_if="Option::is_none")]
pub cursor: Option<Cursor>,
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub rows: Vec<Row>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename="kebab-case")]
pub struct RunResult {
#[serde(default, skip_serializing_if="Option::is_none")]
pub r_tag: Option<String>,
pub success: bool,
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub text: Vec<String>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub abort: Option<bool>,
/// Execution time in seconds
pub time: f32,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename="kebab-case")]
pub struct Scroll {
#[serde(default, skip_serializing_if="Option::is_none")]
pub fg: Option<Color>,
#[serde(default, skip_serializing_if="Option::is_none")]
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 {
pub bytes_received: usize,
pub bytes_sent: usize,
pub records_received: usize,
pub records_sent: usize,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct TerminalName {
pub text: String,
#[serde(rename="override")]
pub override_: bool,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
pub struct Thumb {
/// Fraction of scrollbar to top of thumb
pub top: f32,
/// Fraction of scrollbar for thumb display
pub shown: f32,
/// Number of rows saved
pub saved: usize,
/// Size of a screen in rows
pub screen: usize,
/// Number of rows scrolled back
pub back: usize,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct TraceFile {
#[serde(default, skip_serializing_if="Option::is_none")]
pub name: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct UiError {
pub fatal: bool,
pub text: String,
#[serde(default, skip_serializing_if="Option::is_none")]
pub operation: Option<String>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub member: Option<String>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub line: Option<usize>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub column: Option<usize>,
}

46
src/b3270/operation.rs Normal file
View File

@@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
// Operations
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct Run {
#[serde(default, skip_serializing_if="Option::is_none")]
pub r_tag: Option<String>,
#[serde(rename="type", default, skip_serializing_if="Option::is_none")]
pub type_: Option<String>,
pub actions: Vec<Action>,
}
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct Action {
pub action: String,
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub args: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct Register {
pub name: String,
#[serde(default, skip_serializing_if="Option::is_none")]
pub help_text: Option<String>,
#[serde(default, skip_serializing_if="Option::is_none")]
pub help_params: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
/// Completes a passthru action unsuccessfully
#[serde(rename_all="kebab-case")]
pub struct Fail {
pub p_tag: String,
pub text: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub struct Succeed {
pub p_tag: String,
#[serde(default, skip_serializing_if="Vec::is_empty")]
pub text: Vec<String>,
}

116
src/b3270/types.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::fmt::{Display, Formatter, Write};
use std::str::FromStr;
use bitflags::bitflags;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::{Error, Visitor};
bitflags! {
#[derive(Clone,Copy,Debug,PartialEq, Eq, Hash)]
pub struct GraphicRendition: u16 {
const UNDERLINE = 0x001;
const BLINK = 0x002;
const HIGHLIGHT = 0x004;
const SELECTABLE = 0x008;
const REVERSE = 0x010;
const WIDE = 0x020;
const ORDER = 0x040;
const PRIVATE_USE = 0x080;
const NO_COPY = 0x100;
const WRAP = 0x200;
}
}
static FLAG_NAMES: &'static [(GraphicRendition, &'static str)] = &[
(GraphicRendition::UNDERLINE, "underline"),
(GraphicRendition::BLINK, "blink"),
(GraphicRendition::HIGHLIGHT, "highlight"),
(GraphicRendition::SELECTABLE, "selectable"),
(GraphicRendition::REVERSE, "reverse"),
(GraphicRendition::WIDE, "wide"),
(GraphicRendition::ORDER, "order"),
(GraphicRendition::PRIVATE_USE, "private-use"),
(GraphicRendition::NO_COPY, "no-copy"),
(GraphicRendition::WRAP, "wrap"),
];
impl Display for GraphicRendition {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let flag_names = FLAG_NAMES.iter()
.filter_map(|(val, name)| self.contains(*val).then_some(*name));
for (n, name) in flag_names.enumerate() {
if n != 0 {
f.write_char(',')?;
}
f.write_str(name)?;
}
Ok(())
}
}
impl Serialize for GraphicRendition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_string())
} else {
serializer.serialize_u16(self.bits())
}
}
}
struct GrVisitor;
impl Visitor<'_> for GrVisitor {
type Value = GraphicRendition;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "graphic rendition string or binary value")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> where E: Error {
self.visit_u64((v & 0xFFFF) as u64)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> where E: Error {
Ok(GraphicRendition::from_bits_truncate((v & 0xFFFF) as u16))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: Error {
GraphicRendition::from_str(v).map_err(E::custom)
}
}
impl FromStr for GraphicRendition {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.split(",")
.map(|attr| {
FLAG_NAMES.iter()
.find(|(_, name)| *name == attr)
.map(|x| x.0)
.ok_or_else(|| format!("Invalid attr name {attr}"))
})
.collect()
}
}
impl<'de> Deserialize<'de> for GraphicRendition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
if deserializer.is_human_readable() {
deserializer.deserialize_str(GrVisitor)
} else {
deserializer.deserialize_u16(GrVisitor)
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::GraphicRendition;
#[test]
fn from_str_1() {
assert_eq!(GraphicRendition::from_str("underline,blink"), Ok(GraphicRendition::BLINK | GraphicRendition::UNDERLINE))
}
}

2
src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod b3270;
pub mod tracker;

0
src/proto.rs Normal file
View File

5
src/tracker.rs Normal file
View File

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