#[cfg(feature = "alloc")]
#[allow(
unused_imports,
reason = "alloc prelude items; subset used per cfg/feature combination"
)]
use alloc::{borrow::ToOwned, format, string::String, vec, vec::Vec};
pub mod dip;
pub mod direct;
pub mod drt;
pub mod icp;
pub mod ixn;
pub mod rot;
use crate::core::matter::code::{CesrCode, DigestCode};
use crate::core::matter::matter::Matter;
use crate::core::primitives::{Saider, Tholder};
use crate::keri::{
DelegatedInceptionEvent, DelegatedRotationEvent, Identifier, Ilk, InceptionEvent,
InteractionEvent, KeriEvent, RotationEvent, Seal,
};
use core::ops::Range;
use serde_json::{Map, Value};
use crate::serder::error::SerderError;
use crate::serder::primitives::{sn_to_hex, to_qb64_string};
use crate::serder::said::{compute_digest, said_placeholder};
use crate::serder::version::VERSION_SIZE_MAX;
pub use dip::serialize_delegated_inception;
pub use direct::DirectJson;
pub use drt::serialize_delegated_rotation;
pub use icp::serialize_inception;
pub use ixn::serialize_interaction;
pub use rot::serialize_rotation;
pub fn serialize(event: &KeriEvent) -> Result<SerializedEvent, SerderError> {
match event {
KeriEvent::Inception(e) => serialize_inception(e),
KeriEvent::Rotation(e) => serialize_rotation(e),
KeriEvent::Interaction(e) => serialize_interaction(e),
KeriEvent::DelegatedInception(e) => serialize_delegated_inception(e),
KeriEvent::DelegatedRotation(e) => serialize_delegated_rotation(e),
}
}
#[derive(Clone, Copy)]
pub enum EventRef<'e> {
Inception(&'e InceptionEvent),
Rotation(&'e RotationEvent),
Interaction(&'e InteractionEvent),
DelegatedInception(&'e DelegatedInceptionEvent),
DelegatedRotation(&'e DelegatedRotationEvent),
}
impl EventRef<'_> {
#[must_use]
pub const fn ilk(self) -> Ilk {
match self {
Self::Inception(_) => Ilk::Icp,
Self::Rotation(_) => Ilk::Rot,
Self::Interaction(_) => Ilk::Ixn,
Self::DelegatedInception(_) => Ilk::Dip,
Self::DelegatedRotation(_) => Ilk::Drt,
}
}
#[must_use]
pub const fn said_code(self) -> DigestCode {
match self {
Self::Inception(e) => *e.said().code(),
Self::Rotation(e) => *e.said().code(),
Self::Interaction(e) => *e.said().code(),
Self::DelegatedInception(e) => *e.inception().said().code(),
Self::DelegatedRotation(e) => *e.rotation().said().code(),
}
}
#[must_use]
pub const fn is_double_said(self) -> bool {
match self {
Self::Inception(e) => matches!(e.prefix(), Identifier::SelfAddressing(_)),
Self::DelegatedInception(e) => {
matches!(e.inception().prefix(), Identifier::SelfAddressing(_))
}
Self::Rotation(_) | Self::Interaction(_) | Self::DelegatedRotation(_) => false,
}
}
}
impl<'e> From<&'e KeriEvent> for EventRef<'e> {
fn from(event: &'e KeriEvent) -> Self {
match event {
KeriEvent::Inception(e) => Self::Inception(e),
KeriEvent::Rotation(e) => Self::Rotation(e),
KeriEvent::Interaction(e) => Self::Interaction(e),
KeriEvent::DelegatedInception(e) => Self::DelegatedInception(e),
KeriEvent::DelegatedRotation(e) => Self::DelegatedRotation(e),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventLayout {
pub size_slot: Range<usize>,
pub said_slot: Range<usize>,
pub prefix_slot: Option<Range<usize>>,
}
pub trait EventSerializer {
fn render(
&self,
event: EventRef<'_>,
said_placeholder: &str,
buf: &mut Vec<u8>,
) -> Result<EventLayout, SerderError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SerdeJson;
impl EventSerializer for SerdeJson {
fn render(
&self,
event: EventRef<'_>,
said_placeholder: &str,
buf: &mut Vec<u8>,
) -> Result<EventLayout, SerderError> {
let json = match event {
EventRef::Inception(e) => icp::render_json(e, said_placeholder)?,
EventRef::Rotation(e) => rot::render_json(e, said_placeholder)?,
EventRef::Interaction(e) => ixn::render_json(e, said_placeholder)?,
EventRef::DelegatedInception(e) => dip::render_json(e, said_placeholder)?,
EventRef::DelegatedRotation(e) => drt::render_json(e, said_placeholder)?,
};
extend_with_layout(buf, &json, said_placeholder, event.is_double_said())
}
}
pub fn serialize_with<B: EventSerializer>(
backend: &B,
event: EventRef<'_>,
) -> Result<SerializedEvent, SerderError> {
let digest_code = event.said_code();
let placeholder = said_placeholder(digest_code)?;
let mut buf = Vec::new();
let layout = backend.render(event, &placeholder, &mut buf)?;
let size = buf.len();
let size_u32 = u32::try_from(size)
.ok()
.filter(|s| *s <= VERSION_SIZE_MAX)
.ok_or(SerderError::VersionStringOverflow {
field: "size",
max: VERSION_SIZE_MAX,
})?;
patch_slot(
&mut buf,
&layout.size_slot,
format!("{size_u32:06x}").as_bytes(),
)?;
let said = compute_digest(&buf, digest_code)?;
let said_qb64 = to_qb64_string(&said);
patch_slot(&mut buf, &layout.said_slot, said_qb64.as_bytes())?;
let prefix = layout
.prefix_slot
.as_ref()
.map(|slot| {
patch_slot(&mut buf, slot, said_qb64.as_bytes())?;
Ok::<_, SerderError>(said.clone())
})
.transpose()?;
Ok(SerializedEvent {
raw: buf,
said,
prefix,
ilk: event.ilk(),
size,
event: (),
})
}
const ZERO_SIZE_VSTRING: &[u8] = b"KERI10JSON000000_";
const VSTRING_OFFSET: usize = 6;
const SIZE_OFFSET_IN_VSTRING: usize = 10;
const SIZE_WIDTH: usize = 6;
fn extend_with_layout(
buf: &mut Vec<u8>,
json: &str,
placeholder: &str,
double_said: bool,
) -> Result<EventLayout, SerderError> {
let base = buf.len();
let bytes = json.as_bytes();
let vs_end = VSTRING_OFFSET.checked_add(ZERO_SIZE_VSTRING.len()).ok_or(
SerderError::InvalidEventLayout("version-string bounds overflow"),
)?;
if bytes.get(..VSTRING_OFFSET) != Some(br#"{"v":""#.as_slice())
|| bytes.get(VSTRING_OFFSET..vs_end) != Some(ZERO_SIZE_VSTRING)
{
return Err(SerderError::InvalidEventLayout(
"rendered JSON does not begin with a zero-size v1 version string",
));
}
let said_rel = find_subslice(bytes, placeholder.as_bytes(), 0).ok_or(
SerderError::InvalidEventLayout("SAID placeholder not found"),
)?;
let said_rel_end = said_rel
.checked_add(placeholder.len())
.ok_or(SerderError::InvalidEventLayout("SAID slot bounds overflow"))?;
let prefix_slot = if double_said {
let rel = find_subslice(bytes, placeholder.as_bytes(), said_rel_end).ok_or(
SerderError::InvalidEventLayout("second SAID placeholder not found"),
)?;
let rel_end = rel
.checked_add(placeholder.len())
.ok_or(SerderError::InvalidEventLayout(
"prefix slot bounds overflow",
))?;
Some(abs_range(base, rel..rel_end)?)
} else {
None
};
let size_start = VSTRING_OFFSET
.checked_add(SIZE_OFFSET_IN_VSTRING)
.ok_or(SerderError::InvalidEventLayout("size slot bounds overflow"))?;
let size_end = size_start
.checked_add(SIZE_WIDTH)
.ok_or(SerderError::InvalidEventLayout("size slot bounds overflow"))?;
let layout = EventLayout {
size_slot: abs_range(base, size_start..size_end)?,
said_slot: abs_range(base, said_rel..said_rel_end)?,
prefix_slot,
};
buf.extend_from_slice(bytes);
Ok(layout)
}
fn abs_range(base: usize, rel: Range<usize>) -> Result<Range<usize>, SerderError> {
let start = base
.checked_add(rel.start)
.ok_or(SerderError::InvalidEventLayout("slot offset overflow"))?;
let end = base
.checked_add(rel.end)
.ok_or(SerderError::InvalidEventLayout("slot offset overflow"))?;
Ok(start..end)
}
fn find_subslice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
haystack
.get(from..)?
.windows(needle.len())
.position(|w| w == needle)
.and_then(|rel| from.checked_add(rel))
}
fn patch_slot(buf: &mut [u8], slot: &Range<usize>, replacement: &[u8]) -> Result<(), SerderError> {
let dst = buf
.get_mut(slot.clone())
.ok_or(SerderError::InvalidEventLayout("slot out of bounds"))?;
if dst.len() != replacement.len() {
return Err(SerderError::InvalidEventLayout(
"slot width does not match replacement",
));
}
dst.copy_from_slice(replacement);
Ok(())
}
pub struct SerializedEvent<E = ()> {
pub(crate) raw: Vec<u8>,
pub(crate) said: Saider<'static>,
pub(crate) prefix: Option<Saider<'static>>,
pub(crate) ilk: Ilk,
pub(crate) size: usize,
pub(crate) event: E,
}
impl<E> SerializedEvent<E> {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.raw
}
#[must_use]
pub const fn said(&self) -> &Saider<'static> {
&self.said
}
#[must_use]
pub const fn prefix(&self) -> Option<&Saider<'static>> {
self.prefix.as_ref()
}
#[must_use]
pub fn identifier(&self) -> Option<Identifier<'static>> {
self.prefix.clone().map(Identifier::SelfAddressing)
}
#[must_use]
pub const fn ilk(&self) -> Ilk {
self.ilk
}
#[must_use]
pub const fn size(&self) -> usize {
self.size
}
#[must_use]
pub const fn event(&self) -> &E {
&self.event
}
#[must_use]
pub fn into_event(self) -> E {
self.event
}
}
pub(crate) fn seal_to_json(seal: &Seal) -> Value {
let mut map = Map::new();
match seal {
Seal::Digest { d } => {
map.insert("d".to_owned(), Value::String(to_qb64_string(d)));
}
Seal::Root { rd } => {
map.insert("rd".to_owned(), Value::String(to_qb64_string(rd)));
}
Seal::Source { s, d } => {
map.insert("s".to_owned(), Value::String(sn_to_hex(s.value())));
map.insert("d".to_owned(), Value::String(to_qb64_string(d)));
}
Seal::Event { i, s, d } => {
map.insert("i".to_owned(), Value::String(to_qb64_string(i)));
map.insert("s".to_owned(), Value::String(sn_to_hex(s.value())));
map.insert("d".to_owned(), Value::String(to_qb64_string(d)));
}
Seal::Last { i } => {
map.insert("i".to_owned(), Value::String(to_qb64_string(i)));
}
}
Value::Object(map)
}
pub(crate) fn tholder_to_json(tholder: &Tholder) -> Value {
match tholder {
Tholder::Simple(n) => Value::String(format!("{n:x}")),
Tholder::Weighted(clauses) => {
let outer: Vec<Value> = clauses
.iter()
.map(|clause| {
let inner: Vec<Value> = clause
.iter()
.map(|(num, den)| Value::String(weight_to_string(*num, *den)))
.collect();
Value::Array(inner)
})
.collect();
if let [single] = <[Value]>::as_ref(&outer) {
single.clone()
} else {
Value::Array(outer)
}
}
}
}
pub(crate) fn weight_to_string(num: u64, den: u64) -> String {
if den != 0 && (num == 0 || num == den) {
format!("{}", num / den)
} else {
format!("{num}/{den}")
}
}
pub(crate) fn matters_to_json_array<C: CesrCode>(matters: &[Matter<'_, C>]) -> Value {
let mut arr = Vec::with_capacity(matters.len());
for m in matters {
arr.push(Value::String(to_qb64_string(m)));
}
Value::Array(arr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::matter::builder::MatterBuilder;
#[test]
fn tholder_zero_denominator_renders_without_panicking() {
let tholder = Tholder::Weighted(vec![vec![(0, 0), (1, 0)]]);
let rendered = tholder_to_json(&tholder);
assert_eq!(rendered, serde_json::json!(["0/0", "1/0"]));
}
use crate::core::matter::code::{DigestCode, VerKeyCode};
use crate::core::primitives::{Diger, Prefixer, Saider, Seqner, Tholder, Verfer};
use crate::keri::{
DelegatedInceptionEvent, DelegatedRotationEvent, InceptionEvent, InteractionEvent,
RotationEvent,
};
use alloc::borrow::Cow;
fn make_prefixer() -> Prefixer<'static> {
MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![0u8; 32]))
.unwrap()
.build()
.unwrap()
}
fn make_saider() -> Saider<'static> {
MatterBuilder::new()
.with_code(DigestCode::Blake3_256)
.with_raw(Cow::<[u8]>::Owned(vec![1u8; 32]))
.unwrap()
.build()
.unwrap()
}
fn make_verfer() -> Verfer<'static> {
MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![1u8; 32]))
.unwrap()
.build()
.unwrap()
}
fn make_diger() -> Diger<'static> {
MatterBuilder::new()
.with_code(DigestCode::Blake3_256)
.with_raw(Cow::<[u8]>::Owned(vec![2u8; 32]))
.unwrap()
.build()
.unwrap()
}
#[test]
fn serialize_dispatches_icp() {
let event = KeriEvent::Inception(InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
));
let result = serialize(&event).unwrap();
assert_eq!(result.ilk(), Ilk::Icp);
}
#[test]
fn serialize_dispatches_rot() {
let event = KeriEvent::Rotation(RotationEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
vec![],
0,
vec![],
vec![],
));
let result = serialize(&event).unwrap();
assert_eq!(result.ilk(), Ilk::Rot);
}
#[test]
fn serialize_dispatches_ixn() {
let event = KeriEvent::Interaction(InteractionEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![],
));
let result = serialize(&event).unwrap();
assert_eq!(result.ilk(), Ilk::Ixn);
}
#[test]
fn serialize_dispatches_dip() {
let event = KeriEvent::DelegatedInception(DelegatedInceptionEvent::new(
InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
),
make_prefixer().into(),
));
let result = serialize(&event).unwrap();
assert_eq!(result.ilk(), Ilk::Dip);
}
#[test]
fn serialize_dispatches_drt() {
let event = KeriEvent::DelegatedRotation(DelegatedRotationEvent::new(RotationEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
vec![],
0,
vec![],
vec![],
)));
let result = serialize(&event).unwrap();
assert_eq!(result.ilk(), Ilk::Drt);
}
#[test]
fn serialized_event_default_event_is_unit() {
let event = KeriEvent::Inception(InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
));
let result = serialize(&event).unwrap();
assert_eq!(*result.event(), ());
assert_eq!(result.into_event(), ());
}
#[test]
fn identifier_bridges_inception_prefix() {
use crate::serder::builder::icp::InceptionBuilder;
let verfer = MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(alloc::vec![7u8; 32])
.unwrap()
.build()
.unwrap();
let icp = InceptionBuilder::new()
.keys(alloc::vec![verfer])
.build()
.unwrap();
let id = icp
.identifier()
.expect("inception exposes a self-addressing identifier");
let saider = id
.as_saider()
.expect("inception identifier must be self-addressing");
assert_eq!(
saider.raw(),
icp.prefix().unwrap().raw(),
"identifier wraps the prefix SAID"
);
}
#[test]
fn tholder_to_json_weighted_boundary_values() {
let tholder = Tholder::Weighted(vec![vec![(0, 1), (1, 2), (1, 1)]]);
let json = tholder_to_json(&tholder);
let arr = json.as_array().expect("should be array");
assert_eq!(arr[0].as_str().expect("0"), "0");
assert_eq!(arr[1].as_str().expect("1/2"), "1/2");
assert_eq!(arr[2].as_str().expect("1"), "1");
}
#[test]
fn weight_to_string_exact_mapping() {
assert_eq!(weight_to_string(0, 1), "0");
assert_eq!(weight_to_string(1, 1), "1");
assert_eq!(weight_to_string(2, 2), "1");
assert_eq!(weight_to_string(u64::MAX, u64::MAX), "1");
assert_eq!(weight_to_string(1, 2), "1/2");
assert_eq!(weight_to_string(2, 4), "2/4");
assert_eq!(weight_to_string(3, 2), "3/2");
assert_eq!(weight_to_string(0, 0), "0/0");
assert_eq!(weight_to_string(1, 0), "1/0");
assert_eq!(weight_to_string(u64::MAX, 1), "18446744073709551615/1");
}
fn probe_icp_event() -> InceptionEvent {
InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
)
}
fn probe_rot_event() -> RotationEvent {
RotationEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
vec![],
0,
vec![],
vec![],
)
}
fn probe_ixn_event() -> InteractionEvent {
InteractionEvent::new(
make_prefixer().into(),
Seqner::new(1),
make_saider(),
make_saider(),
vec![],
)
}
fn probe_self_addressing_icp_event() -> InceptionEvent {
InceptionEvent::new(
Identifier::SelfAddressing(make_saider()),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_diger()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
)
}
#[test]
fn event_ref_ilk_and_double_said_mapping() {
let icp = probe_icp_event();
let icp_sa = probe_self_addressing_icp_event();
let rot = probe_rot_event();
let ixn = probe_ixn_event();
let dip = DelegatedInceptionEvent::new(probe_icp_event(), make_prefixer().into());
let dip_sa =
DelegatedInceptionEvent::new(probe_self_addressing_icp_event(), make_prefixer().into());
let drt = DelegatedRotationEvent::new(probe_rot_event());
let cases: [(EventRef<'_>, Ilk, bool); 7] = [
(EventRef::Inception(&icp), Ilk::Icp, false),
(EventRef::Inception(&icp_sa), Ilk::Icp, true),
(EventRef::Rotation(&rot), Ilk::Rot, false),
(EventRef::Interaction(&ixn), Ilk::Ixn, false),
(EventRef::DelegatedInception(&dip), Ilk::Dip, false),
(EventRef::DelegatedInception(&dip_sa), Ilk::Dip, true),
(EventRef::DelegatedRotation(&drt), Ilk::Drt, false),
];
for (event, ilk, double_said) in cases {
assert_eq!(event.ilk(), ilk);
assert_eq!(event.is_double_said(), double_said, "ilk {ilk:?}");
}
}
#[test]
fn event_ref_from_keri_event_preserves_variant() {
let events = [
(KeriEvent::Inception(probe_icp_event()), Ilk::Icp),
(KeriEvent::Rotation(probe_rot_event()), Ilk::Rot),
(KeriEvent::Interaction(probe_ixn_event()), Ilk::Ixn),
(
KeriEvent::DelegatedInception(DelegatedInceptionEvent::new(
probe_icp_event(),
make_prefixer().into(),
)),
Ilk::Dip,
),
(
KeriEvent::DelegatedRotation(DelegatedRotationEvent::new(probe_rot_event())),
Ilk::Drt,
),
];
for (event, ilk) in &events {
assert_eq!(EventRef::from(event).ilk(), *ilk);
}
}
struct HostileBackend {
rendered: &'static [u8],
layout: EventLayout,
}
impl EventSerializer for HostileBackend {
fn render(
&self,
_event: EventRef<'_>,
_said_placeholder: &str,
buf: &mut Vec<u8>,
) -> Result<EventLayout, SerderError> {
buf.extend_from_slice(self.rendered);
Ok(self.layout.clone())
}
}
fn expect_layout_error(backend: &HostileBackend) {
let ixn = probe_ixn_event();
let result = serialize_with(backend, EventRef::Interaction(&ixn));
assert!(
matches!(result, Err(SerderError::InvalidEventLayout(_))),
"a bogus layout must surface as InvalidEventLayout, never panic"
);
}
#[test]
fn hostile_backend_out_of_bounds_size_slot_is_rejected() {
expect_layout_error(&HostileBackend {
rendered: b"0123456789",
layout: EventLayout {
size_slot: 100..106,
said_slot: 0..2,
prefix_slot: None,
},
});
}
#[test]
fn hostile_backend_wrong_width_size_slot_is_rejected() {
expect_layout_error(&HostileBackend {
rendered: b"0123456789",
layout: EventLayout {
size_slot: 0..2,
said_slot: 2..4,
prefix_slot: None,
},
});
}
#[test]
fn hostile_backend_wrong_width_said_slot_is_rejected() {
expect_layout_error(&HostileBackend {
rendered: b"0123456789",
layout: EventLayout {
size_slot: 0..6,
said_slot: 6..8,
prefix_slot: None,
},
});
}
#[test]
fn hostile_backend_reversed_range_is_rejected() {
expect_layout_error(&HostileBackend {
rendered: b"0123456789",
layout: EventLayout {
size_slot: Range { start: 6, end: 0 },
said_slot: 0..2,
prefix_slot: None,
},
});
}
#[test]
fn hostile_backend_out_of_bounds_prefix_slot_is_rejected() {
const RENDERED: [u8; 64] = [b'x'; 64];
expect_layout_error(&HostileBackend {
rendered: &RENDERED,
layout: EventLayout {
size_slot: 0..6,
said_slot: 6..50,
prefix_slot: Some(1000..1044),
},
});
}
fn placeholder() -> String {
said_placeholder(DigestCode::Blake3_256).expect("Blake3-256 has a fixed placeholder")
}
#[test]
fn extend_with_layout_rejects_render_without_version_head() {
let ph = placeholder();
let mut buf = Vec::new();
let result = extend_with_layout(&mut buf, "{\"x\":1}", &ph, false);
assert!(matches!(result, Err(SerderError::InvalidEventLayout(_))));
}
#[test]
fn extend_with_layout_rejects_nonzero_size_version_head() {
let ph = placeholder();
let mut buf = Vec::new();
let json = format!("{{\"v\":\"KERI10JSON0000a1_\",\"d\":\"{ph}\"}}");
let result = extend_with_layout(&mut buf, &json, &ph, false);
assert!(
matches!(result, Err(SerderError::InvalidEventLayout(_))),
"a render must start from a zero-size version string"
);
}
#[test]
fn extend_with_layout_rejects_missing_placeholder() {
let ph = placeholder();
let mut buf = Vec::new();
let json = "{\"v\":\"KERI10JSON000000_\",\"t\":\"ixn\"}";
let result = extend_with_layout(&mut buf, json, &ph, false);
assert!(matches!(result, Err(SerderError::InvalidEventLayout(_))));
}
#[test]
fn extend_with_layout_rejects_missing_second_placeholder_for_double_said() {
let ph = placeholder();
let mut buf = Vec::new();
let json = format!("{{\"v\":\"KERI10JSON000000_\",\"d\":\"{ph}\"}}");
let result = extend_with_layout(&mut buf, &json, &ph, true);
assert!(
matches!(result, Err(SerderError::InvalidEventLayout(_))),
"double-SAID events must report two placeholder slots"
);
}
#[test]
fn extend_with_layout_offsets_are_absolute_into_prefilled_buffer() {
let ph = placeholder();
let mut buf = b"PREFILLED".to_vec();
let base = buf.len();
let json = format!("{{\"v\":\"KERI10JSON000000_\",\"d\":\"{ph}\",\"i\":\"{ph}\"}}");
let layout = extend_with_layout(&mut buf, &json, &ph, true).unwrap();
assert_eq!(&buf[layout.size_slot.clone()], b"000000");
assert_eq!(&buf[layout.said_slot.clone()], ph.as_bytes());
let prefix_slot = layout.prefix_slot.expect("double-SAID reports two slots");
assert_eq!(&buf[prefix_slot.clone()], ph.as_bytes());
assert!(
layout.said_slot.start > base && prefix_slot.start > layout.said_slot.end,
"slots must be absolute (past the prefilled bytes) and in order"
);
assert_eq!(&buf[..base], b"PREFILLED");
}
#[test]
fn serde_json_render_into_prefilled_buffer_reports_absolute_slots() {
let ph = placeholder();
let ixn = probe_ixn_event();
let mut buf = b"JUNK".to_vec();
let layout = SerdeJson
.render(EventRef::Interaction(&ixn), &ph, &mut buf)
.unwrap();
assert_eq!(&buf[..4], b"JUNK", "render must append, not overwrite");
assert_eq!(&buf[layout.size_slot], b"000000");
assert_eq!(&buf[layout.said_slot], ph.as_bytes());
assert!(layout.prefix_slot.is_none(), "ixn is single-SAID");
}
}