Crate libmudtelnet_rs

Crate libmudtelnet_rs 

Source
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-rs where practical.
  • Optional no_std support (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 returns Vec<TelnetEvents>.
  • events: Defines TelnetEvents, 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::_dont to drive option state changes for options you support (see compatibility).
  • Use Parser::subnegotiation to send payloads; the parser wraps bytes with IAC SB <option> ... IAC SE and escapes IAC inside the body.
  • GMCP/MSDP interop: Once the server offers WILL GMCP|MSDP and the client responds with DO, both sides may send subnegotiations. The parser treats GMCP and MSDP as bidirectional after WILL/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 sent WILL. This matches common MUD server behavior and avoids handshake deadlocks.

MCCP decompression boundary

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_std environments: libmudtelnet-rs = { version = "*", default-features = false }
  • In no_std, APIs behave the same; internal buffers use bytes.

Tips

  • Always write out TelnetEvents::DataSend exactly as provided.
  • Treat TelnetEvents::DataReceive as application data; it has already had any doubled IACs unescaped.
  • Negotiate only options you actually support (via compatibility).
  • Prefer working with &[u8]/Bytes payloads; avoid String unless 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 for no_std.
  • arbitrary: Implements arbitrary::Arbitrary for fuzzing/dev.

FAQ

  • “Why does send_text return an event?” To unify I/O: everything that must go to the socket is surfaced as TelnetEvents::DataSend(Bytes).
  • “Do I need to escape IAC myself?” No when using send_text and subnegotiation[_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§

vbytesDeprecated
Macro for calling Bytes::copy_from_slice()

Structs§

Parser
Stateful, event‑driven Telnet parser.