//! 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) { 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) -> 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))); } }