Implement NAWS (negotiate about window size)

This commit is contained in:
Condorra 2024-11-09 22:53:30 +11:00
parent c0739262af
commit 78e36e4dcb
6 changed files with 375 additions and 52 deletions

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use gc_arena::Collect;
use wasm_bindgen::prelude::*;
use web_sys::{Element, Node};
use web_sys::{console, Element, Node};
use yew::prelude::*;
use crate::{
@ -164,7 +164,15 @@ fn get_or_make_term_frame<'a>(
.ok()
})
})
.unwrap_or(())
.unwrap_or(());
let mut engine = globals_for_resize.lua_engine.borrow_mut();
match engine.dispatch_resize(&frame_for_resize, dims.cols(), dims.rows()) {
Err(e) => console::log_1(&JsValue::from_str(&format!(
"Error sending resize to Lua: {}",
e
))),
Ok(()) => {}
}
});
term.onResize(&resize_closure);

View File

@ -1,5 +1,6 @@
use self::{frameroutes::*, frames::*, muds::*, storage::*};
use anyhow::Error;
use muds::telopt::configure_telopt_table;
use piccolo::{
async_callback::{AsyncSequence, Locals},
meta_ops::{self, MetaResult},
@ -127,6 +128,31 @@ impl LuaState {
self.interp.execute(&self.exec)?;
Ok(())
}
pub(crate) fn dispatch_resize(
&mut self,
frame: &FrameId,
width: u16,
height: u16,
) -> anyhow::Result<()> {
self.interp.try_enter(|ctx| {
let frames: Table = ctx.get_global("frames")?;
let frame_tab: Value = frames.get(ctx, frame.0 as i64)?;
if frame_tab.is_nil() {
Err(anyhow::Error::msg(
"Dispatching command to frame missing in Lua frames.",
))?;
}
ctx.fetch(&self.exec).restart(
ctx,
Function::Callback(send_resize_to_frame(ctx)),
(frame_tab, width, height),
);
Ok(())
})?;
self.interp.execute(&self.exec)?;
Ok(())
}
}
pub fn install_lua_globals(
@ -209,6 +235,11 @@ pub fn install_lua_globals(
ctx.set_global("commands", cmd_table);
let info_table = Table::new(&ctx);
ctx.set_global("info", info_table);
let telopt_table = Table::new(&ctx);
let _ = info_table.set(ctx, "telopts", telopt_table);
configure_telopt_table(ctx, &telopt_table);
let muds_table = Table::new(&ctx);
ctx.set_global("muds", muds_table);
let frames_table = Table::new(&ctx);
@ -284,6 +315,8 @@ pub fn install_lua_globals(
register_class_function!(mud_class_table, mudinput_line);
register_class_function!(mud_class_table, "closed", mudclass_closed);
register_stateless_class_function!(mud_class_table, "new", new_mud);
register_class_function!(mud_class_table, "term_resized", mud_term_resized);
register_class_function!(mud_class_table, local_naws_enabled);
macro_rules! register_class_nop {
($class_table: ident, $sym: ident) => {
@ -308,6 +341,7 @@ pub fn install_lua_globals(
frame_class_table.set(ctx, MetaMethod::Index, frame_class_table)?;
register_class_function!(frame_class_table, "new", new_frame);
register_class_function!(frame_class_table, "input", frame_input);
register_class_function!(frame_class_table, "resize", frame_resize);
let frameroute_class_table = Table::new(&ctx);
classes_table.set(ctx, "frameroute", frameroute_class_table)?;
@ -384,14 +418,15 @@ pub fn prep_metaop_call<'gc, const N: usize>(
}
}
pub async fn call_checking_metatable<'gc, T: Fetchable>(
pub async fn call_checking_metatable<'gc, T: Fetchable, FN>(
seq: &mut AsyncSequence,
obj: T,
func_name: &'static str,
func_name: FN,
arguments: &[StashedValue],
) -> Result<(), StashedError>
where
for<'gcb> <T as Fetchable>::Fetched<'gcb>: IntoValue<'gcb>,
for<'gcb> FN: IntoValue<'gcb>,
{
let call = seq.try_enter(|ctx, locals, _execution, mut stack| {
let obj = locals.fetch(&obj);
@ -400,11 +435,7 @@ where
ctx,
stack,
locals,
meta_ops::index(
ctx,
obj.into_value(ctx),
ctx.intern_static(func_name.as_bytes()).into_value(ctx),
)?,
meta_ops::index(ctx, obj.into_value(ctx), func_name.into_value(ctx))?,
))
})?;
if let Some(call) = call {

View File

@ -106,7 +106,8 @@ pub fn list_match_tab<'gc>(
let frame = locals.stash(&ctx, intern_id(ctx, frame));
let aliases = locals.stash(&ctx, aliases);
async move {
call_checking_metatable::<StashedUserData>(&mut seq, aliases, "lua_table", &[]).await?;
call_checking_metatable::<StashedUserData, _>(&mut seq, aliases, "lua_table", &[])
.await?;
let echo_func = seq.try_enter(|ctx, locals, _ex, mut stack| {
let alias_tab: Table = stack.consume(ctx)?;
stack.push_back(locals.fetch(&frame).into_value(ctx));
@ -321,7 +322,7 @@ pub fn ensure_frame_instance<'gc>(ctx: Context<'gc>, frame: &FrameId) -> Callbac
let frame_tab = locals.stash(&ctx, frame_tab);
let frame = locals.stash(&ctx, frame);
async move {
call_checking_metatable::<StashedTable>(&mut seq, frame_tab, "new", &[frame])
call_checking_metatable::<StashedTable, _>(&mut seq, frame_tab, "new", &[frame])
.await?;
Ok(SequenceReturn::Return)
}
@ -372,7 +373,8 @@ pub(super) fn send_command_to_frame<'gc>(ctx: Context<'gc>) -> Callback<'gc> {
let frame = locals.stash(&ctx, frame);
let line = locals.stash(&ctx, line.into_value(ctx));
async move {
call_checking_metatable::<StashedTable>(&mut seq, frame, "input", &[line]).await?;
call_checking_metatable::<StashedTable, _>(&mut seq, frame, "input", &[line])
.await?;
Ok(SequenceReturn::Return)
}
});
@ -403,7 +405,7 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell)
let frame_tab = locals.stash(&ctx, frame_tab);
let frame = locals.stash(&ctx, frame);
async move {
call_checking_metatable::<StashedUserData>(
call_checking_metatable::<StashedUserData, _>(
&mut seq,
aliases.clone(),
"try_run_sub",
@ -426,7 +428,7 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell)
return Ok(SequenceReturn::Return);
}
call_checking_metatable::<StashedValue>(
call_checking_metatable::<StashedValue, _>(
&mut seq,
linked_mud,
"mudinput_line",
@ -444,6 +446,77 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell)
})
}
pub(super) fn send_resize_to_frame<'gc>(ctx: Context<'gc>) -> Callback<'gc> {
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let (frame, width, height): (Table, u16, u16) = stack.consume(ctx)?;
let seq = async_sequence(&ctx, |locals, mut seq| {
let frame = locals.stash(&ctx, frame);
let width = locals.stash(&ctx, width.into_value(ctx));
let height = locals.stash(&ctx, height.into_value(ctx));
async move {
call_checking_metatable::<StashedTable, _>(
&mut seq,
frame,
"resize",
&[width, height],
)
.await?;
Ok(SequenceReturn::Return)
}
});
Ok(CallbackReturn::Sequence(seq))
})
}
pub(super) fn frame_resize<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) -> Callback<'gc> {
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let callback_return = {
let frame_tab: Table = Table::from_value(
ctx,
stack
.pop_front()
.ok_or_else(|| anyhow::Error::msg("classes.frame:resize missing object!"))?,
)?;
let width: Value = stack
.pop_front()
.ok_or_else(|| anyhow::Error::msg("classes.frame:resize missing width!"))?;
let height: Value = stack
.pop_front()
.ok_or_else(|| anyhow::Error::msg("classes.frame:resize missing height!"))?;
stack.consume(ctx)?;
frame_tab.set(ctx, "width", width)?;
frame_tab.set(ctx, "height", height)?;
let linked_mud: Value = frame_tab.get(ctx, "linked_mud")?;
if linked_mud.is_nil() {
return Ok(piccolo::CallbackReturn::Return);
}
let seq = async_sequence(&ctx, |locals, mut seq| {
let width = locals.stash(&ctx, width);
let height = locals.stash(&ctx, height);
let linked_mud = locals.stash(&ctx, linked_mud);
async move {
call_checking_metatable::<StashedValue, _>(
&mut seq,
linked_mud,
"term_resized",
&[width, height],
)
.await?;
Ok(SequenceReturn::Return)
}
});
Ok(piccolo::CallbackReturn::Sequence(seq))
};
callback_return
})
}
fn list_timers<'gc>(
ctx: Context<'gc>,
global_memo: &GlobalMemoCell,

View File

@ -14,14 +14,15 @@ use crate::{
id_intern::{intern_id, unintern_id},
logging::{start_deleting_logs, start_downloading_logs, start_listing_logs},
match_table::{create_match_table, match_table_add, match_table_remove},
telnet::{parse_telnet_buf, TelnetOutput},
telnet::{parse_telnet_buf, TelnetOutput, IAC},
websocket::{connect_websocket, send_message_to_mud, WebSocketId},
FrameId, GlobalLayoutCell, GlobalMemoCell,
};
use self::telopt::{
handle_incoming_do, handle_incoming_dont, handle_incoming_will, handle_incoming_wont,
MudWithMemo, Telopt,
get_option_state, handle_incoming_do, handle_incoming_dont, handle_incoming_will,
handle_incoming_wont, send_subnegotiation_if_allowed, set_option_supported, MudWithMemo,
OptionState, Side, Telopt, NAWS_TELOPT,
};
use super::{call_checking_metatable, list_match_tab, try_unwrap_frame, LuaState};
@ -87,8 +88,13 @@ pub(super) fn mudclose<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) ->
let seq = piccolo::async_sequence(&ctx, |locals, mut seq| {
let conntab = locals.stash(&ctx, conntab);
async move {
call_checking_metatable::<StashedTable>(&mut seq, conntab.clone(), "closed", &[])
.await?;
call_checking_metatable::<StashedTable, _>(
&mut seq,
conntab.clone(),
"closed",
&[],
)
.await?;
Ok(piccolo::SequenceReturn::Return)
}
});
@ -169,12 +175,17 @@ pub(super) fn connect_mud<'gc>(
let global_memo = global_memo.clone();
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let mut trusted: bool = false;
let mut naws: bool = true;
let name: String = loop {
let v: String = stack.from_front(ctx)?;
if v == "-trust" {
trusted = true;
continue;
}
if v == "-no_naws" {
naws = false;
continue;
}
break v;
};
let name: Value<'gc> = ctx.intern(name.as_bytes()).into();
@ -225,11 +236,16 @@ pub(super) fn connect_mud<'gc>(
}
}
}
if naws {
set_option_supported(ctx, &conntab, &NAWS_TELOPT, &Side::Us);
}
// Call conntab:new...
let seq = async_sequence(&ctx, |locals, mut seq| {
let conntab = locals.stash(&ctx, conntab);
async move {
call_checking_metatable::<StashedTable>(&mut seq, conntab, "new", &[]).await?;
call_checking_metatable::<StashedTable, _>(&mut seq, conntab, "new", &[]).await?;
Ok(SequenceReturn::Return)
}
});
@ -344,7 +360,7 @@ pub(super) fn mudoutput<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) -
.collect();
async move {
for (func_name, params) in fns {
call_checking_metatable::<StashedTable>(
call_checking_metatable::<StashedTable, _>(
&mut seq,
conntab.clone(),
func_name,
@ -434,7 +450,7 @@ pub(super) fn mudoutput_line<'gc>(
let triggers = locals.stash(&ctx, triggers);
let default_frame = locals.stash(&ctx, default_frame);
async move {
call_checking_metatable::<StashedUserData>(
call_checking_metatable::<StashedUserData, _>(
&mut seq,
triggers,
"try_run_sub",
@ -442,7 +458,7 @@ pub(super) fn mudoutput_line<'gc>(
)
.await?;
for frameroute in frameroutes {
call_checking_metatable::<StashedTable>(
call_checking_metatable::<StashedTable, _>(
&mut seq,
frameroute,
"route",
@ -463,6 +479,19 @@ pub(super) fn mudoutput_prompt<'gc>(
) -> Callback<'gc> {
Callback::from_fn(&ctx, move |_ctx, _ex, _stack| Ok(CallbackReturn::Return))
}
fn name_telopt(ctx: Context, optno: u8) -> anyhow::Result<String> {
for (k, v) in ctx
.get_global::<Table>("info")?
.get::<_, Table>(ctx, "telopts")?
{
if u8::from_value(ctx, v)? == optno {
return Ok(String::from_value(ctx, k)?);
}
}
Ok(format!("{}", optno))
}
pub(super) fn mudoutput_will<'gc>(
ctx: Context<'gc>,
global_memo: &GlobalMemoCell,
@ -477,8 +506,26 @@ pub(super) fn mudoutput_will<'gc>(
memo: global_memo.clone(),
mud: socket.clone(),
};
handle_incoming_will(ctx, &mut mud_memo, &mud, &Telopt(optno));
Ok(CallbackReturn::Return)
if handle_incoming_will(ctx, &mut mud_memo, &mud, &Telopt(optno)) {
let seq = async_sequence(&ctx, move |locals, mut seq| {
let mud = locals.stash(&ctx, mud);
async move {
let name =
seq.try_enter(|ctx, _locals, _ex, _stack| Ok(name_telopt(ctx, optno)?))?;
call_checking_metatable::<StashedTable, _>(
&mut seq,
mud,
format!("remote_{}_enabled", name),
&[],
)
.await?;
Ok(SequenceReturn::Return)
}
});
Ok(CallbackReturn::Sequence(seq))
} else {
Ok(CallbackReturn::Return)
}
})
}
pub(super) fn mudoutput_wont<'gc>(
@ -510,8 +557,26 @@ pub(super) fn mudoutput_do<'gc>(ctx: Context<'gc>, global_memo: &GlobalMemoCell)
memo: global_memo.clone(),
mud: socket.clone(),
};
handle_incoming_do(ctx, &mut mud_memo, &mud, &Telopt(optno));
Ok(CallbackReturn::Return)
if handle_incoming_do(ctx, &mut mud_memo, &mud, &Telopt(optno)) {
let seq = async_sequence(&ctx, move |locals, mut seq| {
let mud = locals.stash(&ctx, mud);
async move {
let name =
seq.try_enter(|ctx, _locals, _ex, _stack| Ok(name_telopt(ctx, optno)?))?;
call_checking_metatable::<StashedTable, _>(
&mut seq,
mud,
format!("local_{}_enabled", name),
&[],
)
.await?;
Ok(SequenceReturn::Return)
}
});
Ok(CallbackReturn::Sequence(seq))
} else {
Ok(CallbackReturn::Return)
}
})
}
pub(super) fn mudoutput_dont<'gc>(
@ -617,8 +682,13 @@ pub(super) fn new_mud<'gc>(ctx: Context<'gc>) -> Callback<'gc> {
let mud = locals.stash(&ctx, mud);
async move {
call_checking_metatable::<StashedTable>(&mut seq, frameroute, "new", &[curr_frame])
.await?;
call_checking_metatable::<StashedTable, _>(
&mut seq,
frameroute,
"new",
&[curr_frame],
)
.await?;
seq.call(&create_match_tab, 0).await?;
seq.try_enter(|ctx, locals, _ex, mut stack| {
@ -734,6 +804,73 @@ pub(super) fn mud_untrigger(ctx: Context<'_>) -> Callback<'_> {
})
}
pub(super) fn mud_term_resized<'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, width, height): (Table, u16, u16) = stack.consume(ctx)?;
let socket: Value = mud.get(ctx, "socket")?;
let socket: &WebSocketId =
UserData::from_value(ctx, socket)?.downcast::<Rootable![Gc<'_, WebSocketId>]>()?;
send_subnegotiation_if_allowed(
ctx,
&mud,
&NAWS_TELOPT,
&Side::Us,
&mut MudWithMemo {
memo: global_memo.clone(),
mud: socket.clone(),
},
&[
(width >> 8) as u8,
(width & 0xFF) as u8,
(height >> 8) as u8,
(height & 0xFF) as u8,
],
);
Ok(CallbackReturn::Return)
})
}
pub(super) fn local_naws_enabled<'gc>(
ctx: Context<'gc>,
global_state_memo: &GlobalMemoCell,
) -> Callback<'gc> {
let global_state_memo = global_state_memo.clone();
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let mud: Table = stack.consume(ctx)?;
let frameroutes: Table = mud.get(ctx, "frameroutes")?;
let default_frame: FrameId = try_unwrap_frame(
ctx,
&frameroutes
.iter()
.map(|(_k, v)| Ok::<Table, Error>(Table::from_value(ctx, v)?))
.next()
.unwrap_or_else(|| Ok(ctx.get_global::<Table>("frames")?.get(ctx, 1_i64)?))?
.get(ctx, "frame")?,
)?;
let default_frame: Table = ctx
.get_global::<Table>("frames")?
.get(ctx, default_frame.0 as i64)?;
let width: Value = default_frame.get(ctx, "width")?;
let height: Value = default_frame.get(ctx, "height")?;
if width.is_nil() || height.is_nil() {
return Ok(CallbackReturn::Return);
}
stack.push_back(mud.into_value(ctx));
stack.push_back(width);
stack.push_back(height);
let cb = mud_term_resized(ctx, &global_state_memo);
Ok(CallbackReturn::Call {
function: Function::Callback(cb),
then: None,
})
})
}
fn name_mud<'gc>(ctx: Context<'gc>, mud: &Table<'gc>) -> String {
fn name_mud_internal<'gc>(ctx: Context<'gc>, mud: &Table<'gc>) -> anyhow::Result<String> {
let socket: Value<'gc> = mud.get(ctx, "socket")?;

View File

@ -4,7 +4,7 @@ use wasm_bindgen::JsValue;
use web_sys::console;
use crate::{
telnet::{DO, DONT, IAC, WILL, WONT},
telnet::{DO, DONT, ENDSUB, IAC, STARTSUB, WILL, WONT},
websocket::{send_message_to_mud, WebSocketId},
GlobalMemoCell,
};
@ -182,6 +182,29 @@ pub fn get_option_state<'gc>(
}
}
pub fn send_subnegotiation_if_allowed<'gc, T: SendRaw>(
ctx: Context<'gc>,
mud_table: &Table<'gc>,
opt: &Telopt,
side: &Side,
mud: &mut T,
msg: &[u8],
) {
if get_option_state(ctx, mud_table, opt, side) == OptionState::Yes {
let mut buf: Vec<u8> = Vec::with_capacity(msg.len() + 4);
buf.extend_from_slice(&[IAC, STARTSUB]);
for c in msg {
if *c == IAC {
buf.extend_from_slice(b"\xff\xff");
} else {
buf.push(*c);
}
}
buf.extend_from_slice(&[IAC, ENDSUB]);
mud.send_bytes(&buf);
}
}
pub fn set_option_state<'gc>(
ctx: Context<'gc>,
input: &Table<'gc>,
@ -214,7 +237,7 @@ fn handle_incoming_positive<'gc, T: SendOptNeg>(
mud: &mut T,
table: &Table<'gc>,
opt: &OptionWithActiveSide,
) {
) -> bool {
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) {
@ -226,6 +249,7 @@ fn handle_incoming_positive<'gc, T: SendOptNeg>(
&OptionState::Yes,
);
send_positive(mud, opt);
return true;
} else {
send_negative(mud, opt);
}
@ -255,15 +279,19 @@ fn handle_incoming_positive<'gc, T: SendOptNeg>(
&opt.option,
&opt.option_active_on,
&OptionState::Yes,
)
);
return true;
}
OptionState::WantYes => {
set_option_state(
ctx,
table,
&opt.option,
&opt.option_active_on,
&OptionState::Yes,
);
return true;
}
OptionState::WantYes => set_option_state(
ctx,
table,
&opt.option,
&opt.option_active_on,
&OptionState::Yes,
),
OptionState::WantYesOpposite => {
set_option_state(
ctx,
@ -275,6 +303,7 @@ fn handle_incoming_positive<'gc, T: SendOptNeg>(
send_negative(mud, opt);
}
}
false
}
fn handle_incoming_negative<'gc, T: SendOptNeg>(
@ -334,7 +363,7 @@ pub fn handle_incoming_will<'gc, T: SendOptNeg>(
mud: &mut T,
table: &Table<'gc>,
option: &Telopt,
) {
) -> bool {
handle_incoming_positive(
ctx,
mud,
@ -343,7 +372,7 @@ pub fn handle_incoming_will<'gc, T: SendOptNeg>(
option: option.clone(),
option_active_on: Side::Him,
},
);
)
}
pub fn handle_incoming_wont<'gc, T: SendOptNeg>(
@ -368,7 +397,7 @@ pub fn handle_incoming_do<'gc, T: SendOptNeg>(
mud: &mut T,
table: &Table<'gc>,
option: &Telopt,
) {
) -> bool {
handle_incoming_positive(
ctx,
mud,
@ -377,7 +406,7 @@ pub fn handle_incoming_do<'gc, T: SendOptNeg>(
option: option.clone(),
option_active_on: Side::Us,
},
);
)
}
pub fn handle_incoming_dont<'gc, T: SendOptNeg>(
@ -398,6 +427,7 @@ pub fn handle_incoming_dont<'gc, T: SendOptNeg>(
}
pub const GMCP_TELOPT: Telopt = Telopt(201);
pub const NAWS_TELOPT: Telopt = Telopt(31);
fn negotiate_option_on<'gc, T: SendOptNeg>(
ctx: Context<'gc>,
@ -487,6 +517,15 @@ pub fn negotiate_option<'gc, T: SendOptNeg>(
}
}
pub fn configure_telopt_table<'gc>(ctx: Context<'gc>, table: &Table<'gc>) {
table
.set(ctx, "naws", NAWS_TELOPT.0)
.expect("Can't set NAWS in telopt table");
table
.set(ctx, "gmcp", GMCP_TELOPT.0)
.expect("Can't set GMCP in telopt table");
}
#[cfg(test)]
mod tests {
use piccolo::{Context, Lua, Table};

View File

@ -29,8 +29,8 @@ const ERASECHAR: u8 = 247;
const ERASELINE: u8 = 248;
const GOAHEAD: u8 = 249;
const EOR: u8 = 239;
const STARTSUB: u8 = 250;
const ENDSUB: u8 = 240;
pub const STARTSUB: u8 = 250;
pub const ENDSUB: u8 = 240;
pub const WILL: u8 = 251;
pub const WONT: u8 = 252;
pub const DO: u8 = 253;
@ -146,22 +146,22 @@ pub fn parse_telnet_buf(input: &[u8]) -> (Vec<u8>, Option<TelnetOutput>) {
}
}
Some(&STARTSUB) => {
match (1..ptr.len() - 1).find(|i| ptr[*i] == IAC && ptr[*i + 1] == ENDSUB) {
match try_parse_subneg(&ptr[1..]) {
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((subneg, new_ptr)) => {
textbuf.extend_from_slice(new_ptr);
return (textbuf, Some(TelnetOutput::Subnegotiation(subneg)));
}
}
}
Some(&IAC) => {
textbuf.push(IAC);
}
Some(c) => {
// Completely unexpected command. Warn and ignore.
console::log_1(&JsValue::from_str(&format!(
@ -177,6 +177,34 @@ pub fn parse_telnet_buf(input: &[u8]) -> (Vec<u8>, Option<TelnetOutput>) {
}
}
fn try_parse_subneg(mut ptr: &[u8]) -> Option<(Vec<u8>, &[u8])> {
let mut subneg: Vec<u8> = Vec::new();
loop {
match ptr.first() {
None => return None,
Some(&IAC) => {
ptr = &ptr[1..];
match ptr.first() {
None => return None,
Some(&ENDSUB) => {
ptr = &ptr[1..];
return Some((subneg, ptr));
}
Some(c) => {
// For c != IAC this is not expected.
subneg.push(*c);
ptr = &ptr[1..];
}
}
}
Some(c) => {
subneg.push(*c);
ptr = &ptr[1..];
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -284,5 +312,12 @@ mod tests {
Some(TelnetOutput::Subnegotiation(b"blah".to_vec()))
)
);
assert_eq!(
parse_telnet_buf(b"Hello \xff\xfablah\xff\xff\xff\xf0World!"), // Partial subnegotiation
(
b"Hello World!".to_vec(),
Some(TelnetOutput::Subnegotiation(b"blah\xff".to_vec()))
)
);
}
}