150 lines
4.2 KiB
Rust
150 lines
4.2 KiB
Rust
//! Netascii translation helpers.
|
|
//!
|
|
//! RFC 1350, Section 1 defines *netascii* as ASCII with the newline / carriage-return
|
|
//! conventions from the Telnet specification (RFC 764). In practice, this means:
|
|
//! - Newline is transmitted as CR LF.
|
|
//! - A literal carriage return is transmitted as CR NUL.
|
|
//!
|
|
//! The translation must work as a streaming transform because CR and LF can be
|
|
//! split across TFTP DATA block boundaries (RFC 1350, Section 2: 512-byte blocks).
|
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
/// Streaming encoder from local bytes into netascii bytes.
|
|
///
|
|
/// This implementation treats `\n` as the local newline and maps:
|
|
/// - `\n` -> `\r\n`
|
|
/// - `\r` -> `\r\0`
|
|
///
|
|
/// RFC reference: RFC 1350, Section 1 (netascii definition).
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct NetasciiEncoder;
|
|
|
|
impl NetasciiEncoder {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
|
|
pub fn encode_chunk(&mut self, input: &[u8], output: &mut Vec<u8>) {
|
|
for &b in input {
|
|
match b {
|
|
b'\n' => output.extend_from_slice(b"\r\n"),
|
|
b'\r' => output.extend_from_slice(b"\r\0"),
|
|
_ => output.push(b),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Streaming decoder from netascii bytes into local bytes.
|
|
///
|
|
/// This decoder maintains state for a pending CR at a chunk boundary.
|
|
/// It maps:
|
|
/// - `\r\n` -> `\n`
|
|
/// - `\r\0` -> `\r`
|
|
///
|
|
/// RFC reference: RFC 1350, Section 1 (netascii definition).
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct NetasciiDecoder {
|
|
pending_cr: bool,
|
|
}
|
|
|
|
impl NetasciiDecoder {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Decodes a chunk of netascii bytes, appending the translated bytes to `output`.
|
|
///
|
|
/// # Errors
|
|
/// Returns an error if an invalid CR sequence is encountered (anything other than
|
|
/// CR LF or CR NUL), per the netascii definition referenced by RFC 1350, Section 1.
|
|
pub fn decode_chunk(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<(), DecodeError> {
|
|
for &b in input {
|
|
if self.pending_cr {
|
|
self.pending_cr = false;
|
|
match b {
|
|
b'\n' => output.push(b'\n'),
|
|
0 => output.push(b'\r'),
|
|
_ => return Err(DecodeError::InvalidCrSequence(b)),
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if b == b'\r' {
|
|
self.pending_cr = true;
|
|
} else {
|
|
output.push(b);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Flushes any pending state at end-of-stream.
|
|
///
|
|
/// A dangling CR is invalid netascii.
|
|
///
|
|
/// # Errors
|
|
/// Returns an error if the stream ends with a dangling CR byte.
|
|
pub fn finish(self) -> Result<(), DecodeError> {
|
|
if self.pending_cr {
|
|
Err(DecodeError::DanglingCr)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum DecodeError {
|
|
DanglingCr,
|
|
InvalidCrSequence(u8),
|
|
}
|
|
|
|
impl core::fmt::Display for DecodeError {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
match self {
|
|
Self::DanglingCr => write!(f, "dangling CR at end of stream"),
|
|
Self::InvalidCrSequence(b) => write!(f, "invalid netascii CR sequence: 0x{b:02x}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DecodeError {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
#[test]
|
|
fn encode_newlines_and_cr() {
|
|
let mut enc = NetasciiEncoder::new();
|
|
let mut out = Vec::new();
|
|
enc.encode_chunk(b"a\nb\rc", &mut out);
|
|
assert_eq!(out, b"a\r\nb\r\0c");
|
|
}
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
#[test]
|
|
fn decode_across_chunk_boundary() {
|
|
let mut dec = NetasciiDecoder::new();
|
|
let mut out = Vec::new();
|
|
dec.decode_chunk(b"a\r", &mut out).unwrap();
|
|
dec.decode_chunk(b"\nb", &mut out).unwrap();
|
|
dec.finish().unwrap();
|
|
assert_eq!(out, b"a\nb");
|
|
}
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
#[test]
|
|
fn decode_rejects_dangling_cr() {
|
|
let mut dec = NetasciiDecoder::new();
|
|
let mut out = Vec::new();
|
|
dec.decode_chunk(b"a\r", &mut out).unwrap();
|
|
assert!(matches!(dec.finish(), Err(DecodeError::DanglingCr)));
|
|
}
|
|
}
|