diff --git a/src/lua_engine/muds.rs b/src/lua_engine/muds.rs index ef3a374..241483b 100644 --- a/src/lua_engine/muds.rs +++ b/src/lua_engine/muds.rs @@ -19,7 +19,13 @@ use crate::{ FrameId, GlobalLayoutCell, GlobalMemoCell, }; +use self::telopt::{ + handle_incoming_do, handle_incoming_dont, handle_incoming_will, handle_incoming_wont, + MudWithMemo, Telopt, +}; + use super::{call_checking_metatable, list_match_tab, try_unwrap_frame, LuaState}; +pub mod telopt; fn try_unwrap_socketid<'gc>( ctx: Context<'gc>, @@ -459,24 +465,72 @@ pub(super) fn mudoutput_prompt<'gc>( } pub(super) fn mudoutput_will<'gc>( ctx: Context<'gc>, - _global_memo: &GlobalMemoCell, + global_memo: &GlobalMemoCell, ) -> Callback<'gc> { - Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (mud, optno): (Table, u8) = stack.consume(ctx)?; + let socket: Value = mud.get(ctx, "socket")?; + let socket: &WebSocketId = + UserData::from_value(ctx, socket)?.downcast::]>()?; + let mut mud_memo = MudWithMemo { + memo: global_memo.clone(), + mud: socket.clone(), + }; + handle_incoming_will(ctx, &mut mud_memo, &mud, &Telopt(optno)); + Ok(CallbackReturn::Return) + }) } pub(super) fn mudoutput_wont<'gc>( ctx: Context<'gc>, - _global_memo: &GlobalMemoCell, + global_memo: &GlobalMemoCell, ) -> Callback<'gc> { - Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (mud, optno): (Table, u8) = stack.consume(ctx)?; + let socket: Value = mud.get(ctx, "socket")?; + let socket: &WebSocketId = + UserData::from_value(ctx, socket)?.downcast::]>()?; + let mut mud_memo = MudWithMemo { + memo: global_memo.clone(), + mud: socket.clone(), + }; + handle_incoming_wont(ctx, &mut mud_memo, &mud, &Telopt(optno)); + 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_do<'gc>(ctx: Context<'gc>, global_memo: &GlobalMemoCell) -> Callback<'gc> { + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (mud, optno): (Table, u8) = stack.consume(ctx)?; + let socket: Value = mud.get(ctx, "socket")?; + let socket: &WebSocketId = + UserData::from_value(ctx, socket)?.downcast::]>()?; + let mut mud_memo = MudWithMemo { + memo: global_memo.clone(), + mud: socket.clone(), + }; + handle_incoming_do(ctx, &mut mud_memo, &mud, &Telopt(optno)); + Ok(CallbackReturn::Return) + }) } pub(super) fn mudoutput_dont<'gc>( ctx: Context<'gc>, - _global_memo: &GlobalMemoCell, + global_memo: &GlobalMemoCell, ) -> Callback<'gc> { - Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return)) + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (mud, optno): (Table, u8) = stack.consume(ctx)?; + let socket: Value = mud.get(ctx, "socket")?; + let socket: &WebSocketId = + UserData::from_value(ctx, socket)?.downcast::]>()?; + let mut mud_memo = MudWithMemo { + memo: global_memo.clone(), + mud: socket.clone(), + }; + handle_incoming_dont(ctx, &mut mud_memo, &mud, &Telopt(optno)); + Ok(CallbackReturn::Return) + }) } pub(super) fn mudoutput_subnegotiation<'gc>( ctx: Context<'gc>, diff --git a/src/lua_engine/muds/telopt.rs b/src/lua_engine/muds/telopt.rs new file mode 100644 index 0000000..e878f25 --- /dev/null +++ b/src/lua_engine/muds/telopt.rs @@ -0,0 +1,1187 @@ +use piccolo::{Context, Table, Value}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsValue; +use web_sys::console; + +use crate::{ + telnet::{DO, DONT, IAC, WILL, WONT}, + websocket::{send_message_to_mud, WebSocketId}, + GlobalMemoCell, +}; + +// These enums use the terminology of RFC1143. +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub enum OptionState { + No, + WantNo, + WantNoOpposite, + WantYes, + WantYesOpposite, + Yes, +} + +#[derive(Eq, PartialEq)] +pub enum Side { + Us, + Him, +} + +#[derive(Eq, PartialEq, Clone)] +pub struct Telopt(pub u8); + +#[derive(Clone)] +pub struct MudWithMemo { + pub memo: GlobalMemoCell, + pub mud: WebSocketId, +} + +pub trait SendRaw { + fn send_bytes(&mut self, msg: &[u8]); +} +impl SendRaw for MudWithMemo { + fn send_bytes(&mut self, msg: &[u8]) { + let _ = send_message_to_mud(&self.mud, msg, &self.memo); + } +} + +pub trait SendOptNeg { + fn send_will(&mut self, opt: &Telopt); + fn send_wont(&mut self, opt: &Telopt); + fn send_do(&mut self, opt: &Telopt); + fn send_dont(&mut self, opt: &Telopt); +} +impl SendOptNeg for T { + fn send_will(&mut self, opt: &Telopt) { + self.send_bytes(&[IAC, WILL, opt.0]); + } + fn send_wont(&mut self, opt: &Telopt) { + self.send_bytes(&[IAC, WONT, opt.0]); + } + fn send_do(&mut self, opt: &Telopt) { + self.send_bytes(&[IAC, DO, opt.0]); + } + fn send_dont(&mut self, opt: &Telopt) { + self.send_bytes(&[IAC, DONT, opt.0]); + } +} + +pub struct OptionWithActiveSide { + option_active_on: Side, + option: Telopt, +} + +fn send_positive(mud: &mut T, opt: &OptionWithActiveSide) { + match opt.option_active_on { + Side::Him => mud.send_do(&opt.option), + Side::Us => mud.send_will(&opt.option), + } +} +fn send_negative(mud: &mut T, opt: &OptionWithActiveSide) { + match opt.option_active_on { + Side::Him => mud.send_dont(&opt.option), + Side::Us => mud.send_wont(&opt.option), + } +} + +pub fn get_option_supported<'gc>( + ctx: Context<'gc>, + input: &Table<'gc>, + opt: &Telopt, + side: &Side, +) -> bool { + let support_table: Table<'gc> = match input.get( + ctx, + match side { + Side::Us => "suppported_options_local", + Side::Him => "supported_options_remote", + }, + ) { + Err(_) => return false, + Ok(v) => v, + }; + + for (_k, v) in support_table.iter() { + if v.to_integer() == Some(opt.0 as i64) { + return true; + } + } + false +} + +pub fn set_option_supported<'gc>(ctx: Context<'gc>, input: &Table<'gc>, opt: &Telopt, side: &Side) { + let table_name = match side { + Side::Us => "suppported_options_local", + Side::Him => "supported_options_remote", + }; + let support_table: Table<'gc> = match input.get(ctx, table_name) { + Err(_) => { + let t = Table::new(&ctx); + let _ = input.set(ctx, table_name, t); + t + } + Ok(v) => v, + }; + + for (_k, v) in support_table.iter() { + if v.to_integer() == Some(opt.0 as i64) { + return; + } + } + let _ = support_table.set(ctx, opt.0, opt.0); +} + +pub fn set_option_unsupported<'gc>( + ctx: Context<'gc>, + input: &Table<'gc>, + opt: &Telopt, + side: &Side, +) { + let table_name = match side { + Side::Us => "suppported_options_local", + Side::Him => "supported_options_remote", + }; + let support_table: Table<'gc> = match input.get(ctx, table_name) { + Err(_) => { + let t = Table::new(&ctx); + let _ = input.set(ctx, table_name, t); + t + } + Ok(v) => v, + }; + + for (k, v) in support_table.iter() { + if v.to_integer() == Some(opt.0 as i64) { + let _ = support_table.set(ctx, k, Value::Nil); + } + } +} + +pub fn get_option_state<'gc>( + ctx: Context<'gc>, + input: &Table<'gc>, + opt: &Telopt, + side: &Side, +) -> OptionState { + let state_table: Table<'gc> = match input.get( + ctx, + match side { + Side::Us => "negotiation_state_local", + Side::Him => "negotiation_state_remote", + }, + ) { + Err(_) => return OptionState::No, + Ok(v) => v, + }; + let v: String = match state_table.get(ctx, opt.0) { + Err(_) => return OptionState::No, + Ok(v) => v, + }; + match serde_json::from_value(serde_json::Value::String(v)) { + Err(_) => OptionState::No, + Ok(v) => v, + } +} + +pub fn set_option_state<'gc>( + ctx: Context<'gc>, + input: &Table<'gc>, + opt: &Telopt, + side: &Side, + state: &OptionState, +) { + let table_name = match side { + Side::Us => "negotiation_state_local", + Side::Him => "negotiation_state_remote", + }; + let state_table: Table<'gc> = match input.get(ctx, table_name) { + Err(_) => { + let t = Table::new(&ctx); + let _ = input.set(ctx, table_name, t); + t + } + Ok(v) => v, + }; + let json_val = serde_json::to_value(state).expect("couldn't serialize state"); + let _ = state_table.set( + ctx, + opt.0, + json_val.as_str().expect("state wasn't a string").to_owned(), + ); +} + +fn handle_incoming_positive<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + opt: &OptionWithActiveSide, +) { + match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { + OptionState::No => { + if get_option_supported(ctx, table, &opt.option, &opt.option_active_on) { + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::Yes, + ); + send_positive(mud, opt); + } else { + send_negative(mud, opt); + } + } + OptionState::Yes => {} + OptionState::WantNo => { + console::log_1(&JsValue::from_str(&format!( + "DONT answered by WILL for option {}", + opt.option.0 + ))); + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::No, + ) + } + OptionState::WantNoOpposite => { + console::log_1(&JsValue::from_str(&format!( + "DONT answered by WILL for option {}", + opt.option.0 + ))); + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::Yes, + ) + } + OptionState::WantYes => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::Yes, + ), + OptionState::WantYesOpposite => { + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantNo, + ); + send_negative(mud, opt); + } + } +} + +fn handle_incoming_negative<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + opt: &OptionWithActiveSide, +) { + match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { + OptionState::No => {} + OptionState::Yes => { + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::No, + ); + send_negative(mud, opt) + } + OptionState::WantNo => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::No, + ), + OptionState::WantNoOpposite => { + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantYes, + ); + send_positive(mud, opt); + } + OptionState::WantYes => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::No, + ), + OptionState::WantYesOpposite => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::No, + ), + } +} + +pub fn handle_incoming_will<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + option: &Telopt, +) { + handle_incoming_positive( + ctx, + mud, + table, + &OptionWithActiveSide { + option: option.clone(), + option_active_on: Side::Him, + }, + ); +} + +pub fn handle_incoming_wont<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + option: &Telopt, +) { + handle_incoming_negative( + ctx, + mud, + table, + &OptionWithActiveSide { + option: option.clone(), + option_active_on: Side::Him, + }, + ); +} + +pub fn handle_incoming_do<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + option: &Telopt, +) { + handle_incoming_positive( + ctx, + mud, + table, + &OptionWithActiveSide { + option: option.clone(), + option_active_on: Side::Us, + }, + ); +} + +pub fn handle_incoming_dont<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + option: &Telopt, +) { + handle_incoming_negative( + ctx, + mud, + table, + &OptionWithActiveSide { + option: option.clone(), + option_active_on: Side::Us, + }, + ); +} + +pub const GMCP_TELOPT: Telopt = Telopt(201); + +fn negotiate_option_on<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + opt: &OptionWithActiveSide, +) { + match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { + OptionState::No => { + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantYes, + ); + send_positive(mud, opt); + } + OptionState::Yes => {} + OptionState::WantNo => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantNoOpposite, + ), + OptionState::WantNoOpposite => {} + OptionState::WantYes => {} + OptionState::WantYesOpposite => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantYes, + ), + } +} + +fn negotiate_option_off<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + opt: &OptionWithActiveSide, +) { + match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { + OptionState::No => {} + OptionState::Yes => { + set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantNo, + ); + send_negative(mud, opt); + } + OptionState::WantNo => {} + OptionState::WantNoOpposite => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantNo, + ), + OptionState::WantYes => set_option_state( + ctx, + table, + &opt.option, + &opt.option_active_on, + &OptionState::WantYesOpposite, + ), + OptionState::WantYesOpposite => {} + } +} + +pub fn negotiate_option<'gc, T: SendOptNeg>( + ctx: Context<'gc>, + mud: &mut T, + table: &Table<'gc>, + opt: &OptionWithActiveSide, + desired_state: bool, +) { + if desired_state { + negotiate_option_on(ctx, mud, table, opt); + } else { + negotiate_option_off(ctx, mud, table, opt); + } +} + +#[cfg(test)] +mod tests { + use piccolo::{Context, Lua, Table}; + + use crate::{ + lua_engine::telopt::{set_option_state, OptionState, Telopt}, + telnet::{parse_telnet_buf, TelnetOutput}, + }; + + use super::{ + get_option_state, handle_incoming_do, handle_incoming_dont, handle_incoming_will, + handle_incoming_wont, negotiate_option, set_option_supported, OptionWithActiveSide, + SendRaw, Side, GMCP_TELOPT, + }; + + #[test] + fn get_option_defaults_no() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + let root_tab = Table::new(&ctx); + assert_eq!( + get_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Him), + OptionState::No + ); + }); + } + + #[test] + fn get_set_option_state_roundtrips() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + let root_tab = Table::new(&ctx); + set_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Him, &OptionState::Yes); + set_option_state( + ctx, + &root_tab, + &GMCP_TELOPT, + &Side::Us, + &OptionState::WantNoOpposite, + ); + set_option_state( + ctx, + &root_tab, + &Telopt(123), + &Side::Us, + &OptionState::WantYes, + ); + assert_eq!( + get_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Him), + OptionState::Yes + ); + assert_eq!( + get_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Us), + OptionState::WantNoOpposite + ); + assert_eq!( + get_option_state(ctx, &root_tab, &Telopt(123), &Side::Us), + OptionState::WantYes + ); + }); + } + + pub enum SideTestAction { + SetOptionSupported { + option: OptionWithActiveSide, + }, + SetOptionEnabled { + state: bool, + option: OptionWithActiveSide, + }, + AssertOptionState { + expected_state: OptionState, + option: OptionWithActiveSide, + }, + } + + pub enum TestEvent { + SideAAction(SideTestAction), + SideBAction(SideTestAction), + SyncUntilStatic, + } + + pub struct FakeMudBuffer(Vec); + impl SendRaw for FakeMudBuffer { + fn send_bytes(&mut self, msg: &[u8]) { + self.0.extend_from_slice(msg); + } + } + + pub struct TwoPartySim<'gc> { + table_a: Table<'gc>, + table_b: Table<'gc>, + buffer_a: FakeMudBuffer, + buffer_b: FakeMudBuffer, + } + + fn run_test_event<'gc>(ctx: Context<'gc>, sim_state: &mut TwoPartySim<'gc>, event: TestEvent) { + match event { + TestEvent::SideAAction(act) => { + run_side_action(ctx, act, sim_state.table_a, &mut sim_state.buffer_a) + } + TestEvent::SideBAction(act) => { + run_side_action(ctx, act, sim_state.table_b, &mut sim_state.buffer_b) + } + TestEvent::SyncUntilStatic => sync_until_static(ctx, sim_state), + } + } + + fn run_simulation<'gc>(ctx: Context<'gc>, simulation: Vec) { + let mut state = TwoPartySim { + table_a: Table::new(&ctx), + table_b: Table::new(&ctx), + buffer_a: FakeMudBuffer(Vec::new()), + buffer_b: FakeMudBuffer(Vec::new()), + }; + for event in simulation { + run_test_event(ctx, &mut state, event); + } + } + + fn run_side_action<'gc>( + ctx: Context<'gc>, + act: SideTestAction, + table: Table<'gc>, + buffer: &mut FakeMudBuffer, + ) { + match act { + SideTestAction::SetOptionEnabled { state, option } => { + negotiate_option(ctx, buffer, &table, &option, state) + } + SideTestAction::AssertOptionState { + expected_state, + option, + } => { + assert_eq!( + get_option_state(ctx, &table, &option.option, &option.option_active_on), + expected_state + ); + } + SideTestAction::SetOptionSupported { option } => { + set_option_supported(ctx, &table, &option.option, &option.option_active_on) + } + } + } + + fn sync_until_static<'gc>(ctx: Context<'gc>, sim_state: &mut TwoPartySim<'gc>) { + loop { + if sim_state.buffer_a.0.is_empty() && sim_state.buffer_b.0.is_empty() { + break; + } + let (remaining, msg_a_to_b) = parse_telnet_buf(&sim_state.buffer_a.0); + sim_state.buffer_a.0 = remaining; + let (remaining, msg_b_to_a) = parse_telnet_buf(&sim_state.buffer_b.0); + sim_state.buffer_b.0 = remaining; + + println!( + "Message exchange: A->B: {:#?}, B->A: {:#?}", + &msg_a_to_b, &msg_b_to_a + ); + + simulate_msg_to(ctx, msg_a_to_b, sim_state.table_b, &mut sim_state.buffer_b); + simulate_msg_to(ctx, msg_b_to_a, sim_state.table_a, &mut sim_state.buffer_a); + } + } + + fn simulate_msg_to<'gc>( + ctx: Context<'gc>, + msg: Option, + table: Table<'gc>, + buffer: &mut FakeMudBuffer, + ) { + match msg { + Some(TelnetOutput::Will(n)) => handle_incoming_will(ctx, buffer, &table, &Telopt(n)), + Some(TelnetOutput::Wont(n)) => handle_incoming_wont(ctx, buffer, &table, &Telopt(n)), + Some(TelnetOutput::Do(n)) => handle_incoming_do(ctx, buffer, &table, &Telopt(n)), + Some(TelnetOutput::Dont(n)) => handle_incoming_dont(ctx, buffer, &table, &Telopt(n)), + _ => {} + } + } + + use SideTestAction::*; + use TestEvent::*; + + #[test] + fn negotiating_unsupported_local_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_unsupported_remote_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_bothways_local_converges_both_on() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::Yes, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::Yes, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_converges_both_on() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionSupported { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::Yes, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::Yes, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_then_changing_locally_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: false, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_then_changing_remotely_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SyncUntilStatic, + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: false, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_and_changing_locally_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: false, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_and_changing_remotely_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: false, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_and_changing_remotely_twice_converges_both_on() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: false, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::Yes, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::Yes, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_and_changing_locally_once_remotely_twice_converges_both_off() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: false, + }), + SideBAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + state: true, + }), + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: false, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::No, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::No, + }), + ], + ) + }); + } + + #[test] + fn negotiating_supported_local_then_changing_locally_and_reversing_converges_both_on() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + run_simulation( + ctx, + vec![ + SideBAction(SetOptionSupported { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + }), + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: false, + }), + SideAAction(SetOptionEnabled { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + state: true, + }), + SyncUntilStatic, + SideAAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Us, + }, + expected_state: OptionState::Yes, + }), + SideBAction(AssertOptionState { + option: OptionWithActiveSide { + option: Telopt(123), + option_active_on: Side::Him, + }, + expected_state: OptionState::Yes, + }), + ], + ) + }); + } +} diff --git a/src/telnet.rs b/src/telnet.rs index 18f0518..66f0265 100644 --- a/src/telnet.rs +++ b/src/telnet.rs @@ -18,7 +18,7 @@ pub enum TelnetOutput { Subnegotiation(Vec), } -const IAC: u8 = 255; +pub const IAC: u8 = 255; const NOP: u8 = 241; const DATA_MARK: u8 = 242; const BREAK: u8 = 243; @@ -31,10 +31,10 @@ 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 const WILL: u8 = 251; +pub const WONT: u8 = 252; +pub const DO: u8 = 253; +pub const DONT: u8 = 254; pub fn parse_telnet_buf(input: &[u8]) -> (Vec, Option) { let mut ptr: &[u8] = input;