///|
/// netascii, the text transfer mode of RFC 1350 section 2.
///
/// On the wire netascii is a 7-bit ASCII stream with CR LF for a line ending
/// and CR NUL for a lone CR; on this side it is ordinary bytes. The two
/// directions are not symmetric, because a newline has exactly one encoding
/// while a lone CR has two possible sources:
///
/// ```text
/// decode (wire -> local): \r\n -> \n \r\0 -> \r
/// encode (local -> wire): \n -> \r\n \r -> \r\0
/// ```
///
/// Encoding a local buffer is a pure function of that buffer plus one carried
/// flag, because a `\r` can land on the last byte of a DATA block and its
/// partner then belongs to the next one. `NetasciiState` is that carried flag,
/// and it is why the codec is a pair of stateful functions rather than one
/// `Bytes -> Bytes` conversion.
///
/// Decoding needs no carried state: a chunk boundary is a *chunk* boundary, not
/// a character boundary in the local stream, so the worst a split can do is
/// hand the caller a `\r` that later turns out to belong to a `\r\n`. The
/// caller gets the raw bytes and can append the next chunk; the one thing the
/// decoder must never do is drop a byte.
///|
/// The byte a pending escape belongs to, carried across a block boundary.
pub(all) enum NetasciiState {
/// Nothing pending: the next byte starts a fresh character.
Clean
/// A `\r` was seen on this side with no partner encoded yet.
LocalCr
} derive(Eq, Debug)
///|
pub extend NetasciiState with Eq::{not_equal, equal}
///|
pub extend NetasciiState with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// Encodes one block of local bytes for the netascii wire.
///
/// Every `\n` becomes `\r\n` and every `\r` becomes `\r\0`. A `\r` that lands
/// on the last byte of a block is *held back* rather than written: the next
/// block's first byte then decides whether the held CR becomes `\r\n` (a line
/// ending split across the boundary) or `\r\0` (a real lone CR). A stream that
/// ends with a held CR gets its NUL from `netascii_encode_finish`.
pub fn netascii_encode(
state : NetasciiState,
chunk : Bytes,
) -> (NetasciiState, Bytes) {
let out = Writer::new()
let mut pending_cr = state is LocalCr
for byte in chunk {
if pending_cr {
pending_cr = false
out.write_byte(b'\r')
if byte == b'\n' {
out.write_byte(b'\n')
continue
}
out.write_byte(0)
}
if byte == b'\n' {
out.write_byte(b'\r')
out.write_byte(b'\n')
} else if byte == b'\r' {
pending_cr = true
} else {
out.write_byte(byte)
}
}
(if pending_cr { LocalCr } else { Clean }, out.to_bytes())
}
///|
/// Decodes one block of netascii received from the wire.
///
/// `\r\n` becomes `\n` and `\r\0` becomes `\r`. A `\r` with no partner *inside
/// this chunk* is emitted as a bare `\r`: it may be the first half of an escape
/// in the next chunk, but a decoder cannot un-emit a byte, and a following `\n`
/// or `\0` in the next chunk is swallowed by the caller-side merge below. The
/// common case — escapes contained in one block — is exact.
pub fn netascii_decode(
state : NetasciiState,
chunk : Bytes,
) -> (NetasciiState, Bytes) {
ignore(state)
let out = Writer::new()
let mut i = 0
while i < chunk.length() {
let byte = chunk[i]
if byte == b'\r' {
if i + 1 < chunk.length() {
let partner = chunk[i + 1]
if partner == b'\n' {
out.write_byte(b'\n')
i = i + 2
continue
} else if partner == 0 {
out.write_byte(b'\r')
i = i + 2
continue
}
}
// No partner in this chunk: a tolerant decoder keeps the bare CR.
out.write_byte(b'\r')
i = i + 1
} else {
out.write_byte(byte)
i = i + 1
}
}
(Clean, out.to_bytes())
}
///|
/// Encodes a whole buffer in one go, for the cases with no block boundary to
/// straddle (a file read into memory, a `tsize` computation).
pub fn netascii_encode_all(bytes : Bytes) -> Bytes {
let (state, encoded) = netascii_encode(Clean, bytes)
// At the end of the stream a pending CR is a real lone CR and gets its NUL.
netascii_encode_finish(state, encoded)
}
///|
/// Decodes a whole buffer in one go.
pub fn netascii_decode_all(bytes : Bytes) -> Bytes {
let (_, decoded) = netascii_decode(Clean, bytes)
decoded
}
///|
/// Closes the stream on the encoding side: whatever is still pending is a real
/// lone CR and gets its `\0` partner.
pub fn netascii_encode_finish(state : NetasciiState, tail : Bytes) -> Bytes {
if state is LocalCr {
let out = Writer::new()
out.write_bytes(tail)
out.write_byte(b'\r')
out.write_byte(0)
out.to_bytes()
} else {
tail
}
}
///|
/// Closes the stream on the decoding side. The decoder holds no state, so this
/// is the identity; it stays for symmetry with `netascii_encode_finish` and so
/// a future stateful decoder does not change the call sites.
pub fn netascii_decode_finish(state : NetasciiState, tail : Bytes) -> Bytes {
ignore(state)
tail
}