weavatrix-refactor-plan 0.1.0

Evidence metadata, validation profiles, and canonical fingerprints for Weavatrix refactor plans
Documentation
//! Streaming RFC 8785 (JCS) serialization without per-node buffering overhead.
//!
//! Byte-for-byte compatible with the previous `serde_jcs` encoding: object
//! members sort by UTF-16 code units, later duplicate members win, numbers use
//! ECMAScript `Number::toString` formatting, and strings use the minimal JSON
//! escape set with lowercase `\u00xx` control escapes.

mod compound;
mod encoders;
mod key;
#[cfg(test)]
mod tests;
mod text;

use serde::{Serialize, ser};
use std::{cmp::Ordering, fmt, io, str};
use text::{U64_TEXT_CAPACITY, format_u64, write_escaped};

/// A canonical-JSON encoding failure.
#[derive(Debug)]
pub(crate) enum CanonicalError {
    /// The underlying writer failed.
    Io(io::Error),
    /// JCS cannot represent NaN or infinite numbers.
    NonFiniteNumber,
    /// JCS object member names must be strings.
    InvalidKey,
    /// A `Serialize` implementation reported a custom failure.
    Message(Box<str>),
}

impl fmt::Display for CanonicalError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(error) => write!(formatter, "{error}"),
            Self::NonFiniteNumber => formatter.write_str("invalid float value"),
            Self::InvalidKey => formatter.write_str("invalid UTF-8 key"),
            Self::Message(message) => formatter.write_str(message),
        }
    }
}

impl std::error::Error for CanonicalError {}

impl ser::Error for CanonicalError {
    fn custom<T: fmt::Display>(message: T) -> Self {
        Self::Message(message.to_string().into_boxed_str())
    }
}

/// Compares two strings by UTF-16 code units, as RFC 8785 requires.
///
/// The common all-ASCII divergence is resolved with a byte comparison; the
/// UTF-16 re-encoding loop only runs when the first differing byte is
/// non-ASCII in either string.
pub(crate) fn compare_utf16_order(left: &str, right: &str) -> Ordering {
    compare_utf16_bytes(left.as_bytes(), right.as_bytes())
}

fn compare_utf16_bytes(left: &[u8], right: &[u8]) -> Ordering {
    let shared = left.len().min(right.len());
    let mut index = 0;
    while index < shared && left[index] == right[index] {
        index += 1;
    }
    if index == shared {
        // One string is a byte prefix of the other; a valid UTF-8 prefix ends
        // on a character boundary, so the longer string is also greater in
        // UTF-16 code-unit order.
        return left.len().cmp(&right.len());
    }
    let (left_byte, right_byte) = (left[index], right[index]);
    if left_byte < 0x80 && right_byte < 0x80 {
        // Identical prefixes mean both strings start a fresh character here;
        // two ASCII characters compare identically in UTF-8 and UTF-16.
        return left_byte.cmp(&right_byte);
    }
    let left = str::from_utf8(left).expect("canonical keys are valid UTF-8");
    let right = str::from_utf8(right).expect("canonical keys are valid UTF-8");
    left.encode_utf16().cmp(right.encode_utf16())
}

/// Every f64 whose magnitude is at most 2^53 and whose fraction is zero
/// prints as a plain integer under ECMAScript `Number::toString`.
const MAX_INTEGRAL_FAST_PATH: f64 = 9_007_199_254_740_992.0;

#[derive(Clone, Copy)]
struct Frame {
    members_start: usize,
    buf_start: usize,
}

#[derive(Clone, Copy)]
struct Member {
    key_start: usize,
    key_end: usize,
    value_end: usize,
}

/// A reusable canonical serializer over one output writer.
pub(crate) struct CanonicalSerializer<W> {
    out: W,
    /// Arena holding raw member keys and rendered member values while an
    /// object is open. Truncated back as each object completes.
    buf: Vec<u8>,
    frames: Vec<Frame>,
    members: Vec<Member>,
    scratch: Vec<u8>,
    sorted: Vec<Member>,
}

impl<W: io::Write> CanonicalSerializer<W> {
    pub(crate) const fn new(out: W) -> Self {
        Self {
            out,
            buf: Vec::new(),
            frames: Vec::new(),
            members: Vec::new(),
            scratch: Vec::new(),
            sorted: Vec::new(),
        }
    }

    /// Serializes one canonical JSON value to the writer.
    pub(crate) fn write_value<T>(&mut self, value: &T) -> Result<(), CanonicalError>
    where
        T: ?Sized + Serialize,
    {
        value.serialize(&mut *self)
    }

    /// Writes one escaped JSON string token (used for pre-sorted keys).
    pub(crate) fn write_string(&mut self, value: &str) -> Result<(), CanonicalError> {
        self.emit_string(value)
    }

    /// Writes raw structural punctuation such as `{`, `,`, `:`, or `}`.
    pub(crate) fn write_punctuation(&mut self, bytes: &[u8]) -> Result<(), CanonicalError> {
        self.emit(bytes)
    }

    fn emit(&mut self, bytes: &[u8]) -> Result<(), CanonicalError> {
        if self.frames.is_empty() {
            self.out.write_all(bytes).map_err(CanonicalError::Io)
        } else {
            self.buf.extend_from_slice(bytes);
            Ok(())
        }
    }

    fn emit_string(&mut self, value: &str) -> Result<(), CanonicalError> {
        if self.frames.is_empty() {
            write_escaped(&mut self.out, value).map_err(CanonicalError::Io)
        } else {
            write_escaped(&mut self.buf, value).map_err(CanonicalError::Io)
        }
    }

    fn emit_u64(&mut self, value: u64) -> Result<(), CanonicalError> {
        let mut buffer = [0_u8; U64_TEXT_CAPACITY];
        let text = format_u64(value, &mut buffer);
        if self.frames.is_empty() {
            self.out.write_all(text).map_err(CanonicalError::Io)
        } else {
            self.buf.extend_from_slice(text);
            Ok(())
        }
    }

    fn emit_i64(&mut self, value: i64) -> Result<(), CanonicalError> {
        if value < 0 {
            self.emit(b"-")?;
        }
        self.emit_u64(value.unsigned_abs())
    }

    fn emit_f64(&mut self, value: f64) -> Result<(), CanonicalError> {
        if !value.is_finite() {
            return Err(CanonicalError::NonFiniteNumber);
        }
        if value == 0.0 {
            // ECMAScript prints both zero signs as plain `0`.
            return self.emit(b"0");
        }
        if value.fract() == 0.0 && value.abs() <= MAX_INTEGRAL_FAST_PATH {
            // Exactly integral and within 2^53: plain decimal digits, matching
            // ECMAScript output while skipping the shortest-float search.
            #[expect(
                clippy::cast_possible_truncation,
                reason = "the magnitude check keeps the value within i64"
            )]
            return self.emit_i64(value as i64);
        }
        let mut buffer = ryu_js::Buffer::new();
        let text = buffer.format_finite(value);
        self.emit(text.as_bytes())
    }

    fn begin_object(&mut self) {
        self.frames.push(Frame {
            members_start: self.members.len(),
            buf_start: self.buf.len(),
        });
    }

    fn end_object(&mut self) -> Result<(), CanonicalError> {
        let frame = self.frames.pop().expect("end_object without begin_object");
        self.sorted.clear();
        self.sorted
            .extend(self.members.drain(frame.members_start..));
        let buf = &self.buf;
        // Stable sort so equal keys keep insertion order; the previous
        // encoder let later duplicates overwrite earlier ones.
        self.sorted.sort_by(|left, right| {
            compare_utf16_bytes(
                &buf[left.key_start..left.key_end],
                &buf[right.key_start..right.key_end],
            )
        });
        render_object(&self.buf, &self.sorted, &mut self.scratch)?;
        self.buf.truncate(frame.buf_start);
        if self.frames.is_empty() {
            self.out
                .write_all(&self.scratch)
                .map_err(CanonicalError::Io)
        } else {
            self.buf.extend_from_slice(&self.scratch);
            Ok(())
        }
    }

    /// Records the raw text of the member key that starts at `buf.len()`.
    fn open_static_key(&mut self, key: &str) -> (usize, usize) {
        let key_start = self.buf.len();
        self.buf.extend_from_slice(key.as_bytes());
        (key_start, self.buf.len())
    }

    fn push_member(&mut self, key_start: usize, key_end: usize) {
        self.members.push(Member {
            key_start,
            key_end,
            value_end: self.buf.len(),
        });
    }
}

/// Renders sorted members as `{"k":v,...}` into `scratch`.
fn render_object(
    buf: &[u8],
    sorted: &[Member],
    scratch: &mut Vec<u8>,
) -> Result<(), CanonicalError> {
    scratch.clear();
    scratch.push(b'{');
    for (index, member) in sorted.iter().enumerate() {
        let duplicated_later = sorted.get(index + 1).is_some_and(|next| {
            buf[next.key_start..next.key_end] == buf[member.key_start..member.key_end]
        });
        if duplicated_later {
            continue;
        }
        if scratch.len() > 1 {
            scratch.push(b',');
        }
        let key =
            str::from_utf8(&buf[member.key_start..member.key_end]).expect("keys are UTF-8 text");
        write_escaped(scratch, key).map_err(CanonicalError::Io)?;
        scratch.push(b':');
        scratch.extend_from_slice(&buf[member.key_end..member.value_end]);
    }
    scratch.push(b'}');
    Ok(())
}