1use super::{
2 MatterPart,
3 code::{CesrCode, MatterCode},
4 error::{MatterBuildError, ParsingError, ValidationError},
5 matter::Matter,
6 sizage::{Sizage, SizeType},
7};
8use crate::b64::{charset::is_b64_url_safe_charset, decode_int, encode_binary};
9use alloc::borrow::Cow;
10#[cfg(feature = "alloc")]
11#[allow(
12 unused_imports,
13 reason = "alloc prelude items; subset used per cfg/feature combination"
14)]
15use alloc::{borrow::ToOwned, format, string::String, string::ToString, vec, vec::Vec};
16use base64::{Engine, decoded_len_estimate, engine::general_purpose as b64};
17use core::num::NonZeroUsize;
18
19pub trait MatterBuilderState {}
21
22pub struct Start {}
24
25impl MatterBuilderState for Start {}
26
27pub struct WithCode<C: CesrCode> {
29 code: C,
30}
31
32impl<C: CesrCode> MatterBuilderState for WithCode<C> {}
33
34pub struct WithSoft<'a, C: CesrCode> {
36 code: C,
37 soft: &'a str,
38}
39
40impl<C: CesrCode> MatterBuilderState for WithSoft<'_, C> {}
41
42pub struct WithRaw<'a, C: CesrCode> {
44 code: C,
45 raw: Cow<'a, [u8]>,
46}
47
48impl<C: CesrCode> MatterBuilderState for WithRaw<'_, C> {}
49
50pub struct WithRawAndSoft<'a, C: CesrCode> {
52 code: C,
53 raw: Cow<'a, [u8]>,
54 soft: &'a str,
55}
56
57impl<C: CesrCode> MatterBuilderState for WithRawAndSoft<'_, C> {}
58
59pub struct MatterBuilder<M>
61where
62 M: MatterBuilderState,
63{
64 state: M,
65}
66
67impl Default for MatterBuilder<Start> {
68 fn default() -> Self {
69 Self { state: Start {} }
70 }
71}
72
73impl MatterBuilder<Start> {
74 #[must_use]
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub const fn with_code<C: CesrCode>(self, code: C) -> MatterBuilder<WithCode<C>> {
82 MatterBuilder {
83 state: WithCode { code },
84 }
85 }
86
87 #[allow(
94 clippy::too_many_lines,
95 reason = "sequential parsing steps that are clearer together"
96 )]
97 pub fn from_qualified_base64<'a>(
98 self,
99 input: impl Into<Cow<'a, [u8]>>,
100 ) -> Result<Matter<'a, MatterCode>, MatterBuildError> {
101 let stream = input.into();
102 if stream.is_empty() {
103 return Err(MatterBuildError::from(ParsingError::EmptyStream));
104 }
105 let code = MatterCode::from_base64_stream(&stream)?;
106 let sizage = code.get_sizage();
110 let hs = sizage.hs();
111 let ss = sizage.ss();
112 let cs = hs + ss;
113 if stream.len() < cs {
114 return Err(MatterBuildError::from(ParsingError::StreamTooShort(
115 MatterPart::Soft,
116 )));
117 }
118 let soft_full = &stream[hs..cs];
119 let xs = sizage.xs();
120 let xtra = &soft_full[..xs];
121 let soft_tail = &soft_full[xs..];
122 if !xtra.iter().all(|&b| b == b'_') {
125 return Err(MatterBuildError::from(ParsingError::MalformedCode {
126 part: MatterPart::Xtra,
127 found: Matter::<MatterCode>::PAD.repeat(xs),
128 }));
129 }
130 let soft_tail_str = str::from_utf8(soft_tail)
133 .map_err(|err| MatterBuildError::from(ParsingError::InvalidUtf8(err)))?;
134 let fs = match sizage.fs() {
138 SizeType::Fixed(fixed) => usize::from(*fixed),
139 SizeType::Small | SizeType::Large => {
140 let size: usize = decode_int(soft_tail_str)
141 .map_err(|err| MatterBuildError::from(ParsingError::Conversion(err)))?;
142 compute_full_size(size, cs)?
143 }
144 };
145 if stream.len() < fs {
146 return Err(MatterBuildError::from(ValidationError::IncorrectRawSize {
147 code: code.to_string(),
148 expected: fs,
149 found: stream.len(),
150 }));
151 }
152 let trim = &stream[..fs];
153 let ps = cs % 4;
154
155 let paw = &trim[cs..];
156 let mut temp: Vec<u8> = Vec::with_capacity(ps + paw.len());
157 temp.resize(ps, b'A');
158 temp.extend_from_slice(paw);
159 let estimate = decoded_len_estimate(temp.len());
160 let mut buf: Vec<u8> = Vec::with_capacity(estimate);
161 b64::URL_SAFE
162 .decode_vec(temp, &mut buf)
163 .map_err(|err| MatterBuildError::from(ParsingError::Base64(err)))?;
164
165 if ps != 0 {
166 let pbs = 2 * ps;
167 let mask = (1u32 << pbs) - 1;
168 let mut pi: u32 = 0;
169 for &byte in buf.iter().take(ps) {
170 pi = (pi << 8) | u32::from(byte);
171 }
172 if (pi & mask) != 0 {
173 return Err(MatterBuildError::from(
174 ValidationError::NonCanonicalEncoding(MatterPart::PadBits),
175 ));
176 }
177 let ls = sizage.ls();
178 let Some(lead_bytes) = buf.get(ps..(ps + ls)) else {
179 return Err(MatterBuildError::from(
180 ValidationError::StructuralIntegrityError,
181 ));
182 };
183 if ls > 0 && lead_bytes.iter().any(|&b| b != 0) {
184 return Err(MatterBuildError::from(
185 ValidationError::NonCanonicalEncoding(MatterPart::LeadBytes),
186 ));
187 }
188 buf.drain(..(ps + ls));
189 } else {
190 let ls = sizage.ls();
191 if ls > 0 {
192 let Some(lead_bytes) = buf.get(..ls) else {
193 return Err(MatterBuildError::from(
194 ValidationError::StructuralIntegrityError,
195 ));
196 };
197 if lead_bytes.iter().any(|&b| b != 0) {
198 return Err(MatterBuildError::from(
199 ValidationError::NonCanonicalEncoding(MatterPart::LeadBytes),
200 ));
201 }
202 buf.drain(..ls);
203 }
204 }
205 let raw: Cow<'a, [u8]> = Cow::Owned(buf);
206
207 let soft_start = hs + xs;
208 let soft_end = cs;
209
210 let soft: Cow<'a, str> = match &stream {
211 Cow::Borrowed(b) => {
212 let s = str::from_utf8(&b[soft_start..soft_end])
213 .map_err(|err| MatterBuildError::from(ParsingError::InvalidUtf8(err)))?;
214 Cow::Borrowed(s)
215 }
216 Cow::Owned(v) => {
217 let s = str::from_utf8(&v[soft_start..soft_end])
218 .map_err(|err| MatterBuildError::from(ParsingError::InvalidUtf8(err)))?;
219 Cow::Owned(s.to_owned())
220 }
221 };
222
223 Ok(Matter::new(code, raw, soft))
224 }
225
226 #[allow(
233 clippy::too_many_lines,
234 reason = "sequential parsing steps that are clearer together"
235 )]
236 pub fn from_qualified_base2(
237 self,
238 stream: &[u8],
239 ) -> Result<Matter<'_, MatterCode>, MatterBuildError> {
240 if stream.is_empty() {
241 return Err(MatterBuildError::from(ParsingError::EmptyStream));
242 }
243 let code = MatterCode::from_stream(stream)?;
244 let hs = code.get_sizage().hs();
245 let ss = code.get_sizage().ss();
246 let cs = hs + ss;
247 let bcs = (cs * 3).div_ceil(4);
248 if stream.len() < bcs {
249 return Err(MatterBuildError::from(ParsingError::StreamTooShort(
250 MatterPart::Soft,
251 )));
252 }
253
254 let char_len = NonZeroUsize::new(cs)
255 .ok_or_else(|| MatterBuildError::from(ParsingError::EmptyStream))?;
256 let both = encode_binary(&stream[..bcs], char_len)
257 .map_err(|err| MatterBuildError::from(ParsingError::Conversion(err)))?;
258
259 let soft_full = &both[hs..cs];
260 let xs = code.get_sizage().xs();
261 let xtra = &soft_full[..xs];
262 let soft_tail = &soft_full[xs..];
263 if xtra != Matter::<MatterCode>::PAD.repeat(xs) {
264 return Err(MatterBuildError::from(ParsingError::MalformedCode {
265 part: MatterPart::Xtra,
266 found: Matter::<MatterCode>::PAD.repeat(xs),
267 }));
268 }
269 let fs = if let SizeType::Fixed(fixed) = code.get_sizage().fs() {
270 usize::from(*fixed)
271 } else {
272 let size: usize = decode_int(soft_tail)
273 .map_err(|err| MatterBuildError::from(ParsingError::Conversion(err)))?;
274 compute_full_size(size, cs)?
275 };
276 let bfs = compute_qb2_byte_size(fs)?;
277 if stream.len() < bfs {
278 return Err(MatterBuildError::from(ValidationError::IncorrectRawSize {
279 code: code.to_string(),
280 expected: bfs,
281 found: stream.len(),
282 }));
283 }
284 let trimmed = &stream[..bfs];
285 let ls = code.get_sizage().ls();
286 let lead_end = bcs
287 .checked_add(ls)
288 .ok_or_else(|| MatterBuildError::from(ValidationError::StructuralIntegrityError))?;
289 if lead_end > trimmed.len() {
290 return Err(MatterBuildError::from(ValidationError::IncorrectRawSize {
291 code: code.to_string(),
292 expected: lead_end,
293 found: trimmed.len(),
294 }));
295 }
296 let ps = cs % 4;
297 if ps != 0 {
298 #[allow(
299 clippy::as_conversions,
300 clippy::cast_possible_truncation,
301 reason = "2 * ps is always <= 6 which fits in u32"
302 )]
303 let pbs = (2 * ps) as u32;
304 let mut pi = trimmed[bcs - 1];
305 pi &= 2_u8.pow(pbs) - 1;
306 if pi != 0 {
307 return Err(MatterBuildError::from(
308 ValidationError::NonCanonicalEncoding(MatterPart::PadBits),
309 ));
310 }
311 }
312 let li = &trimmed[bcs..lead_end];
313 if ls > 0 && li.iter().any(|&b| b != 0) {
314 return Err(MatterBuildError::from(
315 ValidationError::NonCanonicalEncoding(MatterPart::LeadBytes),
316 ));
317 }
318 let raw = &trimmed[lead_end..];
319
320 if raw.len() != trimmed.len() - lead_end {
321 return Err(MatterBuildError::from(
322 ValidationError::StructuralIntegrityError,
323 ));
324 }
325
326 Ok(Matter::new(
327 code,
328 Cow::Borrowed(raw),
329 Cow::Owned(soft_tail.to_owned()),
330 ))
331 }
332}
333
334impl<C: CesrCode> MatterBuilder<WithCode<C>> {
335 pub fn with_raw<'a>(
341 self,
342 raw: impl Into<Cow<'a, [u8]>>,
343 ) -> Result<MatterBuilder<WithRaw<'a, C>>, ParsingError> {
344 let raw_bytes = raw.into();
345 if raw_bytes.is_empty() && !matches!(self.state.code.raw_size(), Ok(0)) {
350 return Err(ParsingError::EmptyStream);
351 }
352 Ok(MatterBuilder {
353 state: WithRaw {
354 code: self.state.code,
355 raw: raw_bytes,
356 },
357 })
358 }
359
360 pub const fn with_soft(
366 self,
367 soft: &str,
368 ) -> Result<MatterBuilder<WithSoft<'_, C>>, ParsingError> {
369 if soft.is_empty() {
370 return Err(ParsingError::EmptyStream);
371 }
372 let state = WithSoft {
373 code: self.state.code,
374 soft,
375 };
376 Ok(MatterBuilder { state })
377 }
378}
379
380impl<'a, C: CesrCode> MatterBuilder<WithRaw<'a, C>> {
381 pub fn build(self) -> Result<Matter<'a, C>, MatterBuildError> {
388 let WithRaw { code, raw: raw_val } = self.state;
389 let mc = code.to_matter_code();
390 let Sizage { ss, fs, .. } = mc.get_sizage();
391 match fs {
392 SizeType::Fixed(_) => {
393 if ss > 0 {
394 return Err(MatterBuildError::from(ValidationError::MissingSoft {
395 code: mc.to_string(),
396 }));
397 }
398 let raw_size = mc.raw_size()?;
399 let trimmed = validate_and_trim_raw(mc, raw_val, raw_size)?;
400 Ok(Matter::new(code, trimmed, Cow::from("")))
401 }
402 _ => Err(MatterBuildError::from(
403 ValidationError::InvalidSizingOperation(mc.to_string()),
404 )),
405 }
406 }
407
408 pub fn with_soft(
414 self,
415 soft: &'a str,
416 ) -> Result<MatterBuilder<WithRawAndSoft<'a, C>>, ParsingError> {
417 if soft.is_empty() {
418 return Err(ParsingError::EmptyStream);
419 }
420 let state = WithRawAndSoft {
421 code: self.state.code,
422 raw: self.state.raw,
423 soft,
424 };
425 Ok(MatterBuilder { state })
426 }
427}
428
429impl<'a, C: CesrCode> MatterBuilder<WithRawAndSoft<'a, C>> {
430 pub fn build(self) -> Result<Matter<'a, C>, MatterBuildError> {
437 let WithRawAndSoft {
438 soft,
439 code,
440 raw: raw_val,
441 } = self.state;
442 let mc = code.to_matter_code();
443 let Sizage { ss, fs, xs, .. } = mc.get_sizage();
444 match fs {
445 SizeType::Fixed(_) => {
446 if ss == 0 {
447 let raw_size = mc.raw_size()?;
448 let trimmed = validate_and_trim_raw(mc, raw_val, raw_size)?;
449 return Ok(Matter::new(code, trimmed, Cow::from("")));
450 }
451 let final_soft = extract_soft(mc, ss, xs, soft)?;
452 let raw_size = mc.raw_size()?;
453 let trimmed = validate_and_trim_raw(mc, raw_val, raw_size)?;
454 Ok(Matter::new(code, trimmed, Cow::Borrowed(final_soft)))
455 }
456 _ => Err(MatterBuildError::from(
457 ValidationError::InvalidSizingOperation(mc.to_string()),
458 )),
459 }
460 }
461}
462
463impl<'a, C: CesrCode> MatterBuilder<WithSoft<'a, C>> {
464 pub fn build(self) -> Result<Matter<'a, C>, ValidationError> {
471 let WithSoft { code, soft } = self.state;
472 let mc = code.to_matter_code();
473 let Sizage {
474 ss,
475 fs: size_type,
476 hs,
477 xs,
478 ..
479 } = mc.get_sizage();
480 match size_type {
481 SizeType::Fixed(fixed_size) if ss > 0 && fixed_size == (hs + ss) => {
482 let final_soft = extract_soft(mc, ss, xs, soft)?;
483 Ok(Matter::new(
484 code,
485 Cow::Borrowed(b""),
486 Cow::Borrowed(final_soft),
487 ))
488 }
489 SizeType::Fixed(_) => Err(ValidationError::InvalidSoftFormat {
490 code: mc.to_string(),
491 }),
492 _ => Err(ValidationError::UnknownMatterCode(mc.to_string())),
493 }
494 }
495}
496
497#[inline]
498#[allow(dead_code, reason = "utility kept for future encoding use")]
499const fn get_lead_size(raw_len: usize) -> usize {
500 (3 - (raw_len % 3)) % 3
501}
502
503#[inline]
504#[allow(dead_code, reason = "utility kept for future encoding use")]
505const fn get_size(raw_len: usize, lead_len: usize) -> usize {
506 (raw_len + lead_len) / 3
507}
508
509#[inline]
516fn compute_full_size(size: usize, cs: usize) -> Result<usize, ValidationError> {
517 size.checked_mul(4)
518 .and_then(|quad| quad.checked_add(cs))
519 .ok_or(ValidationError::SizeOverflow)
520}
521
522#[inline]
529fn compute_qb2_byte_size(fs: usize) -> Result<usize, ValidationError> {
530 fs.checked_mul(3)
531 .map(|tripled| tripled.div_ceil(4))
532 .ok_or(ValidationError::SizeOverflow)
533}
534
535#[inline]
536fn extract_soft(code: MatterCode, ss: u8, xs: u8, soft: &str) -> Result<&str, ValidationError> {
537 let expected_len = usize::from(ss - xs);
538
539 if soft.len() < expected_len {
540 return Err(ValidationError::IncorrectSoftLength {
541 code: code.to_string(),
542 expected: expected_len,
543 found: soft.len(),
544 });
545 }
546
547 let final_soft = &soft[..expected_len];
548
549 if !is_b64_url_safe_charset(final_soft.as_bytes()) {
550 return Err(ValidationError::InvalidSoftFormat {
551 code: code.to_string(),
552 });
553 }
554
555 Ok(final_soft)
556}
557
558#[inline]
559fn validate_and_trim_raw(
560 code: MatterCode,
561 raw: Cow<'_, [u8]>,
562 raw_size: usize,
563) -> Result<Cow<'_, [u8]>, ValidationError> {
564 if raw.len() < raw_size {
565 return Err(ValidationError::IncorrectRawSize {
566 found: raw.len(),
567 expected: raw_size,
568 code: code.to_string(),
569 });
570 }
571 if raw.len() == raw_size {
572 return Ok(raw);
573 }
574 match raw {
575 Cow::Borrowed(s) => Ok(Cow::Borrowed(&s[..raw_size])),
576 Cow::Owned(mut v) => {
577 v.truncate(raw_size);
578 Ok(Cow::Owned(v))
579 }
580 }
581}
582
583impl MatterCode {
584 pub fn frame_size(stream: &[u8]) -> Result<usize, MatterBuildError> {
590 let code = Self::from_base64_stream(stream)?;
591 code.frame_size_of(stream)
592 }
593
594 pub(crate) fn frame_size_of(self, stream: &[u8]) -> Result<usize, MatterBuildError> {
597 let sizage = self.get_sizage();
598 if let SizeType::Fixed(fixed) = sizage.fs() {
599 return Ok(usize::from(*fixed));
600 }
601 let hs = sizage.hs();
602 let ss = sizage.ss();
603 let cs = hs + ss;
604 if stream.len() < cs {
605 return Err(MatterBuildError::from(ParsingError::StreamTooShort(
606 MatterPart::Soft,
607 )));
608 }
609 let xs = sizage.xs();
610 let soft_tail = str::from_utf8(&stream[hs + xs..cs])
611 .map_err(|err| MatterBuildError::from(ParsingError::InvalidUtf8(err)))?;
612 let size: usize = decode_int(soft_tail)
613 .map_err(|err| MatterBuildError::from(ParsingError::Conversion(err)))?;
614 compute_full_size(size, cs).map_err(MatterBuildError::from)
615 }
616}
617
618#[cfg(test)]
619#[allow(clippy::panic, reason = "tests use panic via unwrap/assert macros")]
620mod tests {
621 use super::{
622 MatterBuildError, MatterBuilder, Start, ValidationError, compute_full_size,
623 compute_qb2_byte_size, validate_and_trim_raw,
624 };
625 use crate::core::matter::code::MatterCode;
626 use std::{format, string::String, vec, vec::Vec};
627
628 #[test]
629 fn qb64_lead_bytes_slice_does_not_panic_on_short_buffer() {
630 let err = MatterBuilder::new()
635 .from_qualified_base64(b"5BAA".as_slice())
636 .expect_err("`5BAA` must be rejected, not accepted");
637 assert!(
638 matches!(
639 err,
640 MatterBuildError::Validation(ValidationError::StructuralIntegrityError)
641 ),
642 "expected StructuralIntegrityError, got {err:?}"
643 );
644 }
645
646 #[test]
647 fn matter_frame_size_fixed_and_truncated() {
648 let full = String::from("B") + &"A".repeat(43);
650 assert_eq!(MatterCode::frame_size(full.as_bytes()).unwrap(), 44);
651 assert!(MatterCode::frame_size(b"").is_err()); assert!(MatterCode::frame_size(b"\x00\x00").is_err()); }
654
655 #[test]
656 fn qb64_fixed_code_rejects_non_utf8_soft() {
657 let bytes: &[u8] = &[b'X', 0xFF, 0xFF, 0xFF];
664 let err = MatterBuilder::new()
665 .from_qualified_base64(bytes)
666 .expect_err("non-UTF-8 soft on fixed Tag3 must be rejected");
667 assert!(
668 matches!(
669 err,
670 MatterBuildError::Parsing(crate::core::matter::error::ParsingError::InvalidUtf8(_))
671 ),
672 "expected Parsing(InvalidUtf8), got {err:?}"
673 );
674 }
675
676 #[test]
686 fn compute_full_size_rejects_overflow() {
687 let err = compute_full_size(usize::MAX / 2, 4)
689 .expect_err("size * 4 overflow must be a typed Err, not a panic or wrap");
690 assert_eq!(err, ValidationError::SizeOverflow);
691 }
692
693 #[test]
694 fn compute_full_size_rejects_add_overflow() {
695 let err = compute_full_size(usize::MAX / 4, 8)
697 .expect_err("+ cs overflow must be a typed Err, not a panic or wrap");
698 assert_eq!(err, ValidationError::SizeOverflow);
699 }
700
701 #[test]
702 fn compute_full_size_in_range_is_exact() {
703 assert_eq!(compute_full_size(10, 4), Ok(44));
704 assert_eq!(compute_full_size(0, 2), Ok(2));
705 let max_real = 64_usize.pow(4) - 1;
707 assert_eq!(compute_full_size(max_real, 4), Ok((max_real * 4) + 4));
708 }
709
710 #[test]
711 fn compute_qb2_byte_size_rejects_overflow() {
712 let err = compute_qb2_byte_size(usize::MAX / 2)
714 .expect_err("fs * 3 overflow must be a typed Err, not a panic or wrap");
715 assert_eq!(err, ValidationError::SizeOverflow);
716 }
717
718 #[test]
719 fn compute_qb2_byte_size_in_range_is_exact() {
720 assert_eq!(compute_qb2_byte_size(44), Ok(33));
722 assert_eq!(compute_qb2_byte_size(2), Ok(2));
724 }
725
726 #[test]
727 fn should_extract_raw_size_based() {
728 use alloc::borrow::Cow;
729 let code = MatterCode::Ed25519;
730 let raw = b"18923yjkahds7612378612983189237669jyasgdutgjashgjg";
731 let result = validate_and_trim_raw(code, Cow::Borrowed(&raw[..]), 44);
732 assert!(result.is_ok());
733 let raw_result = result.unwrap();
734 assert_eq!(&*raw_result, &raw[..44]);
735 }
736
737 #[test]
743 fn should_be_able_to_build_code_raw_matter() {
744 let code = MatterCode::Ed25519;
745 let raw = b"18923yjkahds7612378612983189237669jyasgdutgjashgjg";
746 let result = MatterBuilder::<Start>::new()
747 .with_code(code)
748 .with_raw(&raw[..])
749 .unwrap()
750 .build();
751
752 assert!(result.is_ok());
753 let matter = result.unwrap();
754 assert_eq!(matter.code(), &code);
755 assert_eq!(matter.raw(), &raw[..32]); }
757
758 #[test]
759 fn should_build_typed_matter_from_code_and_raw() {
760 use crate::core::matter::code::VerKeyCode;
761 let code = VerKeyCode::Ed25519;
762 let raw = &[0u8; 32];
763 let result = MatterBuilder::<Start>::new()
764 .with_code(code)
765 .with_raw(&raw[..])
766 .unwrap()
767 .build();
768 assert!(result.is_ok());
769 let matter = result.unwrap();
770 assert_eq!(*matter.code(), VerKeyCode::Ed25519);
771 }
772
773 #[test]
776 fn qb64_empty_stream_returns_parsing_error() {
777 let result = MatterBuilder::new().from_qualified_base64(b"");
778 assert!(result.is_err());
779 }
780
781 #[test]
782 fn qb2_empty_stream_returns_parsing_error() {
783 let result = MatterBuilder::new().from_qualified_base2(b"");
784 assert!(result.is_err());
785 }
786
787 #[test]
788 fn builder_with_raw_rejects_empty_raw() {
789 let result = MatterBuilder::new()
790 .with_code(MatterCode::Ed25519)
791 .with_raw(b"");
792 assert!(result.is_err());
793 assert_eq!(
794 result.err().unwrap(),
795 crate::core::matter::error::ParsingError::EmptyStream
796 );
797 }
798
799 #[test]
800 fn builder_with_soft_rejects_empty_soft() {
801 let result = MatterBuilder::new()
802 .with_code(MatterCode::Tag3)
803 .with_soft("");
804 assert!(result.is_err());
805 assert_eq!(
806 result.err().unwrap(),
807 crate::core::matter::error::ParsingError::EmptyStream
808 );
809 }
810
811 #[test]
814 fn builder_variable_code_in_with_raw_build_fails() {
815 let result = MatterBuilder::new()
817 .with_code(MatterCode::Bytes_L0)
818 .with_raw(b"abcdef")
819 .unwrap()
820 .build();
821 assert!(result.is_err());
822 }
823
824 #[test]
825 fn builder_rejects_short_raw_for_ed25519() {
826 let result = MatterBuilder::new()
828 .with_code(MatterCode::Ed25519)
829 .with_raw(&[0u8; 3])
830 .unwrap()
831 .build();
832 assert!(result.is_err());
833 }
834
835 #[test]
836 fn builder_missing_soft_for_special_code() {
837 let result = MatterBuilder::new()
839 .with_code(MatterCode::Tag3)
840 .with_raw(b"abc")
841 .unwrap()
842 .build();
843 assert!(result.is_err());
844 }
845
846 #[test]
849 fn qb64_non_zero_pad_bits_rejected() {
850 let bad_qb64 = b"B_AAY2RlZmdoaWprbG1ub3BxcnN0dXYwMTIzNDU2Nzg5";
854 let result = MatterBuilder::new().from_qualified_base64(bad_qb64);
855 assert!(result.is_err());
856 }
857
858 #[test]
859 fn qb64_non_zero_lead_bytes_tbd1_rejected() {
860 let bad_qb64 = b"2____2Fi";
863 let result = MatterBuilder::new().from_qualified_base64(bad_qb64);
864 assert!(result.is_err());
865 }
866
867 #[test]
868 fn qb64_non_zero_lead_bytes_tbd2_rejected() {
869 let bad_qb64 = b"3_____96";
872 let result = MatterBuilder::new().from_qualified_base64(bad_qb64);
873 assert!(result.is_err());
874 }
875
876 #[test]
877 fn qb2_stream_too_short_rejected() {
878 let truncated: &[u8] = &[0x04, 0x69, 0x4e];
881 let result = MatterBuilder::new().from_qualified_base2(truncated);
882 assert!(result.is_err());
883 }
884
885 #[test]
886 fn qb2_short_bfs_lead_does_not_panic() {
887 let crafted: &[u8] = &[
895 0xe4, 0x70, 0x00, 0x21, 0x58, 0xff, 0xcd, 0x77, 0x4c, 0xa5, 0x50, 0x69, 0xd5, 0x8f,
896 0x3e, 0x87, 0xd4, 0x00, 0xb9, 0xff, 0xf8, 0xcd, 0x94, 0x81, 0xb2, 0xfe, 0x3e, 0x45,
897 0x2f, 0x40, 0x31, 0x6c, 0xb0, 0x69, 0xe4, 0x43, 0x40, 0x7b, 0x70, 0x9c, 0x38, 0x0f,
898 0x00, 0xc3, 0x67, 0x41, 0x21, 0xfb, 0xee, 0xe8, 0x58, 0xee, 0x9e, 0x8c, 0xff, 0x88,
899 0xbd, 0xb1, 0xcd, 0xd0, 0x67, 0x4a, 0x77, 0xe2, 0xea, 0x14, 0x4c, 0x64, 0xb3, 0x8b,
900 0xa5, 0x28, 0xe9, 0xd7, 0x50, 0xc2, 0x07, 0x3b, 0x69, 0xa7, 0xad, 0xc1, 0xd5, 0x25,
901 0xde, 0xc9, 0x8f, 0x58, 0xf3, 0xe4, 0x3e, 0xe4, 0x74, 0x03, 0x87, 0x24, 0x7c, 0xa6,
902 0xd4, 0xd4, 0x18, 0x8c, 0x00, 0x2e, 0xaf, 0x17, 0xb9, 0x1f, 0x79, 0x7d, 0xff, 0x0f,
903 0x35, 0xb0, 0xf8, 0x19, 0x1b, 0x14, 0xcd, 0x25, 0x88, 0xc8, 0x94, 0xf6, 0x80, 0x52,
904 0x81, 0xc3, 0x05, 0xfd, 0xb2, 0xe4, 0xf9, 0x0c, 0xfe, 0xcd, 0x7a, 0x23,
905 ];
906 let err = MatterBuilder::new()
907 .from_qualified_base2(crafted)
908 .expect_err("crafted qb2 with bfs < bcs + ls must be rejected, not parsed");
909 let crate::core::matter::error::MatterBuildError::Validation(validation) = err else {
910 panic!("expected Validation variant, got {err:?}");
911 };
912 assert!(
913 matches!(
914 validation,
915 crate::core::matter::error::ValidationError::IncorrectRawSize { .. }
916 ),
917 "expected IncorrectRawSize, got {validation:?}"
918 );
919 }
920
921 #[test]
924 fn builder_raw_code_fixed_vectors() {
925 use crate::core::matter::test_vectors::FIXED_VECTORS;
926 use core::str::FromStr;
927
928 for (i, vector) in FIXED_VECTORS.iter().enumerate() {
929 if vector.raw.is_empty() {
932 continue;
933 }
934
935 let code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
936 panic!("FIXED_VECTORS[{i}]: unknown code_str '{}'", vector.code_str)
937 });
938
939 let matter = MatterBuilder::new()
940 .with_code(code)
941 .with_raw(vector.raw)
942 .unwrap_or_else(|e| {
943 panic!(
944 "FIXED_VECTORS[{i}] ({}): with_raw failed: {e:?}",
945 vector.rust_variant
946 )
947 })
948 .build()
949 .unwrap_or_else(|e| {
950 panic!(
951 "FIXED_VECTORS[{i}] ({}): build failed: {e:?}",
952 vector.rust_variant
953 )
954 });
955
956 assert_eq!(
957 matter.raw(),
958 vector.raw,
959 "FIXED_VECTORS[{i}] ({}): raw mismatch",
960 vector.rust_variant
961 );
962 assert_eq!(
963 matter.code(),
964 &code,
965 "FIXED_VECTORS[{i}] ({}): code mismatch",
966 vector.rust_variant
967 );
968 }
969 }
970
971 #[test]
974 fn builder_soft_only_tag_codes() {
975 use crate::core::matter::test_vectors::SPECIAL_VECTORS;
976 use core::str::FromStr;
977
978 let soft_only_variants = [
979 "Tag3", "Tag7", "Tag11", "Tag1", "Tag2", "Tag4", "Tag5", "Tag6", "Tag8", "Tag9",
980 "Tag10",
981 ];
982
983 for vector in SPECIAL_VECTORS {
984 if !soft_only_variants.contains(&vector.rust_variant) {
985 continue;
986 }
987
988 let code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
989 panic!(
990 "SPECIAL_VECTORS ({}): unknown code_str '{}'",
991 vector.rust_variant, vector.code_str
992 )
993 });
994
995 let matter = MatterBuilder::new()
996 .with_code(code)
997 .with_soft(vector.soft)
998 .unwrap_or_else(|e| {
999 panic!(
1000 "SPECIAL_VECTORS ({}): with_soft failed: {e:?}",
1001 vector.rust_variant
1002 )
1003 })
1004 .build()
1005 .unwrap_or_else(|e| {
1006 panic!(
1007 "SPECIAL_VECTORS ({}): build failed: {e:?}",
1008 vector.rust_variant
1009 )
1010 });
1011
1012 assert_eq!(
1013 matter.soft(),
1014 vector.soft,
1015 "SPECIAL_VECTORS ({}): soft mismatch",
1016 vector.rust_variant
1017 );
1018 assert!(
1019 matter.raw().is_empty(),
1020 "SPECIAL_VECTORS ({}): expected empty raw for soft-only code",
1021 vector.rust_variant
1022 );
1023 assert_eq!(
1024 matter.code(),
1025 &code,
1026 "SPECIAL_VECTORS ({}): code mismatch",
1027 vector.rust_variant
1028 );
1029 }
1030 }
1031
1032 #[test]
1035 fn builder_raw_and_soft_special_vectors() {
1036 use crate::core::matter::test_vectors::SPECIAL_VECTORS;
1037 use core::str::FromStr;
1038
1039 let raw_and_soft_variants = [
1040 "GramHeadNeck",
1041 "GramHead",
1042 "GramHeadAIDNeck",
1043 "GramHeadAID",
1044 "TBD0S",
1045 "TBD1S",
1046 "TBD2S",
1047 ];
1048
1049 for vector in SPECIAL_VECTORS {
1050 if !raw_and_soft_variants.contains(&vector.rust_variant) {
1051 continue;
1052 }
1053 assert!(
1054 !vector.raw.is_empty(),
1055 "SPECIAL_VECTORS ({}): expected non-empty raw for raw+soft code",
1056 vector.rust_variant
1057 );
1058
1059 let code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1060 panic!(
1061 "SPECIAL_VECTORS ({}): unknown code_str '{}'",
1062 vector.rust_variant, vector.code_str
1063 )
1064 });
1065
1066 let matter = MatterBuilder::new()
1067 .with_code(code)
1068 .with_raw(vector.raw)
1069 .unwrap_or_else(|e| {
1070 panic!(
1071 "SPECIAL_VECTORS ({}): with_raw failed: {e:?}",
1072 vector.rust_variant
1073 )
1074 })
1075 .with_soft(vector.soft)
1076 .unwrap_or_else(|e| {
1077 panic!(
1078 "SPECIAL_VECTORS ({}): with_soft failed: {e:?}",
1079 vector.rust_variant
1080 )
1081 })
1082 .build()
1083 .unwrap_or_else(|e| {
1084 panic!(
1085 "SPECIAL_VECTORS ({}): build failed: {e:?}",
1086 vector.rust_variant
1087 )
1088 });
1089
1090 assert_eq!(
1091 matter.raw(),
1092 vector.raw,
1093 "SPECIAL_VECTORS ({}): raw mismatch",
1094 vector.rust_variant
1095 );
1096 assert_eq!(
1097 matter.soft(),
1098 vector.soft,
1099 "SPECIAL_VECTORS ({}): soft mismatch",
1100 vector.rust_variant
1101 );
1102 assert_eq!(
1103 matter.code(),
1104 &code,
1105 "SPECIAL_VECTORS ({}): code mismatch",
1106 vector.rust_variant
1107 );
1108 }
1109 }
1110
1111 #[test]
1114 fn qb64_round_trip_fixed_vectors() {
1115 use crate::core::matter::test_vectors::FIXED_VECTORS;
1116 use core::str::FromStr;
1117
1118 for (i, vector) in FIXED_VECTORS.iter().enumerate() {
1119 let matter = MatterBuilder::new()
1120 .from_qualified_base64(vector.qb64.as_bytes())
1121 .unwrap_or_else(|e| {
1122 panic!(
1123 "FIXED_VECTORS[{i}] ({}): QB64 parse failed: {e:?}",
1124 vector.rust_variant
1125 )
1126 });
1127
1128 assert_eq!(
1129 matter.raw(),
1130 vector.raw,
1131 "FIXED_VECTORS[{i}] ({}): raw mismatch from QB64",
1132 vector.rust_variant
1133 );
1134 assert_eq!(
1135 matter.soft(),
1136 vector.soft,
1137 "FIXED_VECTORS[{i}] ({}): soft mismatch from QB64",
1138 vector.rust_variant
1139 );
1140
1141 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1142 panic!("FIXED_VECTORS[{i}]: unknown code_str '{}'", vector.code_str)
1143 });
1144 assert_eq!(
1145 matter.code(),
1146 &expected_code,
1147 "FIXED_VECTORS[{i}] ({}): code mismatch from QB64",
1148 vector.rust_variant
1149 );
1150 }
1151 }
1152
1153 #[test]
1154 fn qb64_round_trip_special_vectors() {
1155 use crate::core::matter::test_vectors::SPECIAL_VECTORS;
1156 use core::str::FromStr;
1157
1158 for (i, vector) in SPECIAL_VECTORS.iter().enumerate() {
1159 let matter = MatterBuilder::new()
1160 .from_qualified_base64(vector.qb64.as_bytes())
1161 .unwrap_or_else(|e| {
1162 panic!(
1163 "SPECIAL_VECTORS[{i}] ({}): QB64 parse failed: {e:?}",
1164 vector.rust_variant
1165 )
1166 });
1167
1168 assert_eq!(
1169 matter.raw(),
1170 vector.raw,
1171 "SPECIAL_VECTORS[{i}] ({}): raw mismatch from QB64",
1172 vector.rust_variant
1173 );
1174 assert_eq!(
1175 matter.soft(),
1176 vector.soft,
1177 "SPECIAL_VECTORS[{i}] ({}): soft mismatch from QB64",
1178 vector.rust_variant
1179 );
1180
1181 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1182 panic!(
1183 "SPECIAL_VECTORS[{i}]: unknown code_str '{}'",
1184 vector.code_str
1185 )
1186 });
1187 assert_eq!(
1188 matter.code(),
1189 &expected_code,
1190 "SPECIAL_VECTORS[{i}] ({}): code mismatch from QB64",
1191 vector.rust_variant
1192 );
1193 }
1194 }
1195
1196 #[test]
1197 fn qb64_round_trip_variable_vectors() {
1198 use crate::core::matter::test_vectors::VARIABLE_VECTORS;
1199 use core::str::FromStr;
1200
1201 for (i, vector) in VARIABLE_VECTORS.iter().enumerate() {
1202 let matter = MatterBuilder::new()
1203 .from_qualified_base64(vector.qb64.as_bytes())
1204 .unwrap_or_else(|e| {
1205 panic!(
1206 "VARIABLE_VECTORS[{i}] ({}): QB64 parse failed: {e:?}",
1207 vector.rust_variant
1208 )
1209 });
1210
1211 assert_eq!(
1212 matter.raw(),
1213 vector.raw,
1214 "VARIABLE_VECTORS[{i}] ({}): raw mismatch from QB64",
1215 vector.rust_variant
1216 );
1217 assert_eq!(
1218 matter.soft(),
1219 vector.soft,
1220 "VARIABLE_VECTORS[{i}] ({}): soft mismatch from QB64",
1221 vector.rust_variant
1222 );
1223
1224 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1225 panic!(
1226 "VARIABLE_VECTORS[{i}]: unknown code_str '{}'",
1227 vector.code_str
1228 )
1229 });
1230 assert_eq!(
1231 matter.code(),
1232 &expected_code,
1233 "VARIABLE_VECTORS[{i}] ({}): code mismatch from QB64",
1234 vector.rust_variant
1235 );
1236 }
1237 }
1238
1239 #[test]
1242 fn qb2_round_trip_fixed_vectors() {
1243 use crate::core::matter::test_vectors::FIXED_VECTORS;
1244 use core::str::FromStr;
1245
1246 for (i, vector) in FIXED_VECTORS.iter().enumerate() {
1247 let matter = MatterBuilder::new()
1248 .from_qualified_base2(vector.qb2)
1249 .unwrap_or_else(|e| {
1250 panic!(
1251 "FIXED_VECTORS[{i}] ({}): QB2 parse failed: {e:?}",
1252 vector.rust_variant
1253 )
1254 });
1255
1256 assert_eq!(
1257 matter.raw(),
1258 vector.raw,
1259 "FIXED_VECTORS[{i}] ({}): raw mismatch from QB2",
1260 vector.rust_variant
1261 );
1262
1263 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1264 panic!("FIXED_VECTORS[{i}]: unknown code_str '{}'", vector.code_str)
1265 });
1266 assert_eq!(
1267 matter.code(),
1268 &expected_code,
1269 "FIXED_VECTORS[{i}] ({}): code mismatch from QB2",
1270 vector.rust_variant
1271 );
1272 }
1273 }
1274
1275 #[test]
1276 fn qb2_handles_ls_gt0_and_short_bfs_codes() {
1277 use crate::core::matter::test_vectors::FIXED_VECTORS;
1281 use core::str::FromStr;
1282
1283 let ls_gt0_vectors: Vec<_> = FIXED_VECTORS.iter().filter(|v| v.ls > 0).collect();
1285 assert!(
1286 !ls_gt0_vectors.is_empty(),
1287 "Expected at least one fixed vector with ls > 0"
1288 );
1289 for vector in &ls_gt0_vectors {
1290 let matter = MatterBuilder::new()
1291 .from_qualified_base2(vector.qb2)
1292 .unwrap_or_else(|e| {
1293 panic!(
1294 "FIXED ({}) with ls={}: QB2 parse failed: {e:?}",
1295 vector.rust_variant, vector.ls
1296 )
1297 });
1298 assert_eq!(
1299 matter.raw(),
1300 vector.raw,
1301 "FIXED ({}) with ls={}: raw mismatch",
1302 vector.rust_variant,
1303 vector.ls
1304 );
1305 }
1306
1307 let short_qb2_vectors: Vec<_> = FIXED_VECTORS
1309 .iter()
1310 .filter(|v| v.ls == 0 && v.qb2.len() < usize::from(v.hs))
1311 .collect();
1312 assert!(
1313 !short_qb2_vectors.is_empty(),
1314 "Expected at least one fixed vector with bfs < hs"
1315 );
1316 for vector in &short_qb2_vectors {
1317 let matter = MatterBuilder::new()
1318 .from_qualified_base2(vector.qb2)
1319 .unwrap_or_else(|e| {
1320 panic!(
1321 "FIXED ({}) with bfs < hs: QB2 parse failed: {e:?}",
1322 vector.rust_variant
1323 )
1324 });
1325 let expected_code = MatterCode::from_str(vector.code_str).unwrap();
1326 assert_eq!(
1327 matter.code(),
1328 &expected_code,
1329 "FIXED ({}) with bfs < hs: code mismatch",
1330 vector.rust_variant
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 fn qb2_round_trip_special_vectors() {
1337 use crate::core::matter::test_vectors::SPECIAL_VECTORS;
1338 use core::str::FromStr;
1339
1340 for (i, vector) in SPECIAL_VECTORS.iter().enumerate() {
1341 let matter = MatterBuilder::new()
1342 .from_qualified_base2(vector.qb2)
1343 .unwrap_or_else(|e| {
1344 panic!(
1345 "SPECIAL_VECTORS[{i}] ({}): QB2 parse failed: {e:?}",
1346 vector.rust_variant
1347 )
1348 });
1349
1350 assert_eq!(
1351 matter.raw(),
1352 vector.raw,
1353 "SPECIAL_VECTORS[{i}] ({}): raw mismatch from QB2",
1354 vector.rust_variant
1355 );
1356
1357 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1358 panic!(
1359 "SPECIAL_VECTORS[{i}]: unknown code_str '{}'",
1360 vector.code_str
1361 )
1362 });
1363 assert_eq!(
1364 matter.code(),
1365 &expected_code,
1366 "SPECIAL_VECTORS[{i}] ({}): code mismatch from QB2",
1367 vector.rust_variant
1368 );
1369 }
1370 }
1371
1372 #[test]
1373 fn qb2_round_trip_variable_vectors() {
1374 use crate::core::matter::test_vectors::VARIABLE_VECTORS;
1375 use core::str::FromStr;
1376
1377 for (i, vector) in VARIABLE_VECTORS.iter().enumerate() {
1378 let matter = MatterBuilder::new()
1379 .from_qualified_base2(vector.qb2)
1380 .unwrap_or_else(|e| {
1381 panic!(
1382 "VARIABLE_VECTORS[{i}] ({}): QB2 parse failed: {e:?}",
1383 vector.rust_variant
1384 )
1385 });
1386
1387 assert_eq!(
1388 matter.raw(),
1389 vector.raw,
1390 "VARIABLE_VECTORS[{i}] ({}): raw mismatch from QB2",
1391 vector.rust_variant
1392 );
1393
1394 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1395 panic!(
1396 "VARIABLE_VECTORS[{i}]: unknown code_str '{}'",
1397 vector.code_str
1398 )
1399 });
1400 assert_eq!(
1401 matter.code(),
1402 &expected_code,
1403 "VARIABLE_VECTORS[{i}] ({}): code mismatch from QB2",
1404 vector.rust_variant
1405 );
1406 }
1407 }
1408
1409 #[test]
1412 fn all_fixed_vectors_qb64_and_qb2_produce_identical_matter() {
1413 use crate::core::matter::test_vectors::FIXED_VECTORS;
1414
1415 for (i, v) in FIXED_VECTORS.iter().enumerate() {
1416 let from_qb64 = MatterBuilder::new()
1417 .from_qualified_base64(v.qb64.as_bytes())
1418 .unwrap_or_else(|e| {
1419 panic!("FIXED[{i}] ({}): QB64 parse failed: {e:?}", v.rust_variant)
1420 });
1421 let from_qb2 = MatterBuilder::new()
1422 .from_qualified_base2(v.qb2)
1423 .unwrap_or_else(|e| {
1424 panic!("FIXED[{i}] ({}): QB2 parse failed: {e:?}", v.rust_variant)
1425 });
1426
1427 assert_eq!(
1428 from_qb64.code(),
1429 from_qb2.code(),
1430 "FIXED[{i}] ({}): code mismatch between QB64 and QB2",
1431 v.rust_variant
1432 );
1433 assert_eq!(
1434 from_qb64.raw(),
1435 from_qb2.raw(),
1436 "FIXED[{i}] ({}): raw mismatch between QB64 and QB2",
1437 v.rust_variant
1438 );
1439 assert_eq!(
1440 from_qb64.soft(),
1441 from_qb2.soft(),
1442 "FIXED[{i}] ({}): soft mismatch between QB64 and QB2",
1443 v.rust_variant
1444 );
1445 }
1446 }
1447
1448 #[test]
1449 fn all_special_vectors_qb64_and_qb2_produce_identical_matter() {
1450 use crate::core::matter::test_vectors::SPECIAL_VECTORS;
1451
1452 for (i, v) in SPECIAL_VECTORS.iter().enumerate() {
1453 let from_qb64 = MatterBuilder::new()
1454 .from_qualified_base64(v.qb64.as_bytes())
1455 .unwrap_or_else(|e| {
1456 panic!(
1457 "SPECIAL[{i}] ({}): QB64 parse failed: {e:?}",
1458 v.rust_variant
1459 )
1460 });
1461 let from_qb2 = MatterBuilder::new()
1462 .from_qualified_base2(v.qb2)
1463 .unwrap_or_else(|e| {
1464 panic!("SPECIAL[{i}] ({}): QB2 parse failed: {e:?}", v.rust_variant)
1465 });
1466
1467 assert_eq!(
1468 from_qb64.code(),
1469 from_qb2.code(),
1470 "SPECIAL[{i}] ({}): code mismatch between QB64 and QB2",
1471 v.rust_variant
1472 );
1473 assert_eq!(
1474 from_qb64.raw(),
1475 from_qb2.raw(),
1476 "SPECIAL[{i}] ({}): raw mismatch between QB64 and QB2",
1477 v.rust_variant
1478 );
1479 assert_eq!(
1480 from_qb64.soft(),
1481 from_qb2.soft(),
1482 "SPECIAL[{i}] ({}): soft mismatch between QB64 and QB2",
1483 v.rust_variant
1484 );
1485 }
1486 }
1487
1488 #[test]
1489 fn all_variable_vectors_qb64_and_qb2_produce_identical_matter() {
1490 use crate::core::matter::test_vectors::VARIABLE_VECTORS;
1491
1492 for (i, v) in VARIABLE_VECTORS.iter().enumerate() {
1493 let from_qb64 = MatterBuilder::new()
1494 .from_qualified_base64(v.qb64.as_bytes())
1495 .unwrap_or_else(|e| {
1496 panic!(
1497 "VARIABLE[{i}] ({}): QB64 parse failed: {e:?}",
1498 v.rust_variant
1499 )
1500 });
1501 let from_qb2 = MatterBuilder::new()
1502 .from_qualified_base2(v.qb2)
1503 .unwrap_or_else(|e| {
1504 panic!(
1505 "VARIABLE[{i}] ({}): QB2 parse failed: {e:?}",
1506 v.rust_variant
1507 )
1508 });
1509
1510 assert_eq!(
1511 from_qb64.code(),
1512 from_qb2.code(),
1513 "VARIABLE[{i}] ({}): code mismatch between QB64 and QB2",
1514 v.rust_variant
1515 );
1516 assert_eq!(
1517 from_qb64.raw(),
1518 from_qb2.raw(),
1519 "VARIABLE[{i}] ({}): raw mismatch between QB64 and QB2",
1520 v.rust_variant
1521 );
1522 assert_eq!(
1523 from_qb64.soft(),
1524 from_qb2.soft(),
1525 "VARIABLE[{i}] ({}): soft mismatch between QB64 and QB2",
1526 v.rust_variant
1527 );
1528 }
1529 }
1530
1531 #[test]
1534 fn variable_size_qb64_round_trip_preserves_soft_size_encoding() {
1535 use crate::core::matter::test_vectors::VARIABLE_VECTORS;
1536 use core::str::FromStr;
1537
1538 for (i, vector) in VARIABLE_VECTORS.iter().enumerate() {
1539 let matter = MatterBuilder::new()
1540 .from_qualified_base64(vector.qb64.as_bytes())
1541 .unwrap_or_else(|e| {
1542 panic!(
1543 "VARIABLE_VECTORS[{i}] ({}): QB64 parse failed: {e:?}",
1544 vector.rust_variant
1545 )
1546 });
1547
1548 assert_eq!(
1550 matter.raw(),
1551 vector.raw,
1552 "VARIABLE_VECTORS[{i}] ({}): raw mismatch",
1553 vector.rust_variant
1554 );
1555
1556 assert_eq!(
1558 matter.soft(),
1559 vector.soft,
1560 "VARIABLE_VECTORS[{i}] ({}): soft (size encoding) mismatch",
1561 vector.rust_variant
1562 );
1563
1564 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1566 panic!(
1567 "VARIABLE_VECTORS[{i}]: unknown code_str '{}'",
1568 vector.code_str
1569 )
1570 });
1571 assert_eq!(
1572 matter.code(),
1573 &expected_code,
1574 "VARIABLE_VECTORS[{i}] ({}): code mismatch after promotion",
1575 vector.rust_variant
1576 );
1577
1578 assert!(
1580 vector.fs.is_none(),
1581 "VARIABLE_VECTORS[{i}] ({}): expected variable-size (fs=None)",
1582 vector.rust_variant
1583 );
1584 }
1585 }
1586
1587 #[test]
1588 fn variable_size_qb2_round_trip_preserves_raw_and_code() {
1589 use crate::core::matter::test_vectors::VARIABLE_VECTORS;
1590 use core::str::FromStr;
1591
1592 for (i, vector) in VARIABLE_VECTORS.iter().enumerate() {
1593 let matter = MatterBuilder::new()
1594 .from_qualified_base2(vector.qb2)
1595 .unwrap_or_else(|e| {
1596 panic!(
1597 "VARIABLE_VECTORS[{i}] ({}): QB2 parse failed: {e:?}",
1598 vector.rust_variant
1599 )
1600 });
1601
1602 assert_eq!(
1603 matter.raw(),
1604 vector.raw,
1605 "VARIABLE_VECTORS[{i}] ({}): raw mismatch from QB2",
1606 vector.rust_variant
1607 );
1608
1609 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1610 panic!(
1611 "VARIABLE_VECTORS[{i}]: unknown code_str '{}'",
1612 vector.code_str
1613 )
1614 });
1615 assert_eq!(
1616 matter.code(),
1617 &expected_code,
1618 "VARIABLE_VECTORS[{i}] ({}): code mismatch from QB2",
1619 vector.rust_variant
1620 );
1621 }
1622 }
1623
1624 #[test]
1627 fn boundary_vectors_qb64_round_trip() {
1628 use crate::core::matter::test_vectors_boundary::BOUNDARY_VECTORS;
1629 use core::str::FromStr;
1630
1631 for (i, vector) in BOUNDARY_VECTORS.iter().enumerate() {
1632 let matter = MatterBuilder::new()
1633 .from_qualified_base64(vector.qb64.as_bytes())
1634 .unwrap_or_else(|e| {
1635 panic!(
1636 "BOUNDARY_VECTORS[{i}] ({}): QB64 parse failed: {e:?}",
1637 vector.rust_variant
1638 )
1639 });
1640
1641 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1642 panic!(
1643 "BOUNDARY_VECTORS[{i}]: unknown code_str '{}'",
1644 vector.code_str
1645 )
1646 });
1647 assert_eq!(
1648 matter.code(),
1649 &expected_code,
1650 "BOUNDARY_VECTORS[{i}] ({}): code mismatch from QB64",
1651 vector.rust_variant
1652 );
1653 assert_eq!(
1654 matter.raw(),
1655 vector.raw,
1656 "BOUNDARY_VECTORS[{i}] ({}): raw mismatch from QB64",
1657 vector.rust_variant
1658 );
1659 assert_eq!(
1660 matter.soft(),
1661 vector.soft,
1662 "BOUNDARY_VECTORS[{i}] ({}): soft mismatch from QB64",
1663 vector.rust_variant
1664 );
1665 }
1666 }
1667
1668 #[test]
1669 fn boundary_vectors_qb2_round_trip() {
1670 use crate::core::matter::test_vectors_boundary::BOUNDARY_VECTORS;
1671 use core::str::FromStr;
1672
1673 for (i, vector) in BOUNDARY_VECTORS.iter().enumerate() {
1674 let matter = MatterBuilder::new()
1675 .from_qualified_base2(vector.qb2)
1676 .unwrap_or_else(|e| {
1677 panic!(
1678 "BOUNDARY_VECTORS[{i}] ({}): QB2 parse failed: {e:?}",
1679 vector.rust_variant
1680 )
1681 });
1682
1683 let expected_code = MatterCode::from_str(vector.code_str).unwrap_or_else(|_| {
1684 panic!(
1685 "BOUNDARY_VECTORS[{i}]: unknown code_str '{}'",
1686 vector.code_str
1687 )
1688 });
1689 assert_eq!(
1690 matter.code(),
1691 &expected_code,
1692 "BOUNDARY_VECTORS[{i}] ({}): code mismatch from QB2",
1693 vector.rust_variant
1694 );
1695 assert_eq!(
1696 matter.raw(),
1697 vector.raw,
1698 "BOUNDARY_VECTORS[{i}] ({}): raw mismatch from QB2",
1699 vector.rust_variant
1700 );
1701 }
1702 }
1703
1704 #[test]
1705 fn boundary_vectors_qb64_and_qb2_produce_identical_matter() {
1706 use crate::core::matter::test_vectors_boundary::BOUNDARY_VECTORS;
1707
1708 for (i, vector) in BOUNDARY_VECTORS.iter().enumerate() {
1709 let from_qb64 = MatterBuilder::new()
1710 .from_qualified_base64(vector.qb64.as_bytes())
1711 .unwrap_or_else(|e| {
1712 panic!(
1713 "BOUNDARY_VECTORS[{i}] ({}): QB64 parse failed: {e:?}",
1714 vector.rust_variant
1715 )
1716 });
1717 let from_qb2 = MatterBuilder::new()
1718 .from_qualified_base2(vector.qb2)
1719 .unwrap_or_else(|e| {
1720 panic!(
1721 "BOUNDARY_VECTORS[{i}] ({}): QB2 parse failed: {e:?}",
1722 vector.rust_variant
1723 )
1724 });
1725
1726 assert_eq!(
1727 from_qb64.code(),
1728 from_qb2.code(),
1729 "BOUNDARY_VECTORS[{i}] ({}): code mismatch between QB64 and QB2",
1730 vector.rust_variant
1731 );
1732 assert_eq!(
1733 from_qb64.raw(),
1734 from_qb2.raw(),
1735 "BOUNDARY_VECTORS[{i}] ({}): raw mismatch between QB64 and QB2",
1736 vector.rust_variant
1737 );
1738 assert_eq!(
1739 from_qb64.soft(),
1740 from_qb2.soft(),
1741 "BOUNDARY_VECTORS[{i}] ({}): soft mismatch between QB64 and QB2",
1742 vector.rust_variant
1743 );
1744 }
1745 }
1746
1747 #[test]
1748 fn boundary_vector_code_promotion_is_correct() {
1749 use crate::core::matter::test_vectors_boundary::BOUNDARY_VECTORS;
1750
1751 assert_eq!(
1753 BOUNDARY_VECTORS[0].code_str, "4A",
1754 "12285-byte vector should use Small code '4A'"
1755 );
1756 assert_eq!(BOUNDARY_VECTORS[0].ss, 2, "Small code should have ss=2");
1757
1758 assert_eq!(
1760 BOUNDARY_VECTORS[1].code_str, "7AAA",
1761 "12288-byte vector should promote to Big code '7AAA'"
1762 );
1763 assert_eq!(BOUNDARY_VECTORS[1].ss, 4, "Big code should have ss=4");
1764
1765 assert_eq!(
1767 BOUNDARY_VECTORS[2].code_str, "7AAA",
1768 "12291-byte vector should use Big code '7AAA'"
1769 );
1770 assert_eq!(BOUNDARY_VECTORS[2].ss, 4, "Big code should have ss=4");
1771 }
1772
1773 #[test]
1778 fn qb2_parsing_rejects_single_byte() {
1779 let result = MatterBuilder::new().from_qualified_base2(&[0x00]);
1782 assert!(result.is_err());
1783 }
1784
1785 #[test]
1786 fn qb2_parsing_rejects_two_bytes() {
1787 let result = MatterBuilder::new().from_qualified_base2(&[0x00, 0x00]);
1791 assert!(result.is_err());
1792 }
1793
1794 #[test]
1795 fn qb2_parsing_rejects_truncated_2char_code() {
1796 let result = MatterBuilder::new().from_qualified_base2(&[0xD0]);
1801 assert!(result.is_err());
1802 }
1803
1804 #[test]
1805 fn qb2_parsing_rejects_truncated_4char_code() {
1806 let result = MatterBuilder::new().from_qualified_base2(&[0xD4, 0x00]);
1810 assert!(result.is_err());
1811 }
1812
1813 #[test]
1814 fn qb2_parsing_rejects_header_only_no_payload() {
1815 let result = MatterBuilder::new().from_qualified_base2(&[0x00]);
1820 assert!(result.is_err());
1821 }
1822
1823 #[test]
1824 fn qb2_all_0xff_bytes_rejected() {
1825 let all_ff = [0xFF; 64];
1828 let result = MatterBuilder::new().from_qualified_base2(&all_ff);
1829 assert!(result.is_err());
1830 }
1831
1832 #[test]
1833 fn qb2_high_bit_patterns_do_not_panic() {
1834 let patterns: Vec<Vec<u8>> = vec![
1837 vec![0x80; 4],
1838 vec![0xC0; 4],
1839 vec![0xE0; 4],
1840 vec![0xF0; 4],
1841 vec![0xFE; 4],
1842 vec![0x80, 0x00, 0x00, 0x00],
1843 vec![0xFF, 0x00, 0xFF, 0x00],
1844 ];
1845 for pattern in &patterns {
1846 let _ = MatterBuilder::new().from_qualified_base2(pattern);
1848 }
1849 }
1850
1851 mod prop {
1854 use super::*;
1855 use proptest::prelude::*;
1856
1857 macro_rules! proptest_builder_raw {
1861 ($name:ident, $code:expr, $raw_size:expr) => {
1862 proptest! {
1863 #[test]
1864 fn $name(
1865 raw_bytes in proptest::collection::vec(any::<u8>(), $raw_size..=$raw_size + 64)
1866 ) {
1867 let code = $code;
1868 let matter = MatterBuilder::new()
1869 .with_code(code)
1870 .with_raw(&raw_bytes)
1871 .unwrap()
1872 .build()
1873 .unwrap();
1874
1875 prop_assert_eq!(matter.raw(), &raw_bytes[..$raw_size]);
1876 prop_assert_eq!(matter.code(), &code);
1877 }
1878 }
1879 };
1880 }
1881
1882 proptest_builder_raw!(random_raw_ed25519_seed, MatterCode::Ed25519Seed, 32);
1884 proptest_builder_raw!(random_raw_blake3_256, MatterCode::Blake3_256, 32);
1885 proptest_builder_raw!(random_raw_short, MatterCode::Short, 2);
1886
1887 proptest_builder_raw!(random_raw_salt128, MatterCode::Salt128, 16);
1889 proptest_builder_raw!(random_raw_ed25519_sig, MatterCode::Ed25519Sig, 64);
1890 proptest_builder_raw!(random_raw_long, MatterCode::Long, 4);
1891
1892 proptest_builder_raw!(random_raw_ed448_sig, MatterCode::Ed448Sig, 114);
1894 proptest_builder_raw!(random_raw_ecdsa256k1n, MatterCode::ECDSA256k1N, 33);
1895
1896 proptest! {
1897 #[test]
1898 fn qb2_parsing_never_panics_on_random_bytes(
1899 random_bytes in proptest::collection::vec(any::<u8>(), 0..256)
1900 ) {
1901 let _ = MatterBuilder::new().from_qualified_base2(&random_bytes);
1904 }
1905 }
1906
1907 proptest! {
1908 #[test]
1909 fn qb64_parsing_never_panics_on_random_payload(
1910 idx in 0usize..50,
1911 random_bytes in proptest::collection::vec(any::<u8>(), 0..256)
1912 ) {
1913 use crate::core::matter::test_vectors::FIXED_VECTORS;
1914
1915 let vector = &FIXED_VECTORS[idx % FIXED_VECTORS.len()];
1916
1917 let cs = usize::from(vector.hs) + usize::from(vector.ss);
1919 let prefix = &vector.qb64[..cs];
1920 let b64_chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1921 let payload: String = random_bytes.iter()
1922 .map(|&b| char::from(b64_chars[usize::from(b) % 64]))
1923 .collect();
1924
1925 if let Some(fs) = vector.fs {
1927 let needed = usize::from(fs) - cs;
1928 if payload.len() >= needed {
1929 let stream = format!("{prefix}{}", &payload[..needed]);
1930 let _ = MatterBuilder::new()
1932 .from_qualified_base64(stream.as_bytes());
1933 }
1934 }
1935 }
1936 }
1937 }
1938}