epiphany/crates/epiphany-ops/src/encode.rs

127 lines
4.7 KiB
Rust

//! Canonical-encoding helpers for the composite Chapter 6 types.
//!
//! The determinism crate fixes the canonical byte form of the *primitives*
//! (identifiers, hashes, `CanonicalF64`, `QuantizedCoord`) via
//! [`epiphany_determinism::CanonicalEncode`]; `epiphany-core` does the same for
//! the graph value types. The Chapter 6 types are *composites* of those — with
//! variable-length parts (vectors, the one text label) — so they need a
//! length-discipline that stays unambiguous and deterministic.
//!
//! The rules here are deliberately boring and total:
//!
//! * Integers are little-endian (the convention `epiphany-determinism`'s
//! [`Preimage`](epiphany_determinism::Preimage) already uses for length and
//! counter fields).
//! * Variable-length parts carry a `u32` little-endian length prefix, so a
//! decoder never has to guess where a field ends and two distinct values can
//! never share an encoding (Appendix D §"Canonical serialization
//! determinism").
//! * Sequences carry a `u32` count, then each element. Fixed-width elements are
//! written directly; variable-width elements are each length-prefixed.
//! * The single text field (a transaction label) is NFC-normalized before its
//! UTF-8 bytes are length-prefixed (Appendix D §"Text and Unicode").
//!
//! These mirror Agent B/D's provisional encodings: a concrete, reversible
//! canonical form that predates the Binary Format companion (a Pass 11
//! candidate — see `DECISIONS.md`).
use epiphany_determinism::CanonicalEncode;
use unicode_normalization::UnicodeNormalization;
/// Appends a `u32` in little-endian order.
#[inline]
pub(crate) fn push_u32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_le_bytes());
}
/// Appends a `u64` in little-endian order.
#[inline]
pub(crate) fn push_u64(out: &mut Vec<u8>, v: u64) {
out.extend_from_slice(&v.to_le_bytes());
}
/// Appends a single tag/discriminant byte.
#[inline]
pub(crate) fn push_tag(out: &mut Vec<u8>, tag: u8) {
out.push(tag);
}
/// Appends a boolean as a single canonical byte (`0` or `1`).
#[inline]
pub(crate) fn push_u8_bool(out: &mut Vec<u8>, b: bool) {
out.push(b as u8);
}
/// Appends a `u32` little-endian length prefix. Panics in debug builds if the
/// length does not fit in `u32`; canonical structures this large are not
/// representable in the prototype and would indicate a logic error, not data.
#[inline]
pub(crate) fn push_len(out: &mut Vec<u8>, len: usize) {
debug_assert!(len <= u32::MAX as usize, "canonical length exceeds u32");
push_u32(out, len as u32);
}
/// Appends raw bytes with a `u32` length prefix.
#[inline]
pub(crate) fn push_lp_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
push_len(out, bytes.len());
out.extend_from_slice(bytes);
}
/// Appends a text field: NFC-normalized, UTF-8, with a `u32` length prefix
/// (Appendix D §"Text and Unicode": canonical text fields MUST be NFC).
#[inline]
pub(crate) fn push_str(out: &mut Vec<u8>, s: &str) {
let nfc: String = s.nfc().collect();
push_lp_bytes(out, nfc.as_bytes());
}
/// Appends a canonical-encodable value's bytes directly (no length prefix);
/// use for fixed-width primitives whose width is known to the decoder.
#[inline]
pub(crate) fn push_canon<T: CanonicalEncode>(out: &mut Vec<u8>, value: &T) {
value.encode_canonical(out);
}
/// Appends a sequence of canonical-encodable values as `count` then each
/// element, every element length-prefixed so variable-width elements stay
/// unambiguous. The caller is responsible for having put `items` into the
/// normative iteration order *before* calling this (Appendix D §"Ordered
/// Iteration"); this helper preserves the given order and does not sort.
pub(crate) fn push_seq<T: CanonicalEncode>(out: &mut Vec<u8>, items: &[T]) {
push_len(out, items.len());
let mut scratch = Vec::new();
for item in items {
scratch.clear();
item.encode_canonical(&mut scratch);
push_lp_bytes(out, &scratch);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn length_prefixing_disambiguates_concatenation() {
// ("ab","") and ("a","b") must not collide once length-prefixed.
let mut x = Vec::new();
push_lp_bytes(&mut x, b"ab");
push_lp_bytes(&mut x, b"");
let mut y = Vec::new();
push_lp_bytes(&mut y, b"a");
push_lp_bytes(&mut y, b"b");
assert_ne!(x, y);
}
#[test]
fn nfc_normalizes_before_hashing() {
// U+00E9 (é) vs U+0065 U+0301 (e + combining acute) are the same NFC.
let mut a = Vec::new();
push_str(&mut a, "\u{00e9}");
let mut b = Vec::new();
push_str(&mut b, "e\u{0301}");
assert_eq!(a, b, "canonically-equivalent text must encode identically");
}
}