Lines
95.24 %
Functions
39.02 %
Branches
100 %
//! SLYNK wire framing: a 6-hex-digit ASCII payload-length header followed by
//! that many bytes of UTF-8 (one s-expression). See
//! `doc/editor/slynk-protocol-transcript.org`.
use std::io;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
/// Largest payload we will read. SLYNK frames are small control/eval messages;
/// a header claiming more than this is treated as a protocol error rather than
/// allocating an attacker-controlled buffer.
const MAX_FRAME_LEN: usize = 16 * 1024 * 1024;
/// Reads one framed message: 6 hex length digits, then `len` UTF-8 bytes.
/// Returns `Ok(None)` on a clean EOF at a frame boundary (peer closed), `Err`
/// on a malformed header / oversize length / non-UTF-8 / truncated body — the
/// caller closes the connection, never panics.
pub async fn read_frame<R: AsyncRead + Unpin>(reader: &mut R) -> io::Result<Option<String>> {
let mut header = [0u8; 6];
match reader.read_exact(&mut header).await {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let header_str = std::str::from_utf8(&header)
.map_err(|_| protocol_err("frame length header is not ASCII"))?;
let len = usize::from_str_radix(header_str, 16)
.map_err(|_| protocol_err("frame length header is not hex"))?;
if len > MAX_FRAME_LEN {
return Err(protocol_err("frame length exceeds maximum"));
let mut body = vec![0u8; len];
reader.read_exact(&mut body).await?;
let text = String::from_utf8(body).map_err(|_| protocol_err("frame body is not UTF-8"))?;
Ok(Some(text))
/// Writes one framed message: the 6-hex length header then the payload bytes.
/// Errors if the payload exceeds the 6-hex-digit ceiling (a 16 MiB-ish limit
/// the protocol never approaches for control/eval messages).
pub async fn write_frame<W: AsyncWrite + Unpin>(writer: &mut W, payload: &str) -> io::Result<()> {
let len = payload.len();
if len > 0xff_ffff {
return Err(protocol_err("outgoing frame exceeds 6-hex-digit length"));
writer.write_all(format!("{len:06x}").as_bytes()).await?;
writer.write_all(payload.as_bytes()).await?;
writer.flush().await?;
Ok(())
fn protocol_err(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg)
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[tokio::test]
async fn round_trips_a_frame() {
let mut buf = Vec::new();
write_frame(&mut buf, "(:return (:ok nil) 1)")
.await
.unwrap();
// header is the lowercase hex byte length of the payload
assert_eq!(&buf[..6], b"000015");
let mut cur = Cursor::new(buf);
let got = read_frame(&mut cur).await.unwrap();
assert_eq!(got.as_deref(), Some("(:return (:ok nil) 1)"));
async fn clean_eof_yields_none() {
let mut cur = Cursor::new(Vec::new());
assert_eq!(read_frame(&mut cur).await.unwrap(), None);
async fn non_hex_header_is_error_not_panic() {
let mut cur = Cursor::new(b"zzzzzz".to_vec());
assert!(read_frame(&mut cur).await.is_err());
async fn truncated_body_is_error() {
// header claims 10 bytes, only 3 follow
let mut cur = Cursor::new(b"00000aabc".to_vec());
async fn utf8_payload_round_trips() {
let payload = "(:write-string \"caf\u{e9}\")";
write_frame(&mut buf, payload).await.unwrap();
assert_eq!(
read_frame(&mut cur).await.unwrap().as_deref(),
Some(payload)
);