holger-plugin-abi 0.1.0

holger package-handler plugin ABI: the wire contract a handler speaks whether it is linked in or loaded from a .wasm
Documentation
//! The byte codec both surfaces share. **One writer, one reader** — the host
//! links these functions and so does every guest, so a field cannot be encoded
//! one way and decoded another (LAW 5, by construction rather than by a guard
//! watching two copies agree).
//!
//! # Why not JSON
//!
//! znippy's `wasm_loader::parse_json_to_row` splits on `,` and `:` *before*
//! stripping quotes, so a value containing either separator silently corrupts
//! the row, and it has no null representation at all — an absent column and an
//! empty one are the same bytes on the wire. Both are properties of the ad-hoc
//! format, not bugs that can be patched out of the parser.
//!
//! This codec is length-prefixed, so a value is copied by length and its
//! contents are never scanned for structure: `,`, `:`, `"`, a NUL and an
//! arbitrary UTF-8 sequence all survive a round trip. `Option` carries an
//! explicit 1-byte tag, so `None` and `Some("")` are distinct on the wire.
//! `codec_tests.rs` asserts exactly those two properties.
//!
//! # Layout
//!
//! | type | bytes |
//! |---|---|
//! | `u32` / `u16` / `u64` / `i64` | little-endian, fixed width |
//! | `bool` | 1 byte, `0` or `1` |
//! | bytes / `String` | `u32` length, then that many bytes |
//! | `Option<T>` | `0u8`, or `1u8` then `T` |
//! | `Vec<T>` | `u32` count, then that many `T` |
//! | `Result<T, String>` | `0u8` then `T`, or `1u8` then the message |

use std::string::ToString;

/// Why a byte string could not be decoded. Every variant names the field being
/// read, because a decode failure on the host side is otherwise indistinguishable
/// from a plugin that returned nothing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
    /// Ran off the end of the buffer while reading `field`.
    Truncated { field: &'static str, need: usize, have: usize },
    /// A length prefix exceeded what the remaining buffer can hold.
    LengthOverflow { field: &'static str, len: u64, remaining: usize },
    /// A tag byte was not one of the values the type defines.
    BadTag { field: &'static str, tag: u8 },
    /// A length-prefixed string was not valid UTF-8.
    NotUtf8 { field: &'static str },
    /// Bytes were left over after the value was fully decoded — the two sides
    /// disagree about the shape, which is never benign.
    TrailingBytes { field: &'static str, left: usize },
}

impl core::fmt::Display for DecodeError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            DecodeError::Truncated { field, need, have } => write!(
                f,
                "ABI decode: truncated reading `{field}` — need {need} bytes, {have} left"
            ),
            DecodeError::LengthOverflow { field, len, remaining } => write!(
                f,
                "ABI decode: `{field}` declares {len} bytes but only {remaining} remain"
            ),
            DecodeError::BadTag { field, tag } => {
                write!(f, "ABI decode: `{field}` carries invalid tag byte {tag}")
            }
            DecodeError::NotUtf8 { field } => write!(f, "ABI decode: `{field}` is not UTF-8"),
            DecodeError::TrailingBytes { field, left } => write!(
                f,
                "ABI decode: {left} trailing byte(s) after `{field}` — encoder and decoder \
                 disagree about the shape"
            ),
        }
    }
}

/// Append-only byte writer.
#[derive(Default)]
pub struct Writer {
    buf: Vec<u8>,
}

impl Writer {
    pub fn new() -> Self {
        Self { buf: Vec::new() }
    }

    pub fn finish(self) -> Vec<u8> {
        self.buf
    }

    pub fn u8(&mut self, v: u8) -> &mut Self {
        self.buf.push(v);
        self
    }

    pub fn u16(&mut self, v: u16) -> &mut Self {
        self.buf.extend_from_slice(&v.to_le_bytes());
        self
    }

    pub fn u32(&mut self, v: u32) -> &mut Self {
        self.buf.extend_from_slice(&v.to_le_bytes());
        self
    }

    pub fn u64(&mut self, v: u64) -> &mut Self {
        self.buf.extend_from_slice(&v.to_le_bytes());
        self
    }

    pub fn i64(&mut self, v: i64) -> &mut Self {
        self.buf.extend_from_slice(&v.to_le_bytes());
        self
    }

    pub fn bool(&mut self, v: bool) -> &mut Self {
        self.buf.push(u8::from(v));
        self
    }

    /// Length-prefixed bytes. The payload is copied verbatim — no escaping, no
    /// separator scan, so any byte sequence survives.
    pub fn bytes(&mut self, v: &[u8]) -> &mut Self {
        self.u32(v.len() as u32);
        self.buf.extend_from_slice(v);
        self
    }

    pub fn str(&mut self, v: &str) -> &mut Self {
        self.bytes(v.as_bytes())
    }

    /// `None` and `Some("")` differ by the tag byte, not by length.
    pub fn opt_str(&mut self, v: Option<&str>) -> &mut Self {
        match v {
            None => self.u8(0),
            Some(s) => self.u8(1).str(s),
        }
    }

    pub fn opt_bytes(&mut self, v: Option<&[u8]>) -> &mut Self {
        match v {
            None => self.u8(0),
            Some(b) => self.u8(1).bytes(b),
        }
    }
}

/// Cursor over a byte string.
pub struct Reader<'a> {
    buf: &'a [u8],
    pos: usize,
}

impl<'a> Reader<'a> {
    pub fn new(buf: &'a [u8]) -> Self {
        Self { buf, pos: 0 }
    }

    pub fn remaining(&self) -> usize {
        self.buf.len() - self.pos
    }

    /// Assert the value consumed the whole buffer. Called at the end of every
    /// top-level decode: a decoder that stops early is reading a *different*
    /// shape than the encoder wrote, and silently ignoring the tail is how an
    /// ABI skew turns into wrong values instead of an error.
    pub fn expect_end(&self, field: &'static str) -> Result<(), DecodeError> {
        if self.remaining() == 0 {
            Ok(())
        } else {
            Err(DecodeError::TrailingBytes { field, left: self.remaining() })
        }
    }

    fn take(&mut self, n: usize, field: &'static str) -> Result<&'a [u8], DecodeError> {
        if self.remaining() < n {
            return Err(DecodeError::Truncated { field, need: n, have: self.remaining() });
        }
        let out = &self.buf[self.pos..self.pos + n];
        self.pos += n;
        Ok(out)
    }

    pub fn u8(&mut self, field: &'static str) -> Result<u8, DecodeError> {
        Ok(self.take(1, field)?[0])
    }

    pub fn u16(&mut self, field: &'static str) -> Result<u16, DecodeError> {
        let b = self.take(2, field)?;
        Ok(u16::from_le_bytes([b[0], b[1]]))
    }

    pub fn u32(&mut self, field: &'static str) -> Result<u32, DecodeError> {
        let b = self.take(4, field)?;
        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
    }

    pub fn u64(&mut self, field: &'static str) -> Result<u64, DecodeError> {
        let b = self.take(8, field)?;
        let mut a = [0u8; 8];
        a.copy_from_slice(b);
        Ok(u64::from_le_bytes(a))
    }

    pub fn i64(&mut self, field: &'static str) -> Result<i64, DecodeError> {
        Ok(self.u64(field)? as i64)
    }

    pub fn bool(&mut self, field: &'static str) -> Result<bool, DecodeError> {
        match self.u8(field)? {
            0 => Ok(false),
            1 => Ok(true),
            tag => Err(DecodeError::BadTag { field, tag }),
        }
    }

    pub fn bytes(&mut self, field: &'static str) -> Result<Vec<u8>, DecodeError> {
        let len = self.u32(field)? as usize;
        if len > self.remaining() {
            return Err(DecodeError::LengthOverflow {
                field,
                len: len as u64,
                remaining: self.remaining(),
            });
        }
        Ok(self.take(len, field)?.to_vec())
    }

    pub fn str(&mut self, field: &'static str) -> Result<String, DecodeError> {
        let b = self.bytes(field)?;
        String::from_utf8(b).map_err(|_| DecodeError::NotUtf8 { field })
    }

    pub fn opt_str(&mut self, field: &'static str) -> Result<Option<String>, DecodeError> {
        match self.u8(field)? {
            0 => Ok(None),
            1 => Ok(Some(self.str(field)?)),
            tag => Err(DecodeError::BadTag { field, tag }),
        }
    }

    pub fn opt_bytes(&mut self, field: &'static str) -> Result<Option<Vec<u8>>, DecodeError> {
        match self.u8(field)? {
            0 => Ok(None),
            1 => Ok(Some(self.bytes(field)?)),
            tag => Err(DecodeError::BadTag { field, tag }),
        }
    }

    /// Read a `u32` count and check it against the bytes actually left, so a
    /// corrupt count cannot make the caller pre-allocate an absurd `Vec`.
    pub fn count(&mut self, field: &'static str) -> Result<usize, DecodeError> {
        let n = self.u32(field)? as usize;
        if n > self.remaining() {
            return Err(DecodeError::LengthOverflow {
                field,
                len: n as u64,
                remaining: self.remaining(),
            });
        }
        Ok(n)
    }
}

/// Encode a `Result<T, String>`: tag `0` = ok, tag `1` = the error message.
pub fn write_result<T>(w: &mut Writer, v: &Result<T, String>, ok: impl FnOnce(&mut Writer, &T)) {
    match v {
        Ok(t) => {
            w.u8(0);
            ok(w, t);
        }
        Err(e) => {
            w.u8(1);
            w.str(e);
        }
    }
}

/// Decode a `Result<T, String>` written by [`write_result`].
pub fn read_result<T>(
    r: &mut Reader<'_>,
    field: &'static str,
    ok: impl FnOnce(&mut Reader<'_>) -> Result<T, DecodeError>,
) -> Result<Result<T, String>, DecodeError> {
    match r.u8(field)? {
        0 => Ok(Ok(ok(r)?)),
        1 => Ok(Err(r.str(field)?)),
        tag => Err(DecodeError::BadTag { field, tag }),
    }
}

impl DecodeError {
    /// Render for a `Result<_, String>` boundary (the ABI carries messages, not types).
    pub fn message(&self) -> String {
        self.to_string()
    }
}