#[cfg(feature = "alloc")]
#[allow(
unused_imports,
reason = "alloc prelude items; subset used per cfg/feature combination"
)]
use alloc::{borrow::ToOwned, format, string::String, string::ToString, vec, vec::Vec};
use core::ops::Range;
use core::str;
use crate::serder::error::SerderError;
use crate::serder::version::{SerKind, VERSION_STRING_LEN, VersionString};
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) struct Spanned<'a> {
pub(crate) value: &'a str,
pub(crate) span: Range<usize>,
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) enum ParsedTholder<'a> {
Hex(&'a str),
Number(&'a str),
Weighted(Vec<Vec<&'a str>>),
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) enum ParsedCount<'a> {
Hex(&'a str),
Number(&'a str),
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) enum ParsedSeal<'a> {
Digest {
d: &'a str,
},
Root {
rd: &'a str,
},
Source {
s: &'a str,
d: &'a str,
},
Event {
i: &'a str,
s: &'a str,
d: &'a str,
},
Last {
i: &'a str,
},
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) struct ParsedIcp<'a> {
pub(crate) said: Spanned<'a>,
pub(crate) prefix: Spanned<'a>,
pub(crate) sn: &'a str,
pub(crate) threshold: ParsedTholder<'a>,
pub(crate) keys: Vec<&'a str>,
pub(crate) next_threshold: ParsedTholder<'a>,
pub(crate) next_keys: Vec<&'a str>,
pub(crate) witness_threshold: ParsedCount<'a>,
pub(crate) witnesses: Vec<&'a str>,
pub(crate) config: Vec<&'a str>,
pub(crate) anchors: Vec<ParsedSeal<'a>>,
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) struct ParsedDip<'a> {
pub(crate) icp: ParsedIcp<'a>,
pub(crate) delegator: &'a str,
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) struct ParsedRot<'a> {
pub(crate) said: Spanned<'a>,
pub(crate) prefix: &'a str,
pub(crate) sn: &'a str,
pub(crate) prior: &'a str,
pub(crate) threshold: ParsedTholder<'a>,
pub(crate) keys: Vec<&'a str>,
pub(crate) next_threshold: ParsedTholder<'a>,
pub(crate) next_keys: Vec<&'a str>,
pub(crate) witness_threshold: ParsedCount<'a>,
pub(crate) witness_removals: Vec<&'a str>,
pub(crate) witness_additions: Vec<&'a str>,
pub(crate) anchors: Vec<ParsedSeal<'a>>,
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) struct ParsedIxn<'a> {
pub(crate) said: Spanned<'a>,
pub(crate) prefix: &'a str,
pub(crate) sn: &'a str,
pub(crate) prior: &'a str,
pub(crate) anchors: Vec<ParsedSeal<'a>>,
}
#[derive(Debug)]
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) enum ParsedEvent<'a> {
Inception(ParsedIcp<'a>),
Rotation(ParsedRot<'a>),
Interaction(ParsedIxn<'a>),
DelegatedInception(ParsedDip<'a>),
DelegatedRotation(ParsedRot<'a>),
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) struct Scanner<'a> {
input: &'a [u8],
pos: usize,
}
impl<'a> Scanner<'a> {
pub(crate) const fn new(input: &'a [u8]) -> Self {
Self { input, pos: 0 }
}
fn err_at(&self, offset: usize, expected: &'static str) -> SerderError {
SerderError::NonCanonical {
offset,
expected,
found: self.input.get(offset).copied(),
}
}
fn err(&self, expected: &'static str) -> SerderError {
self.err_at(self.pos, expected)
}
fn peek(&self) -> Option<u8> {
self.input.get(self.pos).copied()
}
fn take_lit(&mut self, lit: &'static str) -> bool {
let Some(end) = self.pos.checked_add(lit.len()) else {
return false;
};
if self.input.get(self.pos..end) == Some(lit.as_bytes()) {
self.pos = end;
true
} else {
false
}
}
pub(crate) fn expect(&mut self, lit: &'static str) -> Result<(), SerderError> {
if self.take_lit(lit) {
Ok(())
} else {
Err(self.err(lit))
}
}
fn advance(&mut self, by: usize, expected: &'static str) -> Result<(), SerderError> {
self.pos = self.pos.checked_add(by).ok_or_else(|| self.err(expected))?;
Ok(())
}
pub(crate) fn string(&mut self) -> Result<Spanned<'a>, SerderError> {
self.expect("\"")?;
let start = self.pos;
loop {
match self.peek() {
Some(b'"') => break,
Some(b'\\') => {
return Err(
self.err("unescaped string byte (canonical values never require escaping)")
);
}
Some(b) if b < 0x20 => {
return Err(self.err("unescaped string byte (no control characters)"));
}
Some(_) => self.advance(1, "string byte")?,
None => return Err(self.err("closing '\"'")),
}
}
let span = start..self.pos;
let bytes = self
.input
.get(span.clone())
.ok_or(SerderError::InvalidEventLayout("string span out of bounds"))?;
let value = str::from_utf8(bytes).map_err(|e| {
start.checked_add(e.valid_up_to()).map_or_else(
|| SerderError::InvalidEventLayout("UTF-8 error offset overflow"),
|offset| self.err_at(offset, "UTF-8 string value"),
)
})?;
self.expect("\"")?;
Ok(Spanned { value, span })
}
pub(crate) fn integer(&mut self) -> Result<&'a str, SerderError> {
let start = self.pos;
match self.peek() {
Some(b'0') => {
self.advance(1, "digit")?;
if matches!(self.peek(), Some(b'0'..=b'9')) {
return Err(self.err("no leading zeros in canonical integer"));
}
}
Some(b'1'..=b'9') => {
self.advance(1, "digit")?;
while matches!(self.peek(), Some(b'0'..=b'9')) {
self.advance(1, "digit")?;
}
}
_ => return Err(self.err("digit")),
}
let bytes = self
.input
.get(start..self.pos)
.ok_or(SerderError::InvalidEventLayout(
"integer span out of bounds",
))?;
str::from_utf8(bytes).map_err(|_| self.err_at(start, "ASCII integer"))
}
pub(crate) fn finish(&self) -> Result<(), SerderError> {
if self.pos == self.input.len() {
Ok(())
} else {
Err(self.err("end of input"))
}
}
}
fn tail_list<'a, T>(
sc: &mut Scanner<'a>,
mut item: impl FnMut(&mut Scanner<'a>) -> Result<T, SerderError>,
) -> Result<Vec<T>, SerderError> {
let mut items = vec![item(sc)?];
loop {
if sc.take_lit("]") {
return Ok(items);
}
sc.expect(",")?;
items.push(item(sc)?);
}
}
fn delimited_list<'a, T>(
sc: &mut Scanner<'a>,
item: impl FnMut(&mut Scanner<'a>) -> Result<T, SerderError>,
) -> Result<Vec<T>, SerderError> {
sc.expect("[")?;
if sc.take_lit("]") {
return Ok(Vec::new());
}
tail_list(sc, item)
}
fn string_array<'a>(sc: &mut Scanner<'a>) -> Result<Vec<&'a str>, SerderError> {
delimited_list(sc, |s| s.string().map(|sp| sp.value))
}
fn tholder<'a>(sc: &mut Scanner<'a>) -> Result<ParsedTholder<'a>, SerderError> {
match sc.peek() {
Some(b'"') => Ok(ParsedTholder::Hex(sc.string()?.value)),
Some(b'0'..=b'9') => Ok(ParsedTholder::Number(sc.integer()?)),
Some(b'[') => weighted(sc),
_ => Err(sc.err("threshold (hex string, integer, or weighted array)")),
}
}
fn weighted<'a>(sc: &mut Scanner<'a>) -> Result<ParsedTholder<'a>, SerderError> {
sc.expect("[")?;
if sc.take_lit("]") {
return Ok(ParsedTholder::Weighted(Vec::new()));
}
match sc.peek() {
Some(b'"') => {
let clause = tail_list(sc, |s| s.string().map(|sp| sp.value))?;
Ok(ParsedTholder::Weighted(vec![clause]))
}
Some(b'[') => {
let clauses = tail_list(sc, string_array)?;
Ok(ParsedTholder::Weighted(clauses))
}
_ => Err(sc.err("weight fraction string or clause array")),
}
}
fn count<'a>(sc: &mut Scanner<'a>) -> Result<ParsedCount<'a>, SerderError> {
match sc.peek() {
Some(b'"') => Ok(ParsedCount::Hex(sc.string()?.value)),
Some(b'0'..=b'9') => Ok(ParsedCount::Number(sc.integer()?)),
_ => Err(sc.err("count (hex string or integer)")),
}
}
fn seal<'a>(sc: &mut Scanner<'a>) -> Result<ParsedSeal<'a>, SerderError> {
sc.expect("{")?;
if sc.take_lit("\"d\":") {
let d = sc.string()?.value;
sc.expect("}")?;
return Ok(ParsedSeal::Digest { d });
}
if sc.take_lit("\"rd\":") {
let rd = sc.string()?.value;
sc.expect("}")?;
return Ok(ParsedSeal::Root { rd });
}
if sc.take_lit("\"s\":") {
let s = sc.string()?.value;
sc.expect(",\"d\":")?;
let d = sc.string()?.value;
sc.expect("}")?;
return Ok(ParsedSeal::Source { s, d });
}
if sc.take_lit("\"i\":") {
let i = sc.string()?.value;
if sc.take_lit("}") {
return Ok(ParsedSeal::Last { i });
}
sc.expect(",\"s\":")?;
let s = sc.string()?.value;
sc.expect(",\"d\":")?;
let d = sc.string()?.value;
sc.expect("}")?;
return Ok(ParsedSeal::Event { i, s, d });
}
Err(sc.err("seal object key (\"d\", \"rd\", \"s\", or \"i\")"))
}
fn seal_array<'a>(sc: &mut Scanner<'a>) -> Result<Vec<ParsedSeal<'a>>, SerderError> {
delimited_list(sc, seal)
}
fn head(raw: &[u8]) -> Result<(Scanner<'_>, Spanned<'_>), SerderError> {
let mut sc = Scanner::new(raw);
sc.expect("{\"v\":\"")?;
let vs_start = sc.pos;
let vs_end = vs_start
.checked_add(VERSION_STRING_LEN)
.ok_or(SerderError::InvalidEventLayout("version span overflow"))?;
let vs_bytes = raw
.get(vs_start..vs_end)
.ok_or_else(|| sc.err("17-byte version string"))?;
if let Some(rel) = vs_bytes.iter().position(|b| !b.is_ascii()) {
let offset = vs_start
.checked_add(rel)
.ok_or(SerderError::InvalidEventLayout("version span overflow"))?;
return Err(sc.err_at(offset, "ASCII version string"));
}
let vs_str =
str::from_utf8(vs_bytes).map_err(|_| sc.err_at(vs_start, "ASCII version string"))?;
let vs = VersionString::parse(vs_str)?;
if vs.kind != SerKind::Json {
return Err(SerderError::InvalidVersionString(format!(
"expected JSON, got {}",
vs.kind.as_str()
)));
}
let expected_size =
usize::try_from(vs.size).map_err(|e| SerderError::InvalidVersionString(e.to_string()))?;
if expected_size != raw.len() {
return Err(SerderError::InvalidVersionString(format!(
"version string size {} does not match actual size {}",
expected_size,
raw.len()
)));
}
sc.pos = vs_end;
sc.expect("\",\"t\":")?;
let ilk = sc.string()?;
Ok((sc, ilk))
}
fn icp_fields<'a>(sc: &mut Scanner<'a>) -> Result<ParsedIcp<'a>, SerderError> {
sc.expect(",\"d\":")?;
let said = sc.string()?;
sc.expect(",\"i\":")?;
let prefix = sc.string()?;
sc.expect(",\"s\":")?;
let sn = sc.string()?.value;
sc.expect(",\"kt\":")?;
let threshold = tholder(sc)?;
sc.expect(",\"k\":")?;
let keys = string_array(sc)?;
sc.expect(",\"nt\":")?;
let next_threshold = tholder(sc)?;
sc.expect(",\"n\":")?;
let next_keys = string_array(sc)?;
sc.expect(",\"bt\":")?;
let witness_threshold = count(sc)?;
sc.expect(",\"b\":")?;
let witnesses = string_array(sc)?;
sc.expect(",\"c\":")?;
let config = string_array(sc)?;
sc.expect(",\"a\":")?;
let anchors = seal_array(sc)?;
Ok(ParsedIcp {
said,
prefix,
sn,
threshold,
keys,
next_threshold,
next_keys,
witness_threshold,
witnesses,
config,
anchors,
})
}
fn icp_body(mut sc: Scanner<'_>) -> Result<ParsedIcp<'_>, SerderError> {
let fields = icp_fields(&mut sc)?;
sc.expect("}")?;
sc.finish()?;
Ok(fields)
}
fn dip_body(mut sc: Scanner<'_>) -> Result<ParsedDip<'_>, SerderError> {
let icp = icp_fields(&mut sc)?;
sc.expect(",\"di\":")?;
let delegator = sc.string()?.value;
sc.expect("}")?;
sc.finish()?;
Ok(ParsedDip { icp, delegator })
}
fn rot_body(mut sc: Scanner<'_>) -> Result<ParsedRot<'_>, SerderError> {
sc.expect(",\"d\":")?;
let said = sc.string()?;
sc.expect(",\"i\":")?;
let prefix = sc.string()?.value;
sc.expect(",\"s\":")?;
let sn = sc.string()?.value;
sc.expect(",\"p\":")?;
let prior = sc.string()?.value;
sc.expect(",\"kt\":")?;
let threshold = tholder(&mut sc)?;
sc.expect(",\"k\":")?;
let keys = string_array(&mut sc)?;
sc.expect(",\"nt\":")?;
let next_threshold = tholder(&mut sc)?;
sc.expect(",\"n\":")?;
let next_keys = string_array(&mut sc)?;
sc.expect(",\"bt\":")?;
let witness_threshold = count(&mut sc)?;
sc.expect(",\"br\":")?;
let witness_removals = string_array(&mut sc)?;
sc.expect(",\"ba\":")?;
let witness_additions = string_array(&mut sc)?;
sc.expect(",\"a\":")?;
let anchors = seal_array(&mut sc)?;
sc.expect("}")?;
sc.finish()?;
Ok(ParsedRot {
said,
prefix,
sn,
prior,
threshold,
keys,
next_threshold,
next_keys,
witness_threshold,
witness_removals,
witness_additions,
anchors,
})
}
fn ixn_body(mut sc: Scanner<'_>) -> Result<ParsedIxn<'_>, SerderError> {
sc.expect(",\"d\":")?;
let said = sc.string()?;
sc.expect(",\"i\":")?;
let prefix = sc.string()?.value;
sc.expect(",\"s\":")?;
let sn = sc.string()?.value;
sc.expect(",\"p\":")?;
let prior = sc.string()?.value;
sc.expect(",\"a\":")?;
let anchors = seal_array(&mut sc)?;
sc.expect("}")?;
sc.finish()?;
Ok(ParsedIxn {
said,
prefix,
sn,
prior,
anchors,
})
}
fn require_ilk(
sc: &Scanner<'_>,
ilk: &Spanned<'_>,
expected: &'static str,
) -> Result<(), SerderError> {
if ilk.value == expected {
Ok(())
} else {
Err(sc.err_at(ilk.span.start, expected))
}
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) fn parse_event(raw: &[u8]) -> Result<ParsedEvent<'_>, SerderError> {
let (sc, ilk) = head(raw)?;
match ilk.value {
"icp" => Ok(ParsedEvent::Inception(icp_body(sc)?)),
"rot" => Ok(ParsedEvent::Rotation(rot_body(sc)?)),
"ixn" => Ok(ParsedEvent::Interaction(ixn_body(sc)?)),
"dip" => Ok(ParsedEvent::DelegatedInception(dip_body(sc)?)),
"drt" => Ok(ParsedEvent::DelegatedRotation(rot_body(sc)?)),
other => Err(SerderError::UnknownIlk(other.to_owned())),
}
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) fn parse_inception(raw: &[u8]) -> Result<ParsedIcp<'_>, SerderError> {
let (sc, ilk) = head(raw)?;
require_ilk(&sc, &ilk, "icp")?;
icp_body(sc)
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) fn parse_rotation(raw: &[u8]) -> Result<ParsedRot<'_>, SerderError> {
let (sc, ilk) = head(raw)?;
require_ilk(&sc, &ilk, "rot")?;
rot_body(sc)
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) fn parse_interaction(raw: &[u8]) -> Result<ParsedIxn<'_>, SerderError> {
let (sc, ilk) = head(raw)?;
require_ilk(&sc, &ilk, "ixn")?;
ixn_body(sc)
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) fn parse_delegated_inception(raw: &[u8]) -> Result<ParsedDip<'_>, SerderError> {
let (sc, ilk) = head(raw)?;
require_ilk(&sc, &ilk, "dip")?;
dip_body(sc)
}
#[allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is intentional — the enclosing module is crate-internal and `unreachable_pub` denies plain `pub`"
)]
pub(crate) fn parse_delegated_rotation(raw: &[u8]) -> Result<ParsedRot<'_>, SerderError> {
let (sc, ilk) = head(raw)?;
require_ilk(&sc, &ilk, "drt")?;
rot_body(sc)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::matter::builder::MatterBuilder;
use crate::core::matter::code::{DigestCode, VerKeyCode};
use crate::core::primitives::{Prefixer, Saider, Seqner, Tholder, Verfer};
use crate::keri::{
ConfigTrait, DelegatedInceptionEvent, DelegatedRotationEvent, Identifier, InceptionEvent,
InteractionEvent, RotationEvent, Seal,
};
use crate::serder::serialize::{
serialize_delegated_inception, serialize_delegated_rotation, serialize_inception,
serialize_interaction, serialize_rotation,
};
use alloc::borrow::Cow;
fn non_canonical_at(e: &SerderError) -> Option<(usize, &'static str)> {
if let SerderError::NonCanonical {
offset, expected, ..
} = e
{
Some((*offset, expected))
} else {
None
}
}
#[test]
fn scanner_string_reads_value_and_span() {
let mut sc = Scanner::new(b"\"abc\"rest");
let s = sc.string().unwrap();
assert_eq!(s.value, "abc");
assert_eq!(s.span, 1..4);
assert_eq!(sc.pos, 5);
}
#[test]
fn scanner_string_rejects_escape() {
let mut sc = Scanner::new(b"\"a\\u0030\"");
let err = sc.string().unwrap_err();
let (offset, _) = non_canonical_at(&err).expect("NonCanonical");
assert_eq!(offset, 2, "the backslash byte is the violation");
}
#[test]
fn scanner_string_rejects_control_char() {
let mut sc = Scanner::new(b"\"a\x01b\"");
assert!(matches!(
sc.string(),
Err(SerderError::NonCanonical { offset: 2, .. })
));
}
#[test]
fn scanner_string_rejects_unterminated() {
let mut sc = Scanner::new(b"\"abc");
assert!(matches!(
sc.string(),
Err(SerderError::NonCanonical {
offset: 4,
found: None,
..
})
));
}
#[test]
fn scanner_string_rejects_non_utf8() {
let mut sc = Scanner::new(b"\"\xFF\xFE\"");
assert!(matches!(
sc.string(),
Err(SerderError::NonCanonical { offset: 1, .. })
));
}
#[test]
fn scanner_string_utf8_error_reports_violating_byte() {
let mut sc = Scanner::new(b"\"ab\xFF\"");
assert!(matches!(
sc.string(),
Err(SerderError::NonCanonical {
offset: 3,
found: Some(0xFF),
..
})
));
}
#[test]
fn scanner_string_accepts_multibyte_utf8() {
let input = "\"héllo\"".as_bytes();
let mut sc = Scanner::new(input);
let s = sc.string().unwrap();
assert_eq!(s.value, "héllo");
assert_eq!(s.span, 1..7);
assert_eq!(&input[s.span.clone()], s.value.as_bytes());
}
#[test]
fn scanner_string_empty_input_and_empty_value() {
let mut sc = Scanner::new(b"");
assert!(matches!(
sc.string(),
Err(SerderError::NonCanonical {
offset: 0,
found: None,
..
})
));
let mut sc2 = Scanner::new(b"\"\"");
let s = sc2.string().unwrap();
assert_eq!(s.value, "");
assert_eq!(s.span, 1..1);
sc2.finish().unwrap();
}
#[test]
fn scanner_integer_grammar() {
assert_eq!(Scanner::new(b"0,").integer().unwrap(), "0");
assert_eq!(Scanner::new(b"10}").integer().unwrap(), "10");
assert!(Scanner::new(b"01").integer().is_err(), "leading zero");
assert!(Scanner::new(b"-1").integer().is_err(), "sign");
assert!(Scanner::new(b"x").integer().is_err(), "non-digit");
}
#[test]
fn scanner_integer_boundaries() {
let mut empty = Scanner::new(b"");
assert!(matches!(
empty.integer(),
Err(SerderError::NonCanonical {
offset: 0,
found: None,
..
})
));
let mut eof_terminated = Scanner::new(b"907");
assert_eq!(eof_terminated.integer().unwrap(), "907");
eof_terminated.finish().unwrap();
}
#[test]
fn scanner_expect_reports_offset_and_found() {
let mut sc = Scanner::new(b"abc");
let err = sc.expect("abX").unwrap_err();
assert!(matches!(
err,
SerderError::NonCanonical {
offset: 0,
found: Some(b'a'),
..
}
));
}
#[test]
fn scanner_finish_rejects_trailing() {
let mut sc = Scanner::new(b"ab");
sc.expect("ab").unwrap();
sc.finish().unwrap();
let mut sc2 = Scanner::new(b"abX");
sc2.expect("ab").unwrap();
assert!(matches!(
sc2.finish(),
Err(SerderError::NonCanonical {
offset: 2,
found: Some(b'X'),
..
})
));
}
#[test]
fn string_array_shapes() {
assert!(string_array(&mut Scanner::new(b"[]")).unwrap().is_empty());
assert_eq!(
string_array(&mut Scanner::new(b"[\"a\",\"b\"]")).unwrap(),
vec!["a", "b"]
);
assert!(
string_array(&mut Scanner::new(b"[\"a\",]")).is_err(),
"trailing comma"
);
assert!(
string_array(&mut Scanner::new(b"[ \"a\"]")).is_err(),
"whitespace"
);
}
#[test]
fn tholder_shapes() {
assert!(matches!(
tholder(&mut Scanner::new(b"\"a\"")).unwrap(),
ParsedTholder::Hex("a")
));
assert!(matches!(
tholder(&mut Scanner::new(b"2,")).unwrap(),
ParsedTholder::Number("2")
));
let ParsedTholder::Weighted(flat) =
tholder(&mut Scanner::new(b"[\"1/2\",\"1/2\"]")).unwrap()
else {
unreachable!()
};
assert_eq!(flat, vec![vec!["1/2", "1/2"]]);
let ParsedTholder::Weighted(nested) =
tholder(&mut Scanner::new(b"[[\"1/2\",\"1/2\"],[\"1\"]]")).unwrap()
else {
unreachable!()
};
assert_eq!(nested, vec![vec!["1/2", "1/2"], vec!["1"]]);
let ParsedTholder::Weighted(empty) = tholder(&mut Scanner::new(b"[]")).unwrap() else {
unreachable!()
};
assert!(empty.is_empty());
assert!(tholder(&mut Scanner::new(b"true")).is_err());
}
#[test]
fn count_shapes() {
assert!(matches!(
count(&mut Scanner::new(b"\"0\"")).unwrap(),
ParsedCount::Hex("0")
));
assert!(matches!(
count(&mut Scanner::new(b"3,")).unwrap(),
ParsedCount::Number("3")
));
assert!(count(&mut Scanner::new(b"[]")).is_err());
}
#[test]
fn seal_shapes() {
assert!(matches!(
seal(&mut Scanner::new(b"{\"d\":\"X\"}")).unwrap(),
ParsedSeal::Digest { d: "X" }
));
assert!(matches!(
seal(&mut Scanner::new(b"{\"rd\":\"X\"}")).unwrap(),
ParsedSeal::Root { rd: "X" }
));
assert!(matches!(
seal(&mut Scanner::new(b"{\"s\":\"1\",\"d\":\"X\"}")).unwrap(),
ParsedSeal::Source { s: "1", d: "X" }
));
assert!(matches!(
seal(&mut Scanner::new(b"{\"i\":\"I\",\"s\":\"1\",\"d\":\"X\"}")).unwrap(),
ParsedSeal::Event {
i: "I",
s: "1",
d: "X"
}
));
assert!(matches!(
seal(&mut Scanner::new(b"{\"i\":\"I\"}")).unwrap(),
ParsedSeal::Last { i: "I" }
));
assert!(
seal(&mut Scanner::new(b"{\"d\":\"X\",\"s\":\"1\"}")).is_err(),
"out-of-order seal fields are non-canonical"
);
assert!(
seal(&mut Scanner::new(b"{\"x\":\"X\"}")).is_err(),
"unknown seal key"
);
}
#[test]
fn weighted_rejects_non_string_non_array_element() {
let mut sc = Scanner::new(b"[true]");
assert!(matches!(
weighted(&mut sc),
Err(SerderError::NonCanonical { offset: 1, .. })
));
}
#[test]
fn seal_array_shapes() {
assert!(seal_array(&mut Scanner::new(b"[]")).unwrap().is_empty());
let seals = seal_array(&mut Scanner::new(b"[{\"d\":\"X\"},{\"i\":\"I\"}]")).unwrap();
assert_eq!(seals.len(), 2);
assert!(matches!(seals[0], ParsedSeal::Digest { d: "X" }));
assert!(matches!(seals[1], ParsedSeal::Last { i: "I" }));
}
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 probe_icp_bytes() -> Vec<u8> {
let event = InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_saider()],
Tholder::Simple(1),
vec![make_prefixer()],
1,
vec![ConfigTrait::EstOnly],
vec![Seal::Digest { d: make_saider() }],
);
serialize_inception(&event).unwrap().as_bytes().to_vec()
}
fn probe_ixn_bytes() -> Vec<u8> {
let event = InteractionEvent::new(
make_prefixer().into(),
Seqner::new(3),
make_saider(),
make_saider(),
vec![],
);
serialize_interaction(&event).unwrap().as_bytes().to_vec()
}
fn make_rot() -> RotationEvent {
RotationEvent::new(
make_prefixer().into(),
Seqner::new(2),
make_saider(),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_saider()],
Tholder::Simple(1),
vec![make_prefixer()],
vec![make_prefixer()],
1,
vec![],
vec![Seal::Digest { d: make_saider() }],
)
}
fn probe_rot_bytes() -> Vec<u8> {
serialize_rotation(&make_rot()).unwrap().as_bytes().to_vec()
}
fn probe_dip_bytes() -> Vec<u8> {
let icp = InceptionEvent::new(
make_prefixer().into(),
Seqner::new(0),
make_saider(),
vec![make_verfer()],
Tholder::Simple(1),
vec![make_saider()],
Tholder::Simple(1),
vec![],
0,
vec![],
vec![],
);
let delegator: Identifier<'static> = make_prefixer().into();
let dip = DelegatedInceptionEvent::new(icp, delegator);
serialize_delegated_inception(&dip)
.unwrap()
.as_bytes()
.to_vec()
}
fn probe_drt_bytes() -> Vec<u8> {
let drt = DelegatedRotationEvent::new(make_rot());
serialize_delegated_rotation(&drt)
.unwrap()
.as_bytes()
.to_vec()
}
fn fix_size(raw: &mut [u8]) {
let size = raw.len();
let hex = format!("{size:06x}");
raw[16..22].copy_from_slice(hex.as_bytes());
}
#[test]
fn parse_event_reads_writer_output_icp() {
let raw = probe_icp_bytes();
let ParsedEvent::Inception(p) = parse_event(&raw).unwrap() else {
unreachable!()
};
assert_eq!(p.sn, "0");
assert_eq!(p.keys.len(), 1);
assert_eq!(p.config, vec!["EO"]);
assert_eq!(p.anchors.len(), 1);
assert_eq!(p.said.span.len(), 44);
assert_eq!(
&raw[p.said.span.clone()],
p.said.value.as_bytes(),
"span must address the value bytes in raw"
);
assert_eq!(&raw[p.prefix.span.clone()], p.prefix.value.as_bytes());
}
#[test]
fn parse_inception_reads_all_icp_fields() {
let raw = probe_icp_bytes();
let p = parse_inception(&raw).unwrap();
assert!(matches!(p.threshold, ParsedTholder::Hex("1")));
assert!(matches!(p.next_threshold, ParsedTholder::Hex("1")));
assert_eq!(p.next_keys.len(), 1);
assert!(matches!(p.witness_threshold, ParsedCount::Hex("1")));
assert_eq!(p.witnesses.len(), 1);
}
#[test]
fn parse_rotation_reads_all_rot_fields() {
let raw = probe_rot_bytes();
let p = parse_rotation(&raw).unwrap();
assert_eq!(p.sn, "2");
assert_eq!(&raw[p.said.span.clone()], p.said.value.as_bytes());
assert!(!p.prefix.is_empty());
assert!(!p.prior.is_empty());
assert!(matches!(p.threshold, ParsedTholder::Hex("1")));
assert_eq!(p.keys.len(), 1);
assert!(matches!(p.next_threshold, ParsedTholder::Hex("1")));
assert_eq!(p.next_keys.len(), 1);
assert!(matches!(p.witness_threshold, ParsedCount::Hex("1")));
assert_eq!(p.witness_removals.len(), 1);
assert_eq!(p.witness_additions.len(), 1);
assert_eq!(p.anchors.len(), 1);
}
#[test]
fn parse_interaction_reads_all_ixn_fields() {
let raw = probe_ixn_bytes();
let p = parse_interaction(&raw).unwrap();
assert_eq!(p.sn, "3");
assert_eq!(&raw[p.said.span.clone()], p.said.value.as_bytes());
assert!(!p.prefix.is_empty());
assert!(!p.prior.is_empty());
assert!(p.anchors.is_empty());
}
#[test]
fn parse_delegated_inception_reads_icp_and_delegator() {
let raw = probe_dip_bytes();
let p = parse_delegated_inception(&raw).unwrap();
assert_eq!(p.icp.sn, "0");
assert!(!p.delegator.is_empty());
}
#[test]
fn parse_delegated_rotation_reads_rot_fields() {
let raw = probe_drt_bytes();
let p = parse_delegated_rotation(&raw).unwrap();
assert_eq!(p.sn, "2");
}
#[test]
fn parse_event_dispatches_every_ilk_variant() {
match parse_event(&probe_icp_bytes()).unwrap() {
ParsedEvent::Inception(p) => assert_eq!(p.sn, "0"),
other => unreachable!("expected Inception, got {other:?}"),
}
match parse_event(&probe_rot_bytes()).unwrap() {
ParsedEvent::Rotation(p) => assert_eq!(p.sn, "2"),
other => unreachable!("expected Rotation, got {other:?}"),
}
match parse_event(&probe_ixn_bytes()).unwrap() {
ParsedEvent::Interaction(p) => assert_eq!(p.sn, "3"),
other => unreachable!("expected Interaction, got {other:?}"),
}
match parse_event(&probe_dip_bytes()).unwrap() {
ParsedEvent::DelegatedInception(p) => assert_eq!(p.icp.sn, "0"),
other => unreachable!("expected DelegatedInception, got {other:?}"),
}
match parse_event(&probe_drt_bytes()).unwrap() {
ParsedEvent::DelegatedRotation(p) => assert_eq!(p.sn, "2"),
other => unreachable!("expected DelegatedRotation, got {other:?}"),
}
}
#[test]
fn per_ilk_entry_rejects_wrong_ilk() {
let raw = probe_ixn_bytes();
assert!(matches!(
parse_rotation(&raw),
Err(SerderError::NonCanonical {
expected: "rot",
..
})
));
}
#[test]
fn unknown_ilk_is_typed() {
let mut raw = probe_ixn_bytes();
let pos = raw.windows(5).position(|w| w == b"\"ixn\"").unwrap();
raw[pos + 1..pos + 4].copy_from_slice(b"xxx");
assert!(matches!(
parse_event(&raw),
Err(SerderError::UnknownIlk(ref s)) if s == "xxx"
));
}
#[test]
fn whitespace_with_consistent_size_is_non_canonical() {
let raw = probe_ixn_bytes();
let comma = raw.iter().position(|b| *b == b',').unwrap();
let mut padded = Vec::with_capacity(raw.len() + 1);
padded.extend_from_slice(&raw[..=comma]);
padded.push(b' ');
padded.extend_from_slice(&raw[comma + 1..]);
fix_size(&mut padded);
assert!(matches!(
parse_event(&padded),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn duplicate_field_is_non_canonical() {
let mut raw = probe_ixn_bytes();
let pos = raw.windows(5).position(|w| w == b",\"i\":").unwrap();
raw[pos..pos + 5].copy_from_slice(b",\"d\":");
assert!(matches!(
parse_event(&raw),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn reordered_fields_are_non_canonical() {
let mut raw = probe_ixn_bytes();
let s_pos = raw.windows(5).position(|w| w == b",\"s\":").unwrap();
let p_pos = raw.windows(5).position(|w| w == b",\"p\":").unwrap();
raw[s_pos + 2] = b'p';
raw[p_pos + 2] = b's';
assert!(matches!(
parse_event(&raw),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn escape_in_value_is_non_canonical() {
let raw = probe_ixn_bytes();
let pos = raw.windows(8).position(|w| w == b",\"s\":\"3\"").unwrap();
let mut mutated = Vec::with_capacity(raw.len() + 5);
mutated.extend_from_slice(&raw[..pos]);
mutated.extend_from_slice(b",\"s\":\"\\u0033\"");
mutated.extend_from_slice(&raw[pos + 8..]);
fix_size(&mut mutated);
assert!(matches!(
parse_event(&mutated),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn trailing_bytes_are_non_canonical() {
let mut raw = probe_ixn_bytes();
raw.push(b'X');
fix_size(&mut raw);
assert!(matches!(
parse_event(&raw),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn length_lie_is_still_invalid_version_string() {
let mut raw = probe_ixn_bytes();
raw.push(b'X');
assert!(matches!(
parse_event(&raw),
Err(SerderError::InvalidVersionString(_))
));
}
#[test]
fn every_strict_prefix_is_rejected_without_panicking() {
let raw = probe_icp_bytes();
for cut in 0..raw.len() {
assert!(
parse_event(&raw[..cut]).is_err(),
"truncation at {cut} must be rejected"
);
}
}
#[test]
fn multibyte_utf8_in_version_window_is_rejected_not_panicking() {
assert!(parse_event(b"{\"v\":\"KER\xC3\xA9AJSONAAAAAA_").is_err());
}
#[test]
fn wrong_first_byte_is_non_canonical() {
assert!(matches!(
parse_event(b"[\"v\":\"KERI10JSON000017_"),
Err(SerderError::NonCanonical { offset: 0, .. })
));
}
#[test]
fn oversized_ilk_is_rejected() {
let raw = probe_ixn_bytes();
let pos = raw.windows(5).position(|w| w == b"\"ixn\"").unwrap();
let mut mutated = Vec::with_capacity(raw.len() + 1);
mutated.extend_from_slice(&raw[..pos + 4]);
mutated.push(b'X');
mutated.extend_from_slice(&raw[pos + 4..]);
fix_size(&mut mutated);
assert!(matches!(
parse_event(&mutated),
Err(SerderError::UnknownIlk(ref s)) if s == "ixnX"
));
assert!(matches!(
parse_interaction(&mutated),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn delegator_field_on_icp_is_rejected() {
let mut raw = probe_dip_bytes();
let pos = raw.windows(5).position(|w| w == b"\"dip\"").unwrap();
raw[pos + 1..pos + 4].copy_from_slice(b"icp");
assert!(matches!(
parse_event(&raw),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn missing_delegator_on_dip_is_rejected() {
let mut raw = probe_icp_bytes();
let pos = raw.windows(5).position(|w| w == b"\"icp\"").unwrap();
raw[pos + 1..pos + 4].copy_from_slice(b"dip");
assert!(matches!(
parse_event(&raw),
Err(SerderError::NonCanonical { .. })
));
}
#[test]
fn corrupt_version_terminator_seam_is_rejected() {
let mut raw = probe_ixn_bytes();
raw[23] = b'X';
assert!(parse_event(&raw).is_err());
}
mod properties {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn scanner_never_panics(input in proptest::collection::vec(any::<u8>(), 0..64)) {
let _ = Scanner::new(&input).string();
let _ = Scanner::new(&input).integer();
let mut sc = Scanner::new(&input);
let _ = sc.expect("{\"v\":\"");
let _ = sc.finish();
let _ = string_array(&mut Scanner::new(&input));
let _ = tholder(&mut Scanner::new(&input));
let _ = count(&mut Scanner::new(&input));
let _ = seal(&mut Scanner::new(&input));
let _ = seal_array(&mut Scanner::new(&input));
let _ = parse_event(&input);
}
#[test]
fn accepted_string_span_addresses_value(input in proptest::collection::vec(any::<u8>(), 0..64)) {
if let Ok(s) = Scanner::new(&input).string() {
prop_assert_eq!(&input[s.span.clone()], s.value.as_bytes());
}
}
}
}
}