serpent-serializer 0.1.0

Serialize Lua values to round-trippable Lua source with cycle and shared-reference handling
Documentation
//! Lua string-literal escaping.
//!
//! Reproduces Lua 5.1 `string.format("%q", s)` plus serpent's two post-passes.
//! The result is a double-quoted Lua string literal that `load` reads back to
//! the exact input bytes, including NUL and other control characters. This must
//! be byte-exact for round trips, and it runs over binary data such as dumped
//! bytecode.

/// Quote a byte string as a Lua string literal.
///
/// Per-byte rules from Lua 5.1 `%q`:
/// - `"` becomes `\"`
/// - `\` becomes `\\`
/// - newline (10) becomes backslash then a real newline
/// - carriage return (13) becomes `\r`
/// - NUL (0) becomes `\0`, or `\000` when the next byte is a digit
/// - any byte at or above 128 becomes a three-digit decimal escape `\ddd`
/// - any other byte is copied verbatim
///
/// Then serpent's two passes run over the result: every raw newline byte (10)
/// becomes the letter `n`, and every EOF byte (26) becomes the four characters
/// `\026`. Net effect: newline turns into the two-character `\n`, and byte 26
/// turns into `\026`.
///
/// The `\000` and `\ddd` forms keep the output binary-safe. A bare `\0` followed
/// by an ASCII digit would parse as a single decimal escape and lose bytes, so
/// NUL before a digit uses the padded form. High bytes escape to decimal so the
/// output stays valid UTF-8 and still round-trips through [`crate::load`], which
/// reads up to three decimal digits per escape. Bytecode dumps carry high bytes,
/// so this path runs on ordinary input.
#[must_use]
pub fn quote(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() + 2);
    out.push('"');
    for (i, &c) in bytes.iter().enumerate() {
        match c {
            b'"' => out.push_str("\\\""),
            b'\\' => out.push_str("\\\\"),
            // %q emits backslash then a real newline; serpent's first gsub then
            // rewrites the newline byte to `n`, giving `\n`.
            10 => out.push_str("\\n"),
            13 => out.push_str("\\r"),
            0 => {
                let next_is_digit = bytes.get(i + 1).is_some_and(u8::is_ascii_digit);
                out.push_str(if next_is_digit { "\\000" } else { "\\0" });
            }
            // serpent's second gsub maps byte 26 to the four chars `\026`.
            26 => out.push_str("\\026"),
            128..=255 => out.push_str(&format!("\\{c:03}")),
            _ => out.push(c as char),
        }
    }
    out.push('"');
    out
}