From 1015bcb4ad679789e98d9ab3760e3e19eb87e678 Mon Sep 17 00:00:00 2001 From: Condorra Date: Thu, 12 Sep 2024 20:42:41 +1000 Subject: [PATCH] Upgrade to latest (by rev) Lua / gc-arena --- Cargo.lock | 9 +- Cargo.toml | 4 +- rustfmt.toml | 1 + src/lua_engine.rs | 147 +++++++++++--------- src/lua_engine/frames.rs | 27 ++-- src/lua_engine/muds.rs | 173 ++++++++++++++++++----- src/main.rs | 1 + src/telnet.rs | 288 +++++++++++++++++++++++++++++++++++++++ src/term_split.rs | 57 +++----- 9 files changed, 551 insertions(+), 156 deletions(-) create mode 100644 rustfmt.toml create mode 100644 src/telnet.rs diff --git a/Cargo.lock b/Cargo.lock index 43ce28c..66a3a88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,8 +246,7 @@ dependencies = [ [[package]] name = "gc-arena" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd70cf88a32937834aae9614ff2569b5d9467fa0c42c5d7762fd94a8de88266" +source = "git+https://github.com/kyren/gc-arena.git?rev=5a7534b883b703f23cfb8c3cfdf033460aa77ea9#5a7534b883b703f23cfb8c3cfdf033460aa77ea9" dependencies = [ "allocator-api2", "gc-arena-derive", @@ -258,8 +257,7 @@ dependencies = [ [[package]] name = "gc-arena-derive" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c612a69f5557a11046b77a7408d2836fe77077f842171cd211c5ef504bd3cddd" +source = "git+https://github.com/kyren/gc-arena.git?rev=5a7534b883b703f23cfb8c3cfdf033460aa77ea9#5a7534b883b703f23cfb8c3cfdf033460aa77ea9" dependencies = [ "proc-macro2", "quote", @@ -813,8 +811,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "piccolo" version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003bf52de285e1ff1adcbc6572588db3849988ea660a2d55af3a2ffbc81f597f" +source = "git+https://github.com/kyren/piccolo.git?rev=fcbaabc924292170d6549c55440ecbd0522b275a#fcbaabc924292170d6549c55440ecbd0522b275a" dependencies = [ "ahash", "allocator-api2", diff --git a/Cargo.toml b/Cargo.toml index 6343494..1cfac65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" im = "15.1.0" itertools = "0.13.0" nom = "7.1.3" -piccolo = "0.3.3" +piccolo = { git = "https://github.com/kyren/piccolo.git", rev = "fcbaabc924292170d6549c55440ecbd0522b275a" } unicode-segmentation = "1.11.0" unicode-width = "0.1.13" wasm-bindgen = "0.2.92" @@ -21,4 +21,4 @@ console_error_panic_hook = "0.1.7" anyhow = "1.0.86" serde = "1.0.209" serde_json = "1.0.127" -gc-arena = "0.5.3" +gc-arena = { git = "https://github.com/kyren/gc-arena.git", rev = "5a7534b883b703f23cfb8c3cfdf033460aa77ea9" } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3a26366 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/src/lua_engine.rs b/src/lua_engine.rs index a8d5759..628ad94 100644 --- a/src/lua_engine.rs +++ b/src/lua_engine.rs @@ -1,8 +1,8 @@ use self::{frames::*, muds::*}; use anyhow::Error; use piccolo::{ - Closure, Executor, FromValue, Function, IntoValue, Lua, StashedExecutor, StaticError, Table, - Value, Variadic, + Callback, Closure, Context, Executor, ExternError, FromValue, Function, Lua, StashedExecutor, + Table, Value, Variadic, }; use yew::UseStateSetter; @@ -26,9 +26,9 @@ impl LuaState { Ok(LuaState { interp, exec }) } - fn try_set_current_frame(&mut self, frame: &TermFrame) -> Result<(), StaticError> { + fn try_set_current_frame(&mut self, frame: &TermFrame) -> Result<(), ExternError> { self.interp.try_enter(|ctx| { - let info_table = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"info")))?; + let info_table = Table::from_value(ctx, ctx.get_global("info")?)?; info_table.set( ctx, ctx.intern_static(b"current_frame"), @@ -65,9 +65,8 @@ impl LuaState { ) -> Result<(), String> { self.interp .try_enter(|ctx| { - let commands = - Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"commands")))?; - let command_value: Value = commands.get(ctx, ctx.intern(command.as_bytes())); + let commands = ctx.get_global::("commands")?; + let command_value: Value = commands.get(ctx, ctx.intern(command.as_bytes()))?; if command_value.is_nil() { Err(anyhow::Error::msg("Unknown command"))?; } @@ -95,70 +94,84 @@ pub fn install_lua_globals( global_memo: &GlobalMemoCell, global_layout: UseStateSetter, ) -> Result<(), String> { - global_memo - .lua_engine - .borrow_mut() - .interp - .try_enter(|ctx| { - let cmd_table = Table::new(&ctx); - macro_rules! register_command { - ($sym: ident) => { - cmd_table - .set( - ctx, - ctx.intern_static(stringify!($sym).as_bytes()), - $sym(ctx, &global_memo, &global_layout), - ) - .map_err(|_| Error::msg("Can't add command"))?; - }; - } + global_memo.lua_engine.borrow_mut().interp.try_enter(|ctx| { + let cmd_table = Table::new(&ctx); + macro_rules! register_command { + ($sym: ident) => { + cmd_table + .set( + ctx, + ctx.intern_static(stringify!($sym).as_bytes()), + $sym(ctx, &global_memo, &global_layout), + ) + .map_err(|_| Error::msg("Can't add command"))?; + }; + } - register_command!(echo); - register_command!(echo_frame); - register_command!(echo_frame_raw); - register_command!(connect_mud); - register_command!(delete_mud); - register_command!(close_mud); - register_command!(sendmud_raw); - register_command!(hsplit); - register_command!(panel_merge); - register_command!(vsplit); - ctx.set_global(ctx.intern_static(b"commands").into_value(ctx), cmd_table) - .map(|_| ()) - .map_err(|_| Error::msg("Can't set commands key"))?; + register_command!(echo); + register_command!(echo_frame); + register_command!(echo_frame_raw); + register_command!(connect_mud); + register_command!(delete_mud); + register_command!(close_mud); + register_command!(sendmud_raw); + register_command!(hsplit); + register_command!(panel_merge); + register_command!(vsplit); + ctx.set_global("commands", cmd_table); + let info_table = Table::new(&ctx); + ctx.set_global("info", info_table); + let muds_table = Table::new(&ctx); + ctx.set_global("muds", muds_table); - let info_table = Table::new(&ctx); - ctx.set_global(ctx.intern_static(b"info").into_value(ctx), info_table) - .map(|_| ()) - .map_err(|_| Error::msg("Can't set info key"))?; - let muds_table = Table::new(&ctx); - ctx.set_global(ctx.intern_static(b"muds").into_value(ctx), muds_table) - .map(|_| ()) - .map_err(|_| Error::msg("Can't set muds key"))?; + let handlers_table = Table::new(&ctx); + ctx.set_global("handlers", handlers_table); + macro_rules! register_handler { + ($sym: ident) => { + handlers_table + .set( + ctx, + ctx.intern_static(stringify!($sym).as_bytes()), + $sym(ctx, &global_memo), + ) + .map_err(|_| Error::msg("Can't add handler"))?; + }; + } + register_handler!(mudoutput); + register_handler!(mudoutput_line); + register_handler!(mudoutput_prompt); + register_handler!(mudoutput_will); + register_handler!(mudoutput_wont); + register_handler!(mudoutput_do); + register_handler!(mudoutput_dont); + register_handler!(mudoutput_subnegotiation); - let handlers_table = Table::new(&ctx); - ctx.set_global( - ctx.intern_static(b"handlers").into_value(ctx), - handlers_table, - ) - .map(|_| ()) - .map_err(|_| Error::msg("Can't set handlers key"))?; - macro_rules! register_handler { - ($sym: ident) => { - handlers_table - .set( - ctx, - ctx.intern_static(stringify!($sym).as_bytes()), - $sym(ctx, &global_memo), - ) - .map_err(|_| Error::msg("Can't add handler"))?; - }; - } - register_handler!(mudoutput); + macro_rules! register_nop_handler { + ($sym: ident) => { + handlers_table + .set( + ctx, + ctx.intern_static(stringify!($sym).as_bytes()), + lua_nop(ctx), + ) + .map_err(|_| Error::msg("Can't add handler"))?; + }; + } - Ok(()) - }) - .map_err(|e| e.to_string())?; + register_nop_handler!(mudoutput_break); + register_nop_handler!(mudoutput_sync); + register_nop_handler!(mudoutput_interrupt); + register_nop_handler!(mudoutput_abort_output); + register_nop_handler!(mudoutput_areyouthere); + + Ok(()) + }); Ok(()) } + +pub fn lua_nop(ctx: Context<'_>) -> Callback<'_> { + Callback::from_fn(&ctx, |_ctx, _ex, _stack| { + Ok(piccolo::CallbackReturn::Return) + }) +} diff --git a/src/lua_engine/frames.rs b/src/lua_engine/frames.rs index 0f6b335..c6f0558 100644 --- a/src/lua_engine/frames.rs +++ b/src/lua_engine/frames.rs @@ -16,7 +16,12 @@ pub fn echo_frame_raw<'gc, 'a>( ) -> Callback<'gc> { let global_memo = global_memo.clone(); Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - let frame: TermFrame = try_unwrap_frame(ctx, &stack.pop_front())?; + let frame: TermFrame = try_unwrap_frame( + ctx, + &stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("Missing frame"))?, + )?; let message: piccolo::String = stack.from_front(ctx)?; let message_str = str::from_utf8(message.as_bytes()) .map_err(|_| "Expected message to echo to be UTF-8.".into_value(ctx))?; @@ -31,14 +36,15 @@ pub fn echo_frame<'gc>( _global_layout: &UseStateSetter, ) -> Callback<'gc> { Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - let commands: Table<'gc> = - Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"commands")))?; - let function = Function::from_value(ctx, commands.get(ctx, "echo_frame_raw"))?; - let frame_no: Value = stack.pop_front(); + let commands: Table<'gc> = ctx.get_global("commands")?; + let function: Function = commands.get(ctx, "echo_frame_raw")?; + let frame_no: Value = stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("Missing frame number"))?; let all_parts: Vec = stack .consume::>>(ctx)? .into_iter() - .map(|v| format!("{}", v)) + .map(|v| format!("{:?}", v)) .collect(); stack.push_front(frame_no); let message = ctx.intern((all_parts.join(" ") + "\r\n").as_bytes()); @@ -56,11 +62,10 @@ pub fn echo<'gc>( _global_layout: &UseStateSetter, ) -> Callback<'gc> { Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - let commands: Table<'gc> = - Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"commands")))?; - let function = Function::from_value(ctx, commands.get(ctx, "echo_frame"))?; - let info: Table<'gc> = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"info")))?; - let cur_frame = info.get(ctx, ctx.intern_static(b"current_frame")); + let commands: Table<'gc> = ctx.get_global("commands")?; + let function: Function = commands.get(ctx, "echo_frame")?; + let info: Table<'gc> = ctx.get_global("info")?; + let cur_frame: Value = info.get(ctx, ctx.intern_static(b"current_frame"))?; stack.push_front(cur_frame); Ok(piccolo::CallbackReturn::Call { function, diff --git a/src/lua_engine/muds.rs b/src/lua_engine/muds.rs index be9af5a..a6058c8 100644 --- a/src/lua_engine/muds.rs +++ b/src/lua_engine/muds.rs @@ -1,6 +1,9 @@ use anyhow::Error; use gc_arena::{Gc, Rootable}; -use piccolo::{self, Callback, Context, FromValue, Function, Table, UserData, Value}; +use piccolo::{ + self, Callback, CallbackReturn, Context, FromValue, Function, IntoValue, StashedValue, Table, + UserData, Value, +}; use wasm_bindgen::JsValue; use web_sys::console; use yew::UseStateSetter; @@ -8,6 +11,7 @@ use yew::UseStateSetter; use crate::{ command_handler::debrace, id_intern::{intern_id, unintern_id}, + telnet::{parse_telnet_buf, TelnetOutput}, websocket::{connect_websocket, send_message_to_mud, WebSocketId}, GlobalLayoutCell, GlobalMemoCell, }; @@ -19,7 +23,7 @@ fn try_unwrap_socketid<'gc>( value: &Value<'gc>, ) -> Result> { let value = if let Ok(sockname) = String::from_value(ctx, *value) { - let ret = Table::from_value(ctx, ctx.get_global("muds"))?.get(ctx, sockname); + let ret: Value<'gc> = ctx.get_global::>("muds")?.get(ctx, sockname)?; if ret.is_nil() { Err(Error::msg( "Could not find a MUD connection with that name.", @@ -30,7 +34,7 @@ fn try_unwrap_socketid<'gc>( *value }; Ok(UserData::from_value(ctx, value)? - .downcast:: Gc<'gcb, WebSocketId>]>()? + .downcast::]>()? .as_ref() .clone()) } @@ -43,9 +47,8 @@ pub fn handle_websocket_output( engine .interp .try_enter(|ctx| { - let handlers = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"handlers")))?; - let input_fn = - Function::from_value(ctx, handlers.get(ctx, ctx.intern_static(b"mudoutput")))?; + let handlers: Table = ctx.get_global("handlers")?; + let input_fn: Function = handlers.get(ctx, ctx.intern_static(b"mudoutput"))?; ctx.fetch(&engine.exec).restart( ctx, input_fn, @@ -77,9 +80,8 @@ pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Re engine .interp .try_enter(|ctx| { - let handlers = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"handlers")))?; - let input_fn = - Function::from_value(ctx, handlers.get(ctx, ctx.intern_static(b"mudclose")))?; + let handlers: Table = ctx.get_global("handlers")?; + let input_fn: Function = handlers.get(ctx, ctx.intern_static(b"mudclose"))?; ctx.fetch(&engine.exec).restart( ctx, input_fn, @@ -111,7 +113,12 @@ pub(super) fn sendmud_raw<'gc>( ) -> Callback<'gc> { let global_memo = global_memo.clone(); Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - let mud: WebSocketId = try_unwrap_socketid(ctx, &stack.pop_front())?; + let mud: WebSocketId = try_unwrap_socketid( + ctx, + &stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("Missing MUD argument"))?, + )?; let msg: piccolo::String = stack.from_front(ctx)?; send_message_to_mud(&mud, msg.as_bytes(), &global_memo)?; Ok(piccolo::CallbackReturn::Return) @@ -136,8 +143,8 @@ pub(super) fn connect_mud<'gc>( }; let name: Value<'gc> = ctx.intern(debrace(&name).as_bytes()).into(); - let muds = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"muds")))?; - if !muds.get_value(name).is_nil() { + let muds: Table = ctx.get_global("muds")?; + if !muds.get_value(ctx, name).is_nil() { Err(Error::msg( "Attempt to create MUD connection using name that's already taken", ))? @@ -170,7 +177,9 @@ pub(super) fn close_mud<'gc>( ) -> Callback<'gc> { let global_memo = global_memo.clone(); Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - let mud_value: Value<'gc> = stack.pop_front(); + let mud_value: Value<'gc> = stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("Missing MUD value argument"))?; let socket_id = try_unwrap_socketid(ctx, &mud_value)?; match global_memo.ws_registry.borrow_mut().get_mut(&socket_id) { None => Err(Error::msg("That MUD connection doesn't exist."))?, @@ -197,8 +206,8 @@ pub(super) fn delete_mud<'gc>( let name: String = stack.from_front(ctx)?; let name: Value<'gc> = ctx.intern(debrace(&name).as_bytes()).into(); - let muds = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"muds")))?; - let mud_value = muds.get_value(name); + let muds: Table = ctx.get_global("muds")?; + let mud_value = muds.get_value(ctx, name); if mud_value.is_nil() { Err(Error::msg( "Attempt to delete MUD connection that wasn't found", @@ -208,7 +217,7 @@ pub(super) fn delete_mud<'gc>( let socket_id = try_unwrap_socketid(ctx, &mud_value)?; // Delete the MUD data if possible. - let _ = muds.set_value(&ctx, mud_value, Value::Nil); + let _ = muds.set_raw(&ctx, mud_value, Value::Nil)?; for (k, v) in muds.iter() { match UserData::from_value(ctx, v) { Err(_) => continue, @@ -218,7 +227,7 @@ pub(super) fn delete_mud<'gc>( _ => {} }, } - let _ = muds.set_value(&ctx, k, Value::Nil); + let _ = muds.set_raw(&ctx, k, Value::Nil); } unintern_id(ctx, &socket_id); match global_memo.ws_registry.borrow_mut().remove(&socket_id) { @@ -231,28 +240,124 @@ pub(super) fn delete_mud<'gc>( }) } -pub(super) fn mudoutput<'gc>(ctx: Context<'gc>, global_memo: &GlobalMemoCell) -> Callback<'gc> { - let global_memo = global_memo.clone(); +pub(super) fn mudoutput<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) -> Callback<'gc> { Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - // Temporary hack for testing... alias to echo - let echo = Function::from_value( + let mud: Value<'gc> = stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("Missing argument to mudoutput"))?; + let output: &'gc [u8] = stack.from_front::>(ctx)?.as_bytes(); + + let conntab: Table<'gc> = Table::from_value( ctx, - Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"commands")))? - .get(ctx, ctx.intern_static(b"echo")), + Table::from_value(ctx, ctx.get_global("muds")?)?.get_value(ctx, mud), )?; - let mud: Value<'gc> = stack.pop_front(); - let output: piccolo::String<'gc> = stack.from_front(ctx)?; + let buf: &'gc [u8] = + piccolo::String::from_value(ctx, conntab.get(ctx, ctx.intern_static(b"buffer"))?)? + .as_bytes(); + let mut cur_buf: Vec = [buf, output].concat(); - let conntab = - Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"muds")))?.get_value(mud); - if conntab.is_nil() { - Err(Error::msg("Received input from MUD not in muds table."))? + let handlers = Table::from_value(ctx, ctx.get_global("handlers")?)?; + + let mut fns: Vec<(&'static [u8], Vec)> = vec![]; + loop { + match parse_telnet_buf(&cur_buf) { + (new_buf, None) => { + conntab.set(ctx, ctx.intern_static(b"buffer"), ctx.intern(&new_buf))?; + + let seq = piccolo::async_sequence(&ctx, |locals, seq| async { + Ok(piccolo::SequenceReturn::Return) + }); + + return Ok(piccolo::CallbackReturn::Sequence(seq)); + } + (new_buf, Some(cmd)) => { + cur_buf = new_buf; + match cmd { + TelnetOutput::Line(l) => fns.push(( + b"mudoutput_line", + vec![ctx.stash(mud), ctx.stash(ctx.intern(&l).into_value(ctx))], + )), + TelnetOutput::Prompt(p) => fns.push(( + b"mudoutput_prompt", + vec![ctx.stash(mud), ctx.stash(ctx.intern(&p).into_value(ctx))], + )), + TelnetOutput::Nop => {} + TelnetOutput::Break => fns.push((b"mudoutput_break", vec![ctx.stash(mud)])), + TelnetOutput::Sync => fns.push((b"mudoutput_sync", vec![ctx.stash(mud)])), + TelnetOutput::Interrupt => { + fns.push((b"mudoutput_interrupt", vec![ctx.stash(mud)])) + } + + TelnetOutput::AbortOutput => { + fns.push((b"mudoutput_abort_output", vec![ctx.stash(mud)])) + } + TelnetOutput::AreYouThere => { + fns.push((b"mudoutput_areyouthere", vec![ctx.stash(mud)])) + } + TelnetOutput::Will(v) => fns.push(( + b"mudoutput_will", + vec![ctx.stash(mud), ctx.stash(v.into_value(ctx))], + )), + TelnetOutput::Wont(v) => fns.push(( + b"mudoutput_wont", + vec![ctx.stash(mud), ctx.stash(v.into_value(ctx))], + )), + TelnetOutput::Do(v) => fns.push(( + b"mudoutput_do", + vec![ctx.stash(mud), ctx.stash(v.into_value(ctx))], + )), + TelnetOutput::Dont(v) => fns.push(( + b"mudoutput_dont", + vec![ctx.stash(mud), ctx.stash(v.into_value(ctx))], + )), + TelnetOutput::Subnegotiation(t) => fns.push(( + b"mudoutput_subnegotiation", + vec![ctx.stash(mud), ctx.stash(t.into_value(ctx))], + )), + } + } + } } - - Ok(piccolo::CallbackReturn::Call { - function: echo, - then: None, - }) }) } + +pub(super) fn mudoutput_line<'gc>( + ctx: Context<'gc>, + _global_memo: &GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} +pub(super) fn mudoutput_prompt<'gc>( + ctx: Context<'gc>, + _global_memo: &GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} +pub(super) fn mudoutput_will<'gc>( + ctx: Context<'gc>, + _global_memo: &GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} +pub(super) fn mudoutput_wont<'gc>( + ctx: Context<'gc>, + _global_memo: &GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} +pub(super) fn mudoutput_do<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} +pub(super) fn mudoutput_dont<'gc>( + ctx: Context<'gc>, + _global_memo: &GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} +pub(super) fn mudoutput_subnegotiation<'gc>( + ctx: Context<'gc>, + _global_memo: &GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) +} diff --git a/src/main.rs b/src/main.rs index 1ff7e17..22a013b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ pub mod lineengine; pub mod lua_engine; pub mod parsing; pub mod split_panel; +pub mod telnet; pub mod term_split; pub mod term_view; pub mod websocket; diff --git a/src/telnet.rs b/src/telnet.rs new file mode 100644 index 0000000..18f0518 --- /dev/null +++ b/src/telnet.rs @@ -0,0 +1,288 @@ +use wasm_bindgen::JsValue; +use web_sys::console; + +#[derive(Eq, PartialEq, Debug)] +pub enum TelnetOutput { + Line(Vec), + Prompt(Vec), // Like a line but without a newline. + Nop, + Break, + Sync, + Interrupt, + AbortOutput, + AreYouThere, + Will(u8), + Wont(u8), + Do(u8), + Dont(u8), + Subnegotiation(Vec), +} + +const IAC: u8 = 255; +const NOP: u8 = 241; +const DATA_MARK: u8 = 242; +const BREAK: u8 = 243; +const INTERRUPT: u8 = 244; +const ABORT: u8 = 245; +const AYT: u8 = 246; +const ERASECHAR: u8 = 247; +const ERASELINE: u8 = 248; +const GOAHEAD: u8 = 249; +const EOR: u8 = 239; +const STARTSUB: u8 = 250; +const ENDSUB: u8 = 240; +const WILL: u8 = 251; +const WONT: u8 = 252; +const DO: u8 = 253; +const DONT: u8 = 254; + +pub fn parse_telnet_buf(input: &[u8]) -> (Vec, Option) { + let mut ptr: &[u8] = input; + let mut textbuf: Vec = vec![]; + loop { + match ptr.first() { + None => return (textbuf.to_owned(), None), + Some(b'\n') => { + textbuf.push(b'\n'); + return (ptr[1..].to_owned(), Some(TelnetOutput::Line(textbuf))); + } + Some(&IAC) => { + ptr = &ptr[1..]; + match ptr.first() { + None => { + textbuf.push(IAC); + return (textbuf, None); + } + Some(&NOP) => {} + Some(&DATA_MARK) => {} + Some(&BREAK) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::Break)); + } + Some(&INTERRUPT) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::Interrupt)); + } + Some(&ABORT) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::AbortOutput)); + } + Some(&AYT) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::AreYouThere)); + } + Some(&ERASECHAR) => { + if !textbuf.is_empty() { + textbuf = textbuf[0..textbuf.len() - 1].to_owned(); + } + } + Some(&ERASELINE) => { + textbuf = vec![]; + } + Some(&GOAHEAD) => { + if textbuf.len() > 1 { + return (ptr[1..].to_owned(), Some(TelnetOutput::Prompt(textbuf))); + } + } + Some(&EOR) => { + if textbuf.len() > 1 { + return (ptr[1..].to_owned(), Some(TelnetOutput::Prompt(textbuf))); + } + } + Some(&WILL) => { + ptr = &ptr[1..]; + match ptr.first() { + None => { + textbuf.push(IAC); + textbuf.push(WILL); + return (textbuf, None); + } + Some(c) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::Will(*c))); + } + } + } + Some(&WONT) => { + ptr = &ptr[1..]; + match ptr.first() { + None => { + textbuf.push(IAC); + textbuf.push(WONT); + return (textbuf, None); + } + Some(c) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::Wont(*c))); + } + } + } + Some(&DO) => { + ptr = &ptr[1..]; + match ptr.first() { + None => { + textbuf.push(IAC); + textbuf.push(DO); + return (textbuf, None); + } + Some(c) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::Do(*c))); + } + } + } + Some(&DONT) => { + ptr = &ptr[1..]; + match ptr.first() { + None => { + textbuf.push(IAC); + textbuf.push(DONT); + return (textbuf, None); + } + Some(c) => { + textbuf.extend_from_slice(&ptr[1..]); + return (textbuf, Some(TelnetOutput::Dont(*c))); + } + } + } + Some(&STARTSUB) => { + match (1..ptr.len() - 1).find(|i| ptr[*i] == IAC && ptr[*i + 1] == ENDSUB) { + None => { + // Including the STARTSUB... + textbuf.push(IAC); + textbuf.extend_from_slice(ptr); + return (textbuf, None); + } + Some(end_idx) => { + textbuf.extend_from_slice(&ptr[end_idx + 2..]); + return ( + textbuf, + Some(TelnetOutput::Subnegotiation(ptr[1..end_idx].to_owned())), + ); + } + } + } + Some(c) => { + // Completely unexpected command. Warn and ignore. + console::log_1(&JsValue::from_str(&format!( + "Received unknown IAC command {}, assuming single byte and ignoring.", + c + ))); + } + } + } + Some(c) => textbuf.push(*c), + } + ptr = &ptr[1..]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_telnet_works() { + assert_eq!(parse_telnet_buf(b"Hello"), (b"Hello".to_vec(), None)); + assert_eq!( + parse_telnet_buf(b"Hello\r\n"), + ( + b"".to_vec(), + Some(TelnetOutput::Line(b"Hello\r\n".to_vec())) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello\r\nWorld"), + ( + b"World".to_vec(), + Some(TelnetOutput::Line(b"Hello\r\n".to_vec())) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello> \xff\xf9World"), // Go Ahead after prompt. + ( + b"World".to_vec(), + Some(TelnetOutput::Prompt(b"Hello> ".to_vec())) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello> \xff\xefWorld"), // End Of Record after prompt. + ( + b"World".to_vec(), + Some(TelnetOutput::Prompt(b"Hello> ".to_vec())) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xf1World\r\nWoo!"), // NOP in the middle. + ( + b"Woo!".to_vec(), + Some(TelnetOutput::Line(b"Hello World\r\n".to_vec())) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xf2World\r\nWoo!"), // DATA MARK in the middle. + ( + b"Woo!".to_vec(), + Some(TelnetOutput::Line(b"Hello World\r\n".to_vec())) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xf3World\r\nWoo!"), // BREAK in the middle. + (b"Hello World\r\nWoo!".to_vec(), Some(TelnetOutput::Break)) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xf4World\r\nWoo!"), // INTERRUPT in the middle. + ( + b"Hello World\r\nWoo!".to_vec(), + Some(TelnetOutput::Interrupt) + ) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff"), // Incomplete IAC command. + (b"Hello \xff".to_vec(), None) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xf5World!"), // Abort + (b"Hello World!".to_vec(), Some(TelnetOutput::AbortOutput)) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xf6World!"), // Are You There + (b"Hello World!".to_vec(), Some(TelnetOutput::AreYouThere)) + ); + assert_eq!( + parse_telnet_buf(b"Hello B\xff\xf7World!"), // Erase Char + (b"Hello World!".to_vec(), None) + ); + assert_eq!( + parse_telnet_buf(b"Whoops \xff\xf8Hello World!"), // Erase Line + (b"Hello World!".to_vec(), None) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xfb\x01World!"), // Will + (b"Hello World!".to_vec(), Some(TelnetOutput::Will(1))) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xfc\x02World!"), // Wont + (b"Hello World!".to_vec(), Some(TelnetOutput::Wont(2))) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xfd\x03World!"), // Do + (b"Hello World!".to_vec(), Some(TelnetOutput::Do(3))) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xfe\x04World!"), // Dont + (b"Hello World!".to_vec(), Some(TelnetOutput::Dont(4))) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xfablah"), // Partial subnegotiation + (b"Hello \xff\xfablah".to_vec(), None) + ); + assert_eq!( + parse_telnet_buf(b"Hello \xff\xfablah\xff\xf0World!"), // Partial subnegotiation + ( + b"Hello World!".to_vec(), + Some(TelnetOutput::Subnegotiation(b"blah".to_vec())) + ) + ); + } +} diff --git a/src/term_split.rs b/src/term_split.rs index b03ff3b..59abf58 100644 --- a/src/term_split.rs +++ b/src/term_split.rs @@ -216,36 +216,30 @@ mod tests { #[test] fn modify_at_pathstr_works() { use TermSplit::*; - let mut t = Term { + let t = Term { frame: TermFrame(1), }; assert_eq!( - t.modify_at_pathstr("", |v| { - *v = Term { + t.modify_at_pathstr("", |_v| { + Ok(Term { frame: TermFrame(2), - }; - Ok(()) + }) }), - Ok(()) - ); - assert_eq!( - t, - Term { - frame: TermFrame(2) - } + Ok(Term { + frame: TermFrame(2), + }) ); assert_eq!( - t.modify_at_pathstr("tlr", |v| { - *v = Term { + t.modify_at_pathstr("tlr", |_v| { + Ok(Term { frame: TermFrame(2), - }; - Ok(()) + }) }), Err("In split path, found trailing junk tlr after addressing terminal".to_owned()) ); - let mut t = Vertical { + let t = Vertical { top: Horizontal { left: Horizontal { left: Term { @@ -277,26 +271,17 @@ mod tests { .into(), }; assert_eq!( - t.modify_at_pathstr("tlr", |v| { - *v = Term { + t.modify_at_pathstr("tlr", |_v| { + Ok(Term { frame: TermFrame(2), - }; - Ok(()) - }), - Ok(()) - ); - assert_eq!( - t.modify_at_pathstr("bb", |v| { - *v = Term { + }) + }) + .and_then(|t| t.modify_at_pathstr("bb", |_v| { + Ok(Term { frame: TermFrame(3), - }; - Ok(()) - }), - Ok(()) - ); - assert_eq!( - t, - Vertical { + }) + })), + Ok(Vertical { top: Horizontal { left: Horizontal { left: Term { @@ -326,7 +311,7 @@ mod tests { .into(), } .into(), - } + }) ); } }