1use crate::b64::decode_int;
29use crate::b64::encode_int;
30use crate::b64::error::Error as B64Error;
31#[cfg(feature = "alloc")]
32#[allow(
33 unused_imports,
34 reason = "alloc prelude items; subset used per cfg/feature combination"
35)]
36use alloc::{format, string::String};
37use core::num::NonZeroUsize;
38use num_traits::{PrimInt, Unsigned, ops::checked::CheckedShl};
39
40pub const VERSION_STRING_LEN: usize = 17;
42
43pub const VERSION_STRING_V2_LEN: usize = 19;
45
46const SIZE_LEN: usize = 6;
48
49pub const VERSION_SIZE_MAX: u32 = 0x00FF_FFFF;
51
52const VERSION_DIGIT_MAX: u8 = 0xF;
54
55const V2_MAJOR: u8 = 2;
57
58const V2_MINOR_MAX: u16 = 4095;
60
61const VERSION_V2_SIZE_MAX: u32 = 16_777_215;
63
64const V1_TERMINATOR: u8 = b'_';
66
67const V2_TERMINATOR: u8 = b'.';
69
70#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
75pub enum CesrVersion {
76 V1,
78 #[default]
80 V2,
81}
82
83#[derive(Debug, PartialEq, Eq, thiserror::Error)]
85pub enum VersionError {
86 #[error("version string truncated: need {needed} more bytes")]
88 Truncated {
89 needed: usize,
91 },
92
93 #[error("unknown protocol: {}", .found.escape_ascii())]
95 UnknownProtocol {
96 found: [u8; 4],
98 },
99
100 #[error("invalid {field} version hex digit: {}", .found.escape_ascii())]
102 InvalidVersionDigit {
103 field: &'static str,
105 found: u8,
107 },
108
109 #[error("unknown serialization kind: {}", .found.escape_ascii())]
111 UnknownKind {
112 found: [u8; 4],
114 },
115
116 #[error("invalid size hex: {}", .found.escape_ascii())]
118 InvalidSizeHex {
119 found: [u8; SIZE_LEN],
121 },
122
123 #[error("expected {expected:?} terminator, found {}", .found.escape_ascii())]
125 MissingTerminator {
126 expected: char,
128 found: u8,
130 },
131
132 #[error("version string field '{field}' exceeds its fixed-width capacity of {max}")]
135 FieldOverflow {
136 field: &'static str,
138 max: u32,
140 },
141
142 #[error("unsupported {field}: expected {V2_MAJOR}, found {found}")]
144 UnsupportedMajor {
145 field: &'static str,
147 found: u8,
149 },
150
151 #[error("invalid base64 in version string field '{field}'")]
153 Base64 {
154 field: &'static str,
156 #[source]
158 source: B64Error,
159 },
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum SerializationKind {
165 Json,
167 Cbor,
169 Mgpk,
171 Cesr,
173}
174
175impl SerializationKind {
176 #[must_use]
178 pub const fn as_str(self) -> &'static str {
179 match self {
180 Self::Json => "JSON",
181 Self::Cbor => "CBOR",
182 Self::Mgpk => "MGPK",
183 Self::Cesr => "CESR",
184 }
185 }
186
187 pub fn from_repr(repr: &str) -> Result<Self, VersionError> {
194 match repr.as_bytes() {
195 &[c0, c1, c2, c3] => Self::from_wire([c0, c1, c2, c3]),
196 other => Err(VersionError::UnknownKind {
197 found: first4(other),
198 }),
199 }
200 }
201
202 const fn from_wire(wire: [u8; 4]) -> Result<Self, VersionError> {
204 match &wire {
205 b"JSON" => Ok(Self::Json),
206 b"CBOR" => Ok(Self::Cbor),
207 b"MGPK" => Ok(Self::Mgpk),
208 b"CESR" => Ok(Self::Cesr),
209 _ => Err(VersionError::UnknownKind { found: wire }),
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum Protocol {
217 Keri,
219 Acdc,
221}
222
223impl Protocol {
224 #[must_use]
226 pub const fn as_str(self) -> &'static str {
227 match self {
228 Self::Keri => "KERI",
229 Self::Acdc => "ACDC",
230 }
231 }
232
233 pub fn from_repr(repr: &str) -> Result<Self, VersionError> {
240 match repr.as_bytes() {
241 &[c0, c1, c2, c3] => Self::from_wire([c0, c1, c2, c3]),
242 other => Err(VersionError::UnknownProtocol {
243 found: first4(other),
244 }),
245 }
246 }
247
248 const fn from_wire(wire: [u8; 4]) -> Result<Self, VersionError> {
250 match &wire {
251 b"KERI" => Ok(Self::Keri),
252 b"ACDC" => Ok(Self::Acdc),
253 _ => Err(VersionError::UnknownProtocol { found: wire }),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub struct VersionString {
266 proto: Protocol,
267 major: u8,
268 minor: u8,
269 kind: SerializationKind,
270 size: u32,
271}
272
273impl VersionString {
274 pub fn new(
281 proto: Protocol,
282 major: u8,
283 minor: u8,
284 kind: SerializationKind,
285 size: u32,
286 ) -> Result<Self, VersionError> {
287 if major > VERSION_DIGIT_MAX {
288 return Err(VersionError::FieldOverflow {
289 field: "major",
290 max: u32::from(VERSION_DIGIT_MAX),
291 });
292 }
293 if minor > VERSION_DIGIT_MAX {
294 return Err(VersionError::FieldOverflow {
295 field: "minor",
296 max: u32::from(VERSION_DIGIT_MAX),
297 });
298 }
299 if size > VERSION_SIZE_MAX {
300 return Err(VersionError::FieldOverflow {
301 field: "size",
302 max: VERSION_SIZE_MAX,
303 });
304 }
305 Ok(Self {
306 proto,
307 major,
308 minor,
309 kind,
310 size,
311 })
312 }
313
314 #[must_use]
316 pub const fn keri_json_v1() -> Self {
317 Self {
318 proto: Protocol::Keri,
319 major: 1,
320 minor: 0,
321 kind: SerializationKind::Json,
322 size: 0,
323 }
324 }
325
326 pub fn with_size(self, size: u32) -> Result<Self, VersionError> {
333 Self::new(self.proto, self.major, self.minor, self.kind, size)
334 }
335
336 #[must_use]
338 pub const fn proto(&self) -> Protocol {
339 self.proto
340 }
341
342 #[must_use]
344 pub const fn major(&self) -> u8 {
345 self.major
346 }
347
348 #[must_use]
350 pub const fn minor(&self) -> u8 {
351 self.minor
352 }
353
354 #[must_use]
356 pub const fn kind(&self) -> SerializationKind {
357 self.kind
358 }
359
360 #[must_use]
362 pub const fn size(&self) -> u32 {
363 self.size
364 }
365
366 #[must_use]
371 pub fn to_str(&self) -> String {
372 format!(
373 "{}{:x}{:x}{}{:06x}_",
374 self.proto.as_str(),
375 self.major,
376 self.minor,
377 self.kind.as_str(),
378 self.size,
379 )
380 }
381
382 pub fn parse(input: &[u8]) -> Result<(Self, &[u8]), VersionError> {
395 let (frame, rest) = split_frame(input, VERSION_STRING_LEN)?;
396 let &[
397 p0,
398 p1,
399 p2,
400 p3,
401 major_b,
402 minor_b,
403 k0,
404 k1,
405 k2,
406 k3,
407 s0,
408 s1,
409 s2,
410 s3,
411 s4,
412 s5,
413 term,
414 ] = frame
415 else {
416 return Err(VersionError::Truncated {
417 needed: VERSION_STRING_LEN,
418 });
419 };
420
421 let proto = Protocol::from_wire([p0, p1, p2, p3])?;
422 let major = decode_hex_digit(major_b).ok_or(VersionError::InvalidVersionDigit {
423 field: "major",
424 found: major_b,
425 })?;
426 let minor = decode_hex_digit(minor_b).ok_or(VersionError::InvalidVersionDigit {
427 field: "minor",
428 found: minor_b,
429 })?;
430 let kind = SerializationKind::from_wire([k0, k1, k2, k3])?;
431 let size = decode_hex_size([s0, s1, s2, s3, s4, s5])?;
432 if term != V1_TERMINATOR {
433 return Err(VersionError::MissingTerminator {
434 expected: '_',
435 found: term,
436 });
437 }
438
439 Ok((
440 Self {
441 proto,
442 major,
443 minor,
444 kind,
445 size,
446 },
447 rest,
448 ))
449 }
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
462pub struct VersionStringV2 {
463 proto: Protocol,
464 proto_minor: u16,
465 genus_minor: u16,
466 kind: SerializationKind,
467 size: u32,
468}
469
470impl VersionStringV2 {
471 pub fn new(
479 proto: Protocol,
480 proto_minor: u16,
481 genus_minor: u16,
482 kind: SerializationKind,
483 size: u32,
484 ) -> Result<Self, VersionError> {
485 if proto_minor > V2_MINOR_MAX {
486 return Err(VersionError::FieldOverflow {
487 field: "proto_minor",
488 max: u32::from(V2_MINOR_MAX),
489 });
490 }
491 if genus_minor > V2_MINOR_MAX {
492 return Err(VersionError::FieldOverflow {
493 field: "genus_minor",
494 max: u32::from(V2_MINOR_MAX),
495 });
496 }
497 if size > VERSION_V2_SIZE_MAX {
498 return Err(VersionError::FieldOverflow {
499 field: "size",
500 max: VERSION_V2_SIZE_MAX,
501 });
502 }
503 Ok(Self {
504 proto,
505 proto_minor,
506 genus_minor,
507 kind,
508 size,
509 })
510 }
511
512 #[must_use]
514 pub const fn proto(&self) -> Protocol {
515 self.proto
516 }
517
518 #[must_use]
520 pub const fn proto_major(&self) -> u8 {
521 V2_MAJOR
522 }
523
524 #[must_use]
526 pub const fn proto_minor(&self) -> u16 {
527 self.proto_minor
528 }
529
530 #[must_use]
532 pub const fn genus_major(&self) -> u8 {
533 V2_MAJOR
534 }
535
536 #[must_use]
538 pub const fn genus_minor(&self) -> u16 {
539 self.genus_minor
540 }
541
542 #[must_use]
544 pub const fn kind(&self) -> SerializationKind {
545 self.kind
546 }
547
548 #[must_use]
550 pub const fn size(&self) -> u32 {
551 self.size
552 }
553
554 #[must_use]
559 pub fn to_str(&self) -> String {
560 let mut out = String::with_capacity(VERSION_STRING_V2_LEN);
561 out.push_str(self.proto.as_str());
562 push_b64_fixed(&mut out, u32::from(V2_MAJOR), 1);
563 push_b64_fixed(&mut out, u32::from(self.proto_minor), 2);
564 push_b64_fixed(&mut out, u32::from(V2_MAJOR), 1);
565 push_b64_fixed(&mut out, u32::from(self.genus_minor), 2);
566 out.push_str(self.kind.as_str());
567 push_b64_fixed(&mut out, self.size, 4);
568 out.push(char::from(V2_TERMINATOR));
569 out
570 }
571
572 pub fn parse(input: &[u8]) -> Result<(Self, &[u8]), VersionError> {
582 let (frame, rest) = split_frame(input, VERSION_STRING_V2_LEN)?;
583 let &[
584 p0,
585 p1,
586 p2,
587 p3,
588 pj,
589 pmin0,
590 pmin1,
591 gj,
592 gmin0,
593 gmin1,
594 k0,
595 k1,
596 k2,
597 k3,
598 z0,
599 z1,
600 z2,
601 z3,
602 term,
603 ] = frame
604 else {
605 return Err(VersionError::Truncated {
606 needed: VERSION_STRING_V2_LEN,
607 });
608 };
609
610 let proto = Protocol::from_wire([p0, p1, p2, p3])?;
611 let proto_major: u8 = decode_b64_field(&[pj], "proto_major")?;
612 if proto_major != V2_MAJOR {
613 return Err(VersionError::UnsupportedMajor {
614 field: "proto_major",
615 found: proto_major,
616 });
617 }
618 let proto_minor: u16 = decode_b64_field(&[pmin0, pmin1], "proto_minor")?;
619 let genus_major: u8 = decode_b64_field(&[gj], "genus_major")?;
620 if genus_major != V2_MAJOR {
621 return Err(VersionError::UnsupportedMajor {
622 field: "genus_major",
623 found: genus_major,
624 });
625 }
626 let genus_minor: u16 = decode_b64_field(&[gmin0, gmin1], "genus_minor")?;
627 let kind = SerializationKind::from_wire([k0, k1, k2, k3])?;
628 let size: u32 = decode_b64_field(&[z0, z1, z2, z3], "size")?;
629 if term != V2_TERMINATOR {
630 return Err(VersionError::MissingTerminator {
631 expected: '.',
632 found: term,
633 });
634 }
635
636 Ok((
637 Self {
638 proto,
639 proto_minor,
640 genus_minor,
641 kind,
642 size,
643 },
644 rest,
645 ))
646 }
647}
648
649fn split_frame(input: &[u8], frame_len: usize) -> Result<(&[u8], &[u8]), VersionError> {
651 if let Some(needed) = frame_len.checked_sub(input.len())
652 && needed > 0
653 {
654 return Err(VersionError::Truncated { needed });
655 }
656 input
658 .split_at_checked(frame_len)
659 .ok_or(VersionError::Truncated { needed: frame_len })
660}
661
662const fn decode_hex_digit(byte: u8) -> Option<u8> {
664 match byte {
665 b'0'..=b'9' => Some(byte - b'0'),
666 b'a'..=b'f' => Some(byte - b'a' + 10),
667 b'A'..=b'F' => Some(byte - b'A' + 10),
668 _ => None,
669 }
670}
671
672fn decode_hex_size(bytes: [u8; SIZE_LEN]) -> Result<u32, VersionError> {
674 bytes.iter().try_fold(0_u32, |acc, &byte| {
675 let digit = decode_hex_digit(byte).ok_or(VersionError::InvalidSizeHex { found: bytes })?;
676 acc.checked_mul(16)
678 .and_then(|shifted| shifted.checked_add(u32::from(digit)))
679 .ok_or(VersionError::FieldOverflow {
680 field: "size",
681 max: VERSION_SIZE_MAX,
682 })
683 })
684}
685
686fn decode_b64_field<N>(bytes: &[u8], field: &'static str) -> Result<N, VersionError>
688where
689 N: PrimInt + Unsigned + CheckedShl + 'static,
690{
691 decode_int(bytes).map_err(|source| VersionError::Base64 { field, source })
692}
693
694fn push_b64_fixed(out: &mut String, value: u32, width: usize) {
698 let digits = encode_int(value, NonZeroUsize::MIN);
699 (digits.len()..width).for_each(|_| out.push('A'));
700 out.push_str(&digits);
701}
702
703const fn first4(bytes: &[u8]) -> [u8; 4] {
705 let mut out = [b' '; 4];
706 let mut i = 0;
707 while i < bytes.len() && i < 4 {
708 out[i] = bytes[i];
709 i += 1;
710 }
711 out
712}
713
714#[cfg(test)]
715#[allow(
716 clippy::unwrap_used,
717 clippy::expect_used,
718 clippy::panic,
719 reason = "test code: panics acceptable"
720)]
721mod tests {
722 use super::*;
723
724 #[test]
727 fn default_is_v2() {
728 assert_eq!(CesrVersion::default(), CesrVersion::V2);
729 }
730
731 #[test]
732 fn equality() {
733 assert_eq!(CesrVersion::V1, CesrVersion::V1);
734 assert_ne!(CesrVersion::V1, CesrVersion::V2);
735 }
736
737 #[test]
740 fn keri_json_v1_defaults() {
741 let vs = VersionString::keri_json_v1();
742 assert_eq!(vs.proto(), Protocol::Keri);
743 assert_eq!(vs.major(), 1);
744 assert_eq!(vs.minor(), 0);
745 assert_eq!(vs.kind(), SerializationKind::Json);
746 assert_eq!(vs.size(), 0);
747 }
748
749 #[test]
750 fn to_str_zero_size() {
751 let vs = VersionString::keri_json_v1();
752 assert_eq!(vs.to_str(), "KERI10JSON000000_");
753 }
754
755 #[test]
756 fn to_str_nonzero_size() {
757 let vs = VersionString::keri_json_v1().with_size(0x25d).unwrap();
758 assert_eq!(vs.to_str(), "KERI10JSON00025d_");
759 }
760
761 #[test]
762 fn to_str_renders_max_size_at_fixed_width() {
763 let vs = VersionString::keri_json_v1()
764 .with_size(VERSION_SIZE_MAX)
765 .unwrap();
766 let rendered = vs.to_str();
767 assert_eq!(rendered, "KERI10JSONffffff_");
768 assert_eq!(rendered.len(), VERSION_STRING_LEN);
769 }
770
771 #[test]
772 fn rejects_size_beyond_fixed_width_at_construction() {
773 let result = VersionString::keri_json_v1().with_size(VERSION_SIZE_MAX + 1);
777 assert_eq!(
778 result.unwrap_err(),
779 VersionError::FieldOverflow {
780 field: "size",
781 max: VERSION_SIZE_MAX,
782 }
783 );
784 }
785
786 #[test]
787 fn to_str_renders_max_versions_at_fixed_width() {
788 let vs = VersionString::new(Protocol::Keri, 0xF, 0xF, SerializationKind::Json, 0).unwrap();
789 let rendered = vs.to_str();
790 assert_eq!(rendered, "KERIffJSON000000_");
791 assert_eq!(rendered.len(), VERSION_STRING_LEN);
792 }
793
794 #[test]
795 fn rejects_major_beyond_one_hex_digit_at_construction() {
796 let result = VersionString::new(Protocol::Keri, 0x10, 0, SerializationKind::Json, 0);
797 assert!(matches!(
798 result.unwrap_err(),
799 VersionError::FieldOverflow { field: "major", .. }
800 ));
801 }
802
803 #[test]
804 fn rejects_minor_beyond_one_hex_digit_at_construction() {
805 let result = VersionString::new(Protocol::Keri, 0, 0x10, SerializationKind::Json, 0);
806 assert!(matches!(
807 result.unwrap_err(),
808 VersionError::FieldOverflow { field: "minor", .. }
809 ));
810 }
811
812 #[test]
813 fn size_capacity_matches_size_field_width() {
814 let width = u32::try_from(SIZE_LEN).unwrap();
815 assert_eq!(VERSION_SIZE_MAX, (1_u32 << (4 * width)) - 1);
816 }
817
818 #[test]
821 fn parse_valid() {
822 let (vs, rest) = VersionString::parse(b"KERI10JSON00025d_").unwrap();
823 assert_eq!(vs.proto(), Protocol::Keri);
824 assert_eq!(vs.major(), 1);
825 assert_eq!(vs.minor(), 0);
826 assert_eq!(vs.kind(), SerializationKind::Json);
827 assert_eq!(vs.size(), 0x25d);
828 assert!(rest.is_empty());
829 }
830
831 #[test]
832 fn parse_keri_v1_json_returns_rest() {
833 let (parsed, rest) = VersionString::parse(b"KERI10JSON000123_rest").unwrap();
834 assert_eq!(parsed.proto(), Protocol::Keri);
835 assert_eq!(parsed.major(), 1);
836 assert_eq!(parsed.minor(), 0);
837 assert_eq!(parsed.kind(), SerializationKind::Json);
838 assert_eq!(parsed.size(), 0x123);
839 assert_eq!(rest, b"rest");
840 }
841
842 #[test]
843 fn parse_acdc_v1_cbor() {
844 let (parsed, rest) = VersionString::parse(b"ACDC10CBOR000050_").unwrap();
845 assert_eq!(parsed.proto(), Protocol::Acdc);
846 assert_eq!(parsed.kind(), SerializationKind::Cbor);
847 assert_eq!(parsed.size(), 0x50);
848 assert!(rest.is_empty());
849 }
850
851 #[test]
852 fn parse_keri_v2_msgpack() {
853 let (parsed, _) = VersionString::parse(b"KERI20MGPK0000ff_").unwrap();
854 assert_eq!(parsed.major(), 2);
855 assert_eq!(parsed.minor(), 0);
856 assert_eq!(parsed.kind(), SerializationKind::Mgpk);
857 assert_eq!(parsed.size(), 0xff);
858 }
859
860 #[test]
861 fn parse_cesr_kind() {
862 let (parsed, _) = VersionString::parse(b"KERI10CESR000000_").unwrap();
863 assert_eq!(parsed.kind(), SerializationKind::Cesr);
864 assert_eq!(parsed.size(), 0);
865 }
866
867 #[test]
868 fn parse_max_size() {
869 let (parsed, _) = VersionString::parse(b"KERI10JSONffffff_").unwrap();
870 assert_eq!(parsed.size(), 0x00ff_ffff);
871 }
872
873 #[test]
874 fn parse_uppercase_size_hex_accepted() {
875 let (parsed, _) = VersionString::parse(b"KERI10JSONFFFFFF_").unwrap();
878 assert_eq!(parsed.size(), 0x00ff_ffff);
879 }
880
881 #[test]
882 fn parse_too_short_reports_missing_bytes() {
883 let result = VersionString::parse(b"KERI10JSON");
884 assert_eq!(result.unwrap_err(), VersionError::Truncated { needed: 7 });
885 }
886
887 #[test]
888 fn parse_too_short_twelve_bytes() {
889 let result = VersionString::parse(b"KERI10JSON00");
890 assert_eq!(result.unwrap_err(), VersionError::Truncated { needed: 5 });
891 }
892
893 #[test]
894 fn parse_unknown_protocol() {
895 let result = VersionString::parse(b"XXXX10JSON000000_");
896 assert_eq!(
897 result.unwrap_err(),
898 VersionError::UnknownProtocol { found: *b"XXXX" }
899 );
900 }
901
902 #[test]
903 fn parse_unknown_kind() {
904 let result = VersionString::parse(b"KERI10YAML000000_");
905 assert_eq!(
906 result.unwrap_err(),
907 VersionError::UnknownKind { found: *b"YAML" }
908 );
909 }
910
911 #[test]
912 fn parse_invalid_version_digit() {
913 let result = VersionString::parse(b"KERIx0JSON000000_");
914 assert_eq!(
915 result.unwrap_err(),
916 VersionError::InvalidVersionDigit {
917 field: "major",
918 found: b'x',
919 }
920 );
921 }
922
923 #[test]
924 fn parse_invalid_hex_size() {
925 let result = VersionString::parse(b"KERI10JSON00ZZZZ_");
926 assert_eq!(
927 result.unwrap_err(),
928 VersionError::InvalidSizeHex { found: *b"00ZZZZ" }
929 );
930 }
931
932 #[test]
933 fn parse_missing_terminator() {
934 let result = VersionString::parse(b"KERI10JSON000000X");
935 assert_eq!(
936 result.unwrap_err(),
937 VersionError::MissingTerminator {
938 expected: '_',
939 found: b'X',
940 }
941 );
942 }
943
944 #[test]
945 fn parse_multibyte_char_in_proto_is_error_not_panic() {
946 let input = "KER\u{e9}AJSONAAAAAA_";
949 assert_eq!(input.len(), VERSION_STRING_LEN);
950 assert!(matches!(
951 VersionString::parse(input.as_bytes()),
952 Err(VersionError::UnknownProtocol { .. })
953 ));
954 }
955
956 #[test]
957 fn parse_multibyte_char_in_size_is_error_not_panic() {
958 let input = "KERI10JSONAAAAA\u{e9}";
960 assert_eq!(input.len(), VERSION_STRING_LEN);
961 assert!(matches!(
962 VersionString::parse(input.as_bytes()),
963 Err(VersionError::InvalidSizeHex { .. })
964 ));
965 }
966
967 #[test]
970 fn parse_roundtrip() {
971 let original =
972 VersionString::new(Protocol::Acdc, 2, 5, SerializationKind::Cbor, 0x001a_2b3c).unwrap();
973 let rendered = original.to_str();
974 let (parsed, rest) = VersionString::parse(rendered.as_bytes()).unwrap();
975 assert_eq!(original, parsed);
976 assert!(rest.is_empty());
977 }
978
979 #[test]
980 fn roundtrip_boundary_sizes() {
981 for size in [0, 1, VERSION_SIZE_MAX - 1, VERSION_SIZE_MAX] {
982 let original =
983 VersionString::new(Protocol::Keri, 1, 0, SerializationKind::Json, size).unwrap();
984 let (parsed, _) = VersionString::parse(original.to_str().as_bytes()).unwrap();
985 assert_eq!(original, parsed, "size {size} must round-trip");
986 }
987 }
988
989 #[test]
990 fn roundtrip_all_protocols_and_kinds() {
991 for proto in [Protocol::Keri, Protocol::Acdc] {
992 for kind in [
993 SerializationKind::Json,
994 SerializationKind::Cbor,
995 SerializationKind::Mgpk,
996 SerializationKind::Cesr,
997 ] {
998 for (major, minor) in [(0, 0), (1, 0), (0xF, 0xF)] {
999 let original = VersionString::new(proto, major, minor, kind, 42).unwrap();
1000 let (parsed, _) = VersionString::parse(original.to_str().as_bytes()).unwrap();
1001 assert_eq!(original, parsed);
1002 }
1003 }
1004 }
1005 }
1006
1007 #[test]
1008 fn serialization_kind_roundtrip() {
1009 for kind in [
1010 SerializationKind::Json,
1011 SerializationKind::Cbor,
1012 SerializationKind::Mgpk,
1013 SerializationKind::Cesr,
1014 ] {
1015 let repr = kind.as_str();
1016 let parsed = SerializationKind::from_repr(repr).unwrap();
1017 assert_eq!(kind, parsed);
1018 }
1019 }
1020
1021 #[test]
1022 fn protocol_roundtrip() {
1023 for proto in [Protocol::Keri, Protocol::Acdc] {
1024 let repr = proto.as_str();
1025 let parsed = Protocol::from_repr(repr).unwrap();
1026 assert_eq!(proto, parsed);
1027 }
1028 }
1029
1030 #[test]
1033 fn parse_v2_keri_json_size_zero() {
1034 let (parsed, rest) = VersionStringV2::parse(b"KERICAACAAJSONAAAA.").unwrap();
1035 assert_eq!(parsed.proto(), Protocol::Keri);
1036 assert_eq!(parsed.proto_major(), 2);
1037 assert_eq!(parsed.proto_minor(), 0);
1038 assert_eq!(parsed.genus_major(), 2);
1039 assert_eq!(parsed.genus_minor(), 0);
1040 assert_eq!(parsed.kind(), SerializationKind::Json);
1041 assert_eq!(parsed.size(), 0);
1042 assert!(rest.is_empty());
1043 }
1044
1045 #[test]
1046 fn parse_v2_keri_json_size_65() {
1047 let (parsed, rest) = VersionStringV2::parse(b"KERICAACAAJSONAABB.").unwrap();
1048 assert_eq!(parsed.proto(), Protocol::Keri);
1049 assert_eq!(parsed.proto_major(), 2);
1050 assert_eq!(parsed.proto_minor(), 0);
1051 assert_eq!(parsed.genus_major(), 2);
1052 assert_eq!(parsed.genus_minor(), 0);
1053 assert_eq!(parsed.kind(), SerializationKind::Json);
1054 assert_eq!(parsed.size(), 65);
1055 assert!(rest.is_empty());
1056 }
1057
1058 #[test]
1059 fn parse_v2_acdc_json_size_86() {
1060 let (parsed, rest) = VersionStringV2::parse(b"ACDCCAACAAJSONAABW.").unwrap();
1061 assert_eq!(parsed.proto(), Protocol::Acdc);
1062 assert_eq!(parsed.proto_major(), 2);
1063 assert_eq!(parsed.proto_minor(), 0);
1064 assert_eq!(parsed.genus_major(), 2);
1065 assert_eq!(parsed.genus_minor(), 0);
1066 assert_eq!(parsed.kind(), SerializationKind::Json);
1067 assert_eq!(parsed.size(), 86);
1068 assert!(rest.is_empty());
1069 }
1070
1071 #[test]
1072 fn parse_v2_keri_mgpk_size_zero() {
1073 let (parsed, rest) = VersionStringV2::parse(b"KERICAACAAMGPKAAAA.").unwrap();
1074 assert_eq!(parsed.proto(), Protocol::Keri);
1075 assert_eq!(parsed.proto_major(), 2);
1076 assert_eq!(parsed.proto_minor(), 0);
1077 assert_eq!(parsed.genus_major(), 2);
1078 assert_eq!(parsed.genus_minor(), 0);
1079 assert_eq!(parsed.kind(), SerializationKind::Mgpk);
1080 assert_eq!(parsed.size(), 0);
1081 assert!(rest.is_empty());
1082 }
1083
1084 #[test]
1085 fn parse_v2_keri_json_versioned() {
1086 let (parsed, rest) = VersionStringV2::parse(b"KERICABCABJSONAAAA.").unwrap();
1088 assert_eq!(parsed.proto(), Protocol::Keri);
1089 assert_eq!(parsed.proto_major(), 2);
1090 assert_eq!(parsed.proto_minor(), 1);
1091 assert_eq!(parsed.genus_major(), 2);
1092 assert_eq!(parsed.genus_minor(), 1);
1093 assert_eq!(parsed.kind(), SerializationKind::Json);
1094 assert_eq!(parsed.size(), 0);
1095 assert!(rest.is_empty());
1096 }
1097
1098 #[test]
1099 fn parse_v2_returns_rest() {
1100 let (_, rest) = VersionStringV2::parse(b"KERICAACAAJSONAAAA.trailing").unwrap();
1101 assert_eq!(rest, b"trailing");
1102 }
1103
1104 #[test]
1105 fn parse_v2_too_short_reports_missing_bytes() {
1106 let result = VersionStringV2::parse(b"KERICAACAAJSON");
1107 assert_eq!(result.unwrap_err(), VersionError::Truncated { needed: 5 });
1108 }
1109
1110 #[test]
1111 fn parse_v2_wrong_proto_major() {
1112 let result = VersionStringV2::parse(b"KERIBAACAAJSONAAAA.");
1114 assert_eq!(
1115 result.unwrap_err(),
1116 VersionError::UnsupportedMajor {
1117 field: "proto_major",
1118 found: 1,
1119 }
1120 );
1121 }
1122
1123 #[test]
1124 fn parse_v2_wrong_genus_major() {
1125 let result = VersionStringV2::parse(b"KERICAABAAJSONAAAA.");
1127 assert_eq!(
1128 result.unwrap_err(),
1129 VersionError::UnsupportedMajor {
1130 field: "genus_major",
1131 found: 1,
1132 }
1133 );
1134 }
1135
1136 #[test]
1137 fn parse_v2_unknown_kind() {
1138 let result = VersionStringV2::parse(b"KERICAACAAXXXXAAAA.");
1139 assert_eq!(
1140 result.unwrap_err(),
1141 VersionError::UnknownKind { found: *b"XXXX" }
1142 );
1143 }
1144
1145 #[test]
1146 fn parse_v2_wrong_terminator() {
1147 let result = VersionStringV2::parse(b"KERICAACAAJSONAAAA_");
1148 assert_eq!(
1149 result.unwrap_err(),
1150 VersionError::MissingTerminator {
1151 expected: '.',
1152 found: b'_',
1153 }
1154 );
1155 }
1156
1157 #[test]
1158 fn parse_v2_invalid_base64_size() {
1159 let result = VersionStringV2::parse(b"KERICAACAAJSONAA+A.");
1160 assert!(matches!(
1161 result.unwrap_err(),
1162 VersionError::Base64 { field: "size", .. }
1163 ));
1164 }
1165
1166 #[test]
1167 fn parse_v2_cesr_kind() {
1168 let (parsed, _) = VersionStringV2::parse(b"KERICAACAACESRAAAA.").unwrap();
1169 assert_eq!(parsed.kind(), SerializationKind::Cesr);
1170 }
1171
1172 #[test]
1173 fn parse_v2_cbor_kind() {
1174 let (parsed, _) = VersionStringV2::parse(b"KERICAACAACBORAAAA.").unwrap();
1175 assert_eq!(parsed.kind(), SerializationKind::Cbor);
1176 }
1177
1178 fn make_v2(
1181 proto: Protocol,
1182 proto_minor: u16,
1183 genus_minor: u16,
1184 kind: SerializationKind,
1185 size: u32,
1186 ) -> VersionStringV2 {
1187 VersionStringV2::new(proto, proto_minor, genus_minor, kind, size).unwrap()
1188 }
1189
1190 #[test]
1191 fn v2_to_str_keri_json_size_zero() {
1192 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Json, 0);
1193 assert_eq!(vs.to_str(), "KERICAACAAJSONAAAA.");
1194 }
1195
1196 #[test]
1197 fn v2_to_str_keri_json_size_65() {
1198 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Json, 65);
1199 assert_eq!(vs.to_str(), "KERICAACAAJSONAABB.");
1200 }
1201
1202 #[test]
1203 fn v2_to_str_acdc_json_size_86() {
1204 let vs = make_v2(Protocol::Acdc, 0, 0, SerializationKind::Json, 86);
1205 assert_eq!(vs.to_str(), "ACDCCAACAAJSONAABW.");
1206 }
1207
1208 #[test]
1209 fn v2_to_str_keri_mgpk_size_zero() {
1210 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Mgpk, 0);
1211 assert_eq!(vs.to_str(), "KERICAACAAMGPKAAAA.");
1212 }
1213
1214 #[test]
1215 fn v2_to_str_versioned() {
1216 let vs = make_v2(Protocol::Keri, 1, 1, SerializationKind::Json, 0);
1217 assert_eq!(vs.to_str(), "KERICABCABJSONAAAA.");
1218 }
1219
1220 #[test]
1221 fn v2_to_str_length_is_19() {
1222 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Json, 0);
1223 assert_eq!(vs.to_str().len(), VERSION_STRING_V2_LEN);
1224 }
1225
1226 #[test]
1227 fn v2_to_str_cbor() {
1228 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Cbor, 0);
1229 assert_eq!(vs.to_str(), "KERICAACAACBORAAAA.");
1230 }
1231
1232 #[test]
1233 fn v2_to_str_cesr() {
1234 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Cesr, 0);
1235 assert_eq!(vs.to_str(), "KERICAACAACESRAAAA.");
1236 }
1237
1238 #[test]
1239 fn v2_majors_are_fixed_at_two() {
1240 let vs = make_v2(Protocol::Keri, 0, 0, SerializationKind::Json, 0);
1241 assert_eq!(vs.proto_major(), 2);
1242 assert_eq!(vs.genus_major(), 2);
1243 }
1244
1245 #[test]
1246 fn v2_rejects_oversize_minor_at_construction() {
1247 let result = VersionStringV2::new(
1248 Protocol::Keri,
1249 V2_MINOR_MAX + 1,
1250 0,
1251 SerializationKind::Json,
1252 0,
1253 );
1254 assert_eq!(
1255 result.unwrap_err(),
1256 VersionError::FieldOverflow {
1257 field: "proto_minor",
1258 max: u32::from(V2_MINOR_MAX),
1259 }
1260 );
1261 }
1262
1263 #[test]
1264 fn v2_rejects_oversize_size_at_construction() {
1265 let result = VersionStringV2::new(
1266 Protocol::Keri,
1267 0,
1268 0,
1269 SerializationKind::Json,
1270 VERSION_V2_SIZE_MAX + 1,
1271 );
1272 assert_eq!(
1273 result.unwrap_err(),
1274 VersionError::FieldOverflow {
1275 field: "size",
1276 max: VERSION_V2_SIZE_MAX,
1277 }
1278 );
1279 }
1280
1281 #[test]
1284 fn v2_roundtrip_keri_json_size_zero() {
1285 let original = make_v2(Protocol::Keri, 0, 0, SerializationKind::Json, 0);
1286 let rendered = original.to_str();
1287 let (parsed, rest) = VersionStringV2::parse(rendered.as_bytes()).unwrap();
1288 assert_eq!(original, parsed);
1289 assert!(rest.is_empty());
1290 }
1291
1292 #[test]
1293 fn v2_roundtrip_boundary_sizes() {
1294 for size in [0, 1, VERSION_V2_SIZE_MAX - 1, VERSION_V2_SIZE_MAX] {
1295 let original = make_v2(Protocol::Keri, 0, 0, SerializationKind::Json, size);
1296 let (parsed, _) = VersionStringV2::parse(original.to_str().as_bytes()).unwrap();
1297 assert_eq!(original, parsed, "size {size} must round-trip");
1298 }
1299 }
1300
1301 #[test]
1302 fn v2_roundtrip_boundary_minors() {
1303 for minor in [0, 1, V2_MINOR_MAX - 1, V2_MINOR_MAX] {
1304 let original = make_v2(Protocol::Acdc, minor, minor, SerializationKind::Cbor, 86);
1305 let (parsed, _) = VersionStringV2::parse(original.to_str().as_bytes()).unwrap();
1306 assert_eq!(original, parsed, "minor {minor} must round-trip");
1307 }
1308 }
1309}