Expand description
libmudtelnet-rs — Robust, event‑driven Telnet (RFC 854) for MUD clients
libmudtelnet-rs turns a raw Telnet byte stream into a sequence of strongly-typed events you can act on. It prioritizes correctness, compatibility with real MUD servers, and performance in hot paths.
- Minimal allocations in hot paths using
bytes::Bytes/BytesMut. - Defensive parsing of malformed and truncated inputs (no panics on input).
- Event semantics compatible with
libtelnet-rswhere practical. - Optional
no_stdsupport (disable default features).
When to use this crate
- You are building a MUD client or tool and need reliable Telnet parsing.
- You want clean events for negotiation, subnegotiation (GMCP/MSDP/etc.), and data, without dealing with byte-level edge cases.
Core concepts
Parser: Stateful Telnet parser. Feed it bytes; it returnsVec<TelnetEvents>.events: DefinesTelnetEvents, the event enum your code matches on.telnet: Constants for Telnet commands (op_command) and options (op_option).compatibility: Negotiation support/state table used by the parser.
Quickstart
use libmudtelnet_rs::{Parser, events::TelnetEvents};
let mut parser = Parser::new();
let events = parser.receive(b"hello \xff\xff world\r\n");
for ev in events {
match ev {
TelnetEvents::DataReceive(buf) => {
// Application text/data from the server
let _bytes = &buf[..];
}
TelnetEvents::Negotiation(n) => {
// WILL/WONT/DO/DONT notifications (state is tracked internally)
let _cmd = n.command;
let _opt = n.option;
}
TelnetEvents::Subnegotiation(sub) => {
// Protocol payload (e.g., GMCP/MSDP)
let _which = sub.option;
let _payload = &sub.buffer[..];
}
TelnetEvents::IAC(_)
| TelnetEvents::DataSend(_)
| TelnetEvents::DecompressImmediate(_) => {}
_ => {}
}
}
// Sending text (IAC bytes escaped for you):
let _send = parser.send_text("look\r\n");Negotiation and subnegotiation
- Use
Parser::_will,Parser::_wont,Parser::_do,Parser::_dontto drive option state changes for options you support (seecompatibility). - Use
Parser::subnegotiationto send payloads; the parser wraps bytes withIAC SB <option> ... IAC SEand escapesIACinside the body. - GMCP/MSDP interop: Once the server offers
WILL GMCP|MSDPand the client responds withDO, both sides may send subnegotiations. The parser treats GMCP and MSDP as bidirectional afterWILL/DO: it will accept their subnegotiations when either side is active, and it will allow the client to send them when the remote side is active, even if the client never sentWILL. This matches common MUD server behavior and avoids handshake deadlocks.
MCCP decompression boundary
- For MCCP2/3, when a compression turn‑on is received and supported, the parser
emits a
TelnetEvents::Subnegotiationfollowed byTelnetEvents::DecompressImmediatecontaining the bytes that must be decompressed before feeding back intoParser::receive.
Example: handling MCCP2/3 boundaries
use libmudtelnet_rs::{Parser, events::TelnetEvents};
fn decompress_identity(data: &[u8]) -> Vec<u8> { data.to_vec() }
let mut parser = Parser::new();
for ev in parser.receive(&[]) {
match ev {
TelnetEvents::DecompressImmediate(data) => {
let decompressed = decompress_identity(&data);
let more = parser.receive(&decompressed);
// handle `more` like any other events
drop(more);
}
_ => {}
}
}no_std
- Disable default features to build in
no_stdenvironments:libmudtelnet-rs = { version = "*", default-features = false } - In
no_std, APIs behave the same; internal buffers usebytes.
Tips
- Always write out
TelnetEvents::DataSendexactly as provided. - Treat
TelnetEvents::DataReceiveas application data; it has already had any doubled IACs unescaped. - Negotiate only options you actually support (via
compatibility). - Prefer working with
&[u8]/Bytespayloads; avoidStringunless you know the server’s encoding.
Common recipes
- NAWS (window size) send
let mut parser = Parser::new(); let mut payload = BytesMut::with_capacity(4); payload.put_u16(120); // width payload.put_u16(40); // height if let Some(TelnetEvents::DataSend(buf)) = parser.subnegotiation(NAWS, payload.freeze()) { /* write buf to socket */ } - TTYPE (terminal type) identify
let mut parser = Parser::new(); // IAC SB TTYPE IS "xterm-256color" IAC SE let mut body = Vec::new(); body.extend([IAC, IS]); body.extend(b"xterm-256color"); if let Some(TelnetEvents::DataSend(buf)) = parser.subnegotiation(TTYPE, body) { /* write buf to socket */ } - GMCP send (JSON text)
let mut parser = Parser::new(); let json = r#"{\"Core.Supports.Add\":[\"Room 1\"]}"#; if let Some(TelnetEvents::DataSend(buf)) = parser.subnegotiation_text(GMCP, json) { /* write buf to socket */ } - Escape/unescape IAC when handling raw buffers
let escaped = Parser::escape_iac(vec![IAC, 1, 2]); let roundtrip = Parser::unescape_iac(escaped); assert_eq!(&roundtrip[..], [IAC, 1, 2]);
Feature flags
std(default): Enables standard library usage. Disable forno_std.arbitrary: Implementsarbitrary::Arbitraryfor fuzzing/dev.
FAQ
- “Why does
send_textreturn an event?” To unify I/O: everything that must go to the socket is surfaced asTelnetEvents::DataSend(Bytes). - “Do I need to escape IAC myself?” No when using
send_textandsubnegotiation[_text]. Yes only if you craft raw buffers yourself. - “Where is TCP handled?” Out of scope; this crate is protocol parsing only.
Re-exports§
pub use bytes;
Modules§
- compatibility
- Negotiation support and state table.
- events
- Event types emitted by the Telnet parser.
- telnet
- Telnet command and option constants.
Macros§
- vbytes
Deprecated - Macro for calling
Bytes::copy_from_slice()
Structs§
- Parser
- Stateful, event‑driven Telnet parser.