Crates.io | libmudtelnet-rs |
lib.rs | libmudtelnet-rs |
version | 2.0.10 |
created_at | 2025-09-13 16:43:49.370897+00 |
updated_at | 2025-09-15 19:05:19.880216+00 |
description | Robust, event-driven Telnet (RFC 854) parser for MUD clients with GMCP, MSDP, MCCP support and zero-allocation hot paths |
homepage | https://github.com/laudney/libmudtelnet-rs |
repository | https://github.com/laudney/libmudtelnet-rs |
max_upload_size | |
id | 1837892 |
size | 104,330 |
Robust, event‑driven Telnet (RFC 854) parsing for MUD clients in Rust — with minimal allocations, strong real‑world compatibility, and clean, typed events.
libmudtelnet-rs is a fork of libmudtelnet by the Blightmud team, which powers the Blightmud MUD client. The original libmudtelnet was itself forked from libtelnet-rs by envis10n, which is inspired by the robust C library libtelnet by Sean Middleditch. We are deeply grateful for their foundational work.
This project continues the specialization for the unique and demanding world of MUDs. Our focus is on adding comprehensive MUD protocol support, fixing critical edge-case bugs encountered in the wild, and relentlessly pursuing the correctness and performance that modern MUD clients deserve.
You're building a MUD client. You need to handle Telnet negotiation, GMCP data streams, MCCP compression, and MSDP variables. Your parser must be robust against malformed sequences from decades-old servers while maintaining zero-allocation performance in hot paths.
Standard Telnet parsers expect compliance; MUD servers offer chaos. A stray SE
byte without IAC
, truncated subnegotiations, or multiple escaped IAC
sequences can crash naive implementations. libmudtelnet handles these realities gracefully while delivering clean, structured events for your application logic.
Use libmudtelnet‑rs so you can focus on building triggers, mappers, and UIs instead of debugging protocol edge cases.
Add to your Cargo.toml:
[dependencies]
libmudtelnet-rs = "2"
GMCP (201) - Generic MUD Communication Protocol
MSDP (69) - MUD Server Data Protocol
MCCP2/MCCP3 (86/87) - MUD Client Compression Protocol
DecompressImmediate
events for boundary handlingMXP (91) - MUD eXtension Protocol
MSSP (70) - MUD Server Status Protocol
ZMP (93) - Zenith MUD Protocol
ATCP (200) - Achaea Telnet Client Protocol
use libmudtelnet_rs::{Parser, events::TelnetEvents};
use libmudtelnet_rs::telnet::op_option;
let mut parser = Parser::new();
// Feed bytes from your socket
let events = parser.receive(&socket_bytes);
for ev in events {
match ev {
TelnetEvents::DataReceive(data) => {
// bytes::Bytes (zero‑copy view)
app.display_text(&data);
}
TelnetEvents::Subnegotiation(sub) => match sub.option {
op_option::GMCP => app.handle_gmcp(&sub.buffer),
op_option::MSDP => app.handle_msdp(&sub.buffer),
_ => {}
},
TelnetEvents::Negotiation(neg) => app.log_neg(neg.command, neg.option),
TelnetEvents::DecompressImmediate(data) => {
// MCCP2/3: decompress then feed back into the parser
let decompressed = app.decompress(&data)?;
for ev in parser.receive(&decompressed) { app.handle_event(ev); }
}
TelnetEvents::DataSend(buf) => socket.write_all(&buf)?,
_ => {}
}
}
// Send a line (IACs escaped for you, "\r\n" appended)
let to_send = parser.send_text("say Hello, world!");
if let TelnetEvents::DataSend(buf) = to_send { socket.write_all(&buf)?; }
If your app uses Tokio, integrate the parser in your read loop and write any DataSend
bytes back to the socket as-is. Example skeleton:
use libmudtelnet_rs::{Parser, events::TelnetEvents};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut stream = TcpStream::connect("mud.example.org:4000").await?;
let (mut r, mut w) = stream.split();
let mut parser = Parser::new();
let mut buf = vec![0u8; 4096];
loop {
let n = r.read(&mut buf).await?;
if n == 0 { break; }
for ev in parser.receive(&buf[..n]) {
match ev {
TelnetEvents::DataReceive(data) => print!("{}", String::from_utf8_lossy(&data)),
TelnetEvents::DataSend(data) => w.write_all(&data).await?,
TelnetEvents::DecompressImmediate(block) => {
// Decompress then feed back (identity shown)
for ev2 in parser.receive(&block) {
if let TelnetEvents::DataReceive(d) = ev2 {
print!("{}", String::from_utf8_lossy(&d));
}
}
}
_ => {}
}
}
}
Ok(())
}
See a full template in examples/tokio_client.rs
.
use libmudtelnet_rs::{Parser, events::TelnetEvents};
use libmudtelnet_rs::telnet::op_command::{WILL, DO};
use libmudtelnet_rs::telnet::op_option::{GMCP, NAWS};
let mut parser = Parser::new();
// Announce that we WILL use GMCP locally
let will_gmcp = parser.negotiate(WILL, GMCP);
if let TelnetEvents::DataSend(buf) = will_gmcp { socket.write_all(&buf)?; }
// Ask the server to DO NAWS (report window size)
let do_naws = parser.negotiate(DO, NAWS);
if let TelnetEvents::DataSend(buf) = do_naws { socket.write_all(&buf)?; }
Many MUD servers expect GMCP/MSDP to be bidirectional once the server sends
IAC WILL GMCP|MSDP
and the client responds IAC DO
. libmudtelnet‑rs follows
this behavior:
WILL/DO
handshake even
if the client never sent WILL
.DO
to a
client WILL
.Other options retain standard Telnet semantics.
use libmudtelnet_rs::telnet::msdp;
fn parse_msdp_data(data: &[u8]) {
let mut i = 0;
while i < data.len() {
if data[i] == msdp::VAR {
// Extract variable name
} else if data[i] == msdp::VAL {
// Extract variable value
} else if data[i] == msdp::TABLE_OPEN {
// Begin table parsing
}
// ... handle ARRAY_OPEN, TABLE_CLOSE, ARRAY_CLOSE
i += 1;
}
}
libmudtelnet-rs transforms the Telnet byte stream into a clean sequence of structured events:
This separation keeps your network I/O simple and your protocol handling clean.
libmudtelnet-rs has been hardened against real-world protocol violations:
SE
byte without preceding IAC
during subnegotiation is handled gracefullyIAC SB IAC SE
won't cause panicsIAC IAC IAC IAC
) are correctly unescapedcargo-fuzz
bombards the parser with malformed inputsbytes::BytesMut
to avoid copies in parsing loopsbytes::Bytes
sliceslibmudtelnet-rs maintains API compatibility with libtelnet-rs where practical. The event semantics are stable - breaking changes follow semver and include migration guides.
We welcome contributions from the MUD and Rust communities! Whether you've found a bug, want to add protocol support, or improve documentation, your help is appreciated.
# Build and test
cargo build
cargo test
cargo test --no-default-features # Test no_std compatibility
# Code quality
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
# Fuzz testing (requires cargo-fuzz)
cargo install cargo-fuzz
cd fuzz && cargo fuzz run parser_receive
# Benchmarks
cd compat && cargo bench
Report Bugs: Found a MUD server that sends data the parser doesn't handle? Please open an issue with details
Add Protocol Support: Want support for a new MUD protocol? Let's discuss implementation approach
Improve Tests: Additional fuzz targets, edge cases, or property tests are always valuable
Documentation: Code examples, protocol explanations, or usage guides
Good first issues: check the good first issue label
Examples: see examples/basic.rs
, examples/tokio_client.rs
, and docs/API_EXAMPLES.md
This project follows the Rust Code of Conduct. We're committed to providing a welcoming environment for all contributors.
libmudtelnet-rs has been tested for API compatibility with libtelnet-rs. While much of the implementation has been rewritten for improved correctness and performance, the public API remains familiar to ease migration.
See CHANGELOG.md for detailed information about fixes and enhancements.
Many thanks to:
libmudtelnet-rs
is forked from.libmudtelnet
was originally forked from.libtelnet-rs
.