//! Heat2 encoder. //! //! Field layout is `3-byte packed tag || 1 type byte || value`. //! //! Two rules are easy to get wrong and both are load-bearing: //! //! 1. **Members serialise in ascending packed-tag order.** Not source order, //! not alphabetical order of the label — packed-tag order. (They coincide //! for equal-length uppercase labels, which is why a bug here hides.) //! 2. **A nested struct is terminated by `0x00`; the top-level payload is //! not.** The top level is delimited by the Fire2 length instead. //! //! Encoding is infallible: every `Value` has a wire form. use super::tag::Tag; use super::value::{Struct, Value, UNION_UNSET}; use super::varint; /// Encode a top-level payload body (no trailing terminator). pub fn encode(s: &Struct) -> Vec { let mut out = Vec::new(); encode_into(s, &mut out); out } /// Encode a top-level payload body, appending to `out`. pub fn encode_into(s: &Struct, out: &mut Vec) { encode_members(s, out); } fn encode_members(s: &Struct, out: &mut Vec) { // Stable sort by packed tag: equal tags keep their relative order, so a // decoded frame containing duplicates re-encodes identically. let mut ordered: Vec<&(Tag, Value)> = s.fields.iter().collect(); ordered.sort_by_key(|(tag, _)| *tag); for (tag, value) in ordered { out.extend_from_slice(&tag.as_bytes()); out.push(value.type_id().as_byte()); encode_value(value, out); } } fn encode_value(value: &Value, out: &mut Vec) { match value { Value::Int(v) => varint::encode(*v, out), Value::String(s) => { // Trailing NULs are stripped before framing so the length and the // single terminator stay consistent; the length INCLUDES that NUL. let raw = s.as_bytes(); let end = raw.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1); let raw = &raw[..end]; varint::encode(raw.len() as i64 + 1, out); out.extend_from_slice(raw); out.push(0x00); } Value::Blob(b) => { // Blob length EXCLUDES a terminator — there isn't one. varint::encode(b.len() as i64, out); out.extend_from_slice(b); } Value::Struct(s) => { encode_members(s, out); out.push(0x00); } Value::List { elem, items } => { out.push(elem.as_byte()); varint::encode(items.len() as i64, out); for it in items { encode_value(it, out); } } Value::Map { key, val, entries } => { out.push(key.as_byte()); out.push(val.as_byte()); varint::encode(entries.len() as i64, out); for (k, v) in entries { encode_value(k, out); encode_value(v, out); } } Value::Union { key, member } => { out.push(*key); if *key != UNION_UNSET { if let Some(m) = member { let (tag, val) = m.as_ref(); out.extend_from_slice(&tag.as_bytes()); out.push(val.type_id().as_byte()); encode_value(val, out); } } } Value::VarList(items) => { varint::encode(items.len() as i64, out); for n in items { varint::encode(*n, out); } } Value::ObjType { component, ty } => { varint::encode(*component, out); varint::encode(*ty, out); } Value::ObjId { component, ty, id } => { varint::encode(*component, out); varint::encode(*ty, out); varint::encode(*id, out); } Value::Float(f) => out.extend_from_slice(&f.to_be_bytes()), } } /// Encode a value on its own, without a tag or type byte. /// /// Useful for a nested struct that a caller frames itself; note this DOES emit /// the `0x00` terminator for `Value::Struct`. pub fn encode_value_only(value: &Value) -> Vec { let mut out = Vec::new(); encode_value(value, &mut out); out } /// Convenience: the type byte a value will be written with. pub fn type_byte_of(value: &Value) -> u8 { value.type_id().as_byte() } #[cfg(test)] mod tests { use super::*; #[test] fn members_sort_by_packed_tag_not_source_order() { let s = Struct::new() .with("ZZZZ", Value::Int(3)) .with("AAAA", Value::Int(1)) .with("MMMM", Value::Int(2)); let bytes = encode(&s); // Each member is 3 tag + 1 type + 1 varint = 5 bytes; values must come // out 1, 2, 3. assert_eq!(bytes.len(), 15); assert_eq!([bytes[4], bytes[9], bytes[14]], [1, 2, 3]); } #[test] fn top_level_is_unterminated_but_nested_is_terminated() { let flat = encode(&Struct::new().with("A", Value::Int(1))); assert_eq!( flat.last(), Some(&1u8), "no trailing terminator at top level" ); let nested = encode(&Struct::new().with( "OUTR", Value::Struct(Struct::new().with("A", Value::Int(1))), )); assert_eq!( nested.last(), Some(&0u8), "nested struct is 0x00 terminated" ); } #[test] fn empty_nested_struct_is_a_bare_terminator() { let bytes = encode(&Struct::new().with("MTST", Value::Struct(Struct::new()))); assert_eq!(bytes.len(), 5); // 3 tag + 1 type + 1 terminator assert_eq!(bytes[4], 0x00); } #[test] fn string_length_includes_the_nul() { let bytes = encode(&Struct::new().with("S", Value::String("ab".into()))); // tag(3) type(1) len(1)=3 'a' 'b' NUL assert_eq!(&bytes[4..], &[0x03, b'a', b'b', 0x00]); } #[test] fn empty_string_is_length_one_plus_nul() { let bytes = encode(&Struct::new().with("S", Value::String(String::new()))); assert_eq!(&bytes[4..], &[0x01, 0x00]); } #[test] fn blob_length_excludes_a_terminator() { let bytes = encode(&Struct::new().with("B", Value::Blob(vec![1, 2, 3]))); assert_eq!(&bytes[4..], &[0x03, 1, 2, 3]); } #[test] fn float_is_big_endian_f32() { let bytes = encode(&Struct::new().with("F", Value::Float(1.5))); assert_eq!(&bytes[4..], &1.5f32.to_be_bytes()); } }