Skip to main content

cesr/core/
version.rs

1//! CESR/KERI version knowledge — the single owner.
2//!
3//! This module owns every version-related wire fact: the [`CesrVersion`]
4//! code-table selector, the [`Protocol`] and [`SerializationKind`] code
5//! tables, and both version-string frames:
6//!
7//! V1 format ([`VersionString`], 17 bytes): `PPPPmmKKKKssssss_`
8//! - 4 chars protocol (`KERI` or `ACDC`)
9//! - 1 hex char major version, 1 hex char minor version
10//! - 4 chars serialization kind (`JSON`, `CBOR`, `MGPK`, `CESR`)
11//! - 6 hex chars size (zero-padded)
12//! - `_` terminator
13//!
14//! V2 format ([`VersionStringV2`], 19 bytes): `PPPPpmmgmmKKKKssss.`
15//! - 4 chars protocol
16//! - 1 Base64 char protocol major (must decode to 2), 2 Base64 chars
17//!   protocol minor
18//! - 1 Base64 char genus major (must decode to 2), 2 Base64 chars genus
19//!   minor
20//! - 4 chars serialization kind
21//! - 4 Base64 chars size
22//! - `.` terminator
23//!
24//! Both frames enforce their fixed-width field invariants at construction,
25//! so rendering is infallible and `parse(render(x)) == x` holds by
26//! construction.
27
28use 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
40/// Total length of a V1 version string in bytes.
41pub const VERSION_STRING_LEN: usize = 17;
42
43/// Total length of a V2 version string in bytes.
44pub const VERSION_STRING_V2_LEN: usize = 19;
45
46/// Width of the V1 hex size field in characters.
47const SIZE_LEN: usize = 6;
48
49/// Largest event size encodable in the fixed [`SIZE_LEN`]-hex-digit size field.
50pub const VERSION_SIZE_MAX: u32 = 0x00FF_FFFF;
51
52/// Largest major/minor version encodable in one hex digit.
53const VERSION_DIGIT_MAX: u8 = 0xF;
54
55/// The only protocol/genus major version a V2 version string may carry.
56const V2_MAJOR: u8 = 2;
57
58/// Largest minor version encodable in two Base64 characters (64^2 - 1).
59const V2_MINOR_MAX: u16 = 4095;
60
61/// Largest event size encodable in four Base64 characters (64^4 - 1).
62const VERSION_V2_SIZE_MAX: u32 = 16_777_215;
63
64/// V1 version string terminator byte.
65const V1_TERMINATOR: u8 = b'_';
66
67/// V2 version string terminator byte.
68const V2_TERMINATOR: u8 = b'.';
69
70/// CESR protocol version for code table selection.
71///
72/// V1 and V2 use different counter code tables — the same wire code
73/// (e.g. `-A`) maps to different semantic groups depending on version.
74#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
75pub enum CesrVersion {
76    /// CESR V1.0 — 22 counter codes.
77    V1,
78    /// CESR V2.0 — 59 counter codes (default).
79    #[default]
80    V2,
81}
82
83/// Errors from version-string parsing and construction.
84#[derive(Debug, PartialEq, Eq, thiserror::Error)]
85pub enum VersionError {
86    /// Input shorter than the fixed version-string frame.
87    #[error("version string truncated: need {needed} more bytes")]
88    Truncated {
89        /// Additional bytes required to complete the frame.
90        needed: usize,
91    },
92
93    /// The 4-byte protocol field is not a recognized protocol.
94    #[error("unknown protocol: {}", .found.escape_ascii())]
95    UnknownProtocol {
96        /// The offending protocol bytes.
97        found: [u8; 4],
98    },
99
100    /// A major/minor version byte is not a hex digit (V1 frame).
101    #[error("invalid {field} version hex digit: {}", .found.escape_ascii())]
102    InvalidVersionDigit {
103        /// The version field that failed: `"major"` or `"minor"`.
104        field: &'static str,
105        /// The offending byte.
106        found: u8,
107    },
108
109    /// The 4-byte serialization-kind field is not a recognized kind.
110    #[error("unknown serialization kind: {}", .found.escape_ascii())]
111    UnknownKind {
112        /// The offending kind bytes.
113        found: [u8; 4],
114    },
115
116    /// The 6-byte V1 size field contains a non-hex byte.
117    #[error("invalid size hex: {}", .found.escape_ascii())]
118    InvalidSizeHex {
119        /// The offending size-field bytes.
120        found: [u8; SIZE_LEN],
121    },
122
123    /// The terminator byte is missing or wrong.
124    #[error("expected {expected:?} terminator, found {}", .found.escape_ascii())]
125    MissingTerminator {
126        /// The terminator the frame requires (`'_'` for V1, `'.'` for V2).
127        expected: char,
128        /// The byte actually found.
129        found: u8,
130    },
131
132    /// A field value does not fit its fixed-width encoding — rendering it
133    /// anyway would widen the string and corrupt the fixed-length frame.
134    #[error("version string field '{field}' exceeds its fixed-width capacity of {max}")]
135    FieldOverflow {
136        /// The version-string field that does not fit.
137        field: &'static str,
138        /// The largest value the field's fixed width can encode.
139        max: u32,
140    },
141
142    /// A V2 protocol/genus major version other than 2.
143    #[error("unsupported {field}: expected {V2_MAJOR}, found {found}")]
144    UnsupportedMajor {
145        /// The major field that failed: `"proto_major"` or `"genus_major"`.
146        field: &'static str,
147        /// The major version actually found.
148        found: u8,
149    },
150
151    /// A V2 Base64-encoded field failed to decode.
152    #[error("invalid base64 in version string field '{field}'")]
153    Base64 {
154        /// The V2 field that failed to decode.
155        field: &'static str,
156        /// The underlying Base64 decode error.
157        #[source]
158        source: B64Error,
159    },
160}
161
162/// Serialization format for the event payload.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum SerializationKind {
165    /// JSON encoding.
166    Json,
167    /// CBOR encoding.
168    Cbor,
169    /// `MessagePack` encoding.
170    Mgpk,
171    /// Native CESR encoding.
172    Cesr,
173}
174
175impl SerializationKind {
176    /// The 4-character wire representation — the single kind↔bytes table.
177    #[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    /// Parse from a 4-character wire representation.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`VersionError::UnknownKind`] if the input is not a
192    /// recognized serialization kind.
193    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    /// Parse from the exact 4 wire bytes.
203    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/// Protocol identifier.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum Protocol {
217    /// Key Event Receipt Infrastructure.
218    Keri,
219    /// Authentic Chained Data Container.
220    Acdc,
221}
222
223impl Protocol {
224    /// The 4-character wire representation.
225    #[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    /// Parse from a 4-character wire representation.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`VersionError::UnknownProtocol`] if the input is not a
238    /// recognized protocol.
239    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    /// Parse from the exact 4 wire bytes.
249    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/// A parsed V1 KERI version string.
259///
260/// Encodes the protocol, version, serialization kind, and total serialized
261/// size of a KERI event message. Fields are validated at construction:
262/// `major` and `minor` fit one hex digit and `size` fits the six-hex-digit
263/// size field, so rendering can never widen the 17-byte frame.
264#[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    /// Create a new version string with the given parameters.
275    ///
276    /// # Errors
277    ///
278    /// Returns [`VersionError::FieldOverflow`] if `major`, `minor`, or
279    /// `size` does not fit its fixed-width hex field.
280    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    /// KERI v1.0 JSON with zero size (to be filled after serialization).
315    #[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    /// Return a copy with the size field updated.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`VersionError::FieldOverflow`] if `size` does not fit the
331    /// six-hex-digit size field.
332    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    /// The protocol identifier.
337    #[must_use]
338    pub const fn proto(&self) -> Protocol {
339        self.proto
340    }
341
342    /// Major version number (0..=15).
343    #[must_use]
344    pub const fn major(&self) -> u8 {
345        self.major
346    }
347
348    /// Minor version number (0..=15).
349    #[must_use]
350    pub const fn minor(&self) -> u8 {
351        self.minor
352    }
353
354    /// Serialization format.
355    #[must_use]
356    pub const fn kind(&self) -> SerializationKind {
357        self.kind
358    }
359
360    /// Total serialized message size in bytes.
361    #[must_use]
362    pub const fn size(&self) -> u32 {
363        self.size
364    }
365
366    /// Render the 17-byte version string.
367    ///
368    /// Infallible: construction already guaranteed every field fits its
369    /// fixed-width hex slot.
370    #[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    /// Parse a version string from the first 17 bytes of `input`, returning
383    /// the parsed value and the remainder of the input.
384    ///
385    /// Major/minor accept any hex digit (upper- or lowercase), matching the
386    /// historical `from_str_radix` behavior of the serder parser.
387    ///
388    /// # Errors
389    ///
390    /// Returns [`VersionError::Truncated`] if `input` is shorter than 17
391    /// bytes, or the field-specific [`VersionError`] variant for an
392    /// unrecognized protocol/kind, a non-hex version or size digit, or a
393    /// missing `_` terminator.
394    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/// A parsed V2 CESR version string.
453///
454/// Carries the protocol major/minor AND the genus major/minor alongside the
455/// serialization kind and size. The V2 frame fixes both majors at 2 —
456/// parsing rejects anything else — so the majors are not stored: invalid
457/// states are unrepresentable and [`Self::proto_major`] /
458/// [`Self::genus_major`] return the format constant. Minors and `size` are
459/// validated at construction to fit their fixed-width Base64 fields, so
460/// rendering can never widen the 19-byte frame.
461#[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    /// Create a new V2 version string with the given parameters. The
472    /// protocol and genus major versions are fixed at 2 by the V2 frame.
473    ///
474    /// # Errors
475    ///
476    /// Returns [`VersionError::FieldOverflow`] if `proto_minor`,
477    /// `genus_minor`, or `size` does not fit its fixed-width Base64 field.
478    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    /// The protocol identifier.
513    #[must_use]
514    pub const fn proto(&self) -> Protocol {
515        self.proto
516    }
517
518    /// Protocol major version number — always 2 in the V2 frame.
519    #[must_use]
520    pub const fn proto_major(&self) -> u8 {
521        V2_MAJOR
522    }
523
524    /// Protocol minor version number (0..=4095).
525    #[must_use]
526    pub const fn proto_minor(&self) -> u16 {
527        self.proto_minor
528    }
529
530    /// Genus major version number — always 2 in the V2 frame.
531    #[must_use]
532    pub const fn genus_major(&self) -> u8 {
533        V2_MAJOR
534    }
535
536    /// Genus minor version number (0..=4095).
537    #[must_use]
538    pub const fn genus_minor(&self) -> u16 {
539        self.genus_minor
540    }
541
542    /// Serialization format.
543    #[must_use]
544    pub const fn kind(&self) -> SerializationKind {
545        self.kind
546    }
547
548    /// Total serialized message size in bytes.
549    #[must_use]
550    pub const fn size(&self) -> u32 {
551        self.size
552    }
553
554    /// Render the 19-byte V2 version string.
555    ///
556    /// Infallible: construction already guaranteed every field fits its
557    /// fixed-width Base64 slot.
558    #[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    /// Parse a V2 version string from the first 19 bytes of `input`,
573    /// returning the parsed value and the remainder of the input.
574    ///
575    /// # Errors
576    ///
577    /// Returns [`VersionError::Truncated`] if `input` is shorter than 19
578    /// bytes, or the field-specific [`VersionError`] variant for an
579    /// unrecognized protocol/kind, a non-Base64 version or size character,
580    /// a major version other than 2, or a missing `.` terminator.
581    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
649/// Split `input` into a fixed-length frame and the remainder.
650fn 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    // Unreachable fallback: the guard above returned for every shorter input.
657    input
658        .split_at_checked(frame_len)
659        .ok_or(VersionError::Truncated { needed: frame_len })
660}
661
662/// Decode one ASCII hex digit (either case), or `None`.
663const 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
672/// Decode the six-hex-digit V1 size field.
673fn 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        // Unreachable overflow arm: six nibbles occupy at most 24 bits.
677        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
686/// Decode a fixed-width Base64 field of a V2 version string.
687fn 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
694/// Append `value` as exactly `width` Base64 characters (left-padded with
695/// `'A'`). The caller guarantees `value` fits `width` characters — enforced
696/// by the version-string constructors.
697fn 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
703/// The first four bytes of `bytes`, right-padded with spaces.
704const 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    // ── CesrVersion ──────────────────────────────────────────────────────
725
726    #[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    // ── VersionString construction & rendering ──────────────────────────
738
739    #[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        // Bug probe (relocated from `to_str`): sizes above VERSION_SIZE_MAX
774        // would render a widened 7-hex-digit size field, corrupting the
775        // 17-byte frame — construction must refuse them.
776        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    // ── VersionString parsing ────────────────────────────────────────────
819
820    #[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        // from_str_radix historically accepted uppercase hex in the size
876        // field; the byte parser preserves that acceptance.
877        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        // 'é' occupies bytes 3..5 — a non-ASCII protocol field must be a
947        // typed error, never a panic.
948        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        // 'é' occupies bytes 15..17, corrupting the size field.
959        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    // ── VersionString round-trips ────────────────────────────────────────
968
969    #[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    // ── VersionStringV2 parsing (keripy test vectors) ────────────────────
1031
1032    #[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        // pvrsn=(2,1), gvrsn=(2,1)
1087        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        // proto_major = 1 (B64 'B') instead of 2 (B64 'C')
1113        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        // genus_major = 1 (B64 'B') instead of 2 (B64 'C')
1126        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    // ── VersionStringV2 construction & rendering ─────────────────────────
1179
1180    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    // ── VersionStringV2 round-trips ──────────────────────────────────────
1282
1283    #[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}