Skip to main content

cesr/core/matter/code/
matter_code.rs

1use super::cesr_code::CesrCode;
2use super::hard::{get_hard_size_from_byte, get_hard_size_from_sextet};
3use super::sealed::Sealed;
4use crate::b64::encode_binary;
5use crate::core::matter::{
6    MatterPart,
7    error::{ParsingError, ValidationError},
8    sizage::{Sizage, SizeType},
9};
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};
16use core::{num::NonZeroUsize, str::FromStr};
17use strum::{AsRefStr, Display, EnumIter, EnumString, IntoStaticStr, VariantNames};
18
19/// Number of Base64 characters in the header of a small variable-size code (`4`, `5`, `6`).
20pub const SMALL_VAR_HEADER_SIZE: u8 = 3;
21/// Number of Base64 characters in the header of a large variable-size code (`7`, `8`, `9`).
22pub const LARGE_VAR_HEADER_SIZE: u8 = 6;
23
24const SMALL_VAR_SIZES: [char; 3] = ['4', '5', '6'];
25const LARGE_VAR_SIZES: [char; 3] = ['7', '8', '9'];
26
27/// All Matter CESR primitive codes as a single untyped enum.
28///
29/// Each variant maps to a unique Base64 code string via `#[strum(serialize = "...")]`.
30/// For typed access to specific subsets use `DigestCode`, `VerKeyCode`, etc.
31#[derive(
32    Debug,
33    Eq,
34    PartialEq,
35    Clone,
36    Copy,
37    Hash,
38    AsRefStr,
39    EnumString,
40    IntoStaticStr,
41    VariantNames,
42    Display,
43    EnumIter,
44)]
45#[allow(
46    non_camel_case_types,
47    reason = "CESR spec uses underscored code names like Blake3_256"
48)]
49pub enum MatterCode {
50    /// Ed25519 256-bit random seed for a private key (`A`).
51    #[strum(serialize = "A")]
52    Ed25519Seed,
53    /// Ed25519 verification key, non-transferable basic derivation (`B`).
54    #[strum(serialize = "B")]
55    Ed25519N,
56    /// X25519 public encryption key, may be converted from `Ed25519` or `Ed25519N` (`C`).
57    #[strum(serialize = "C")]
58    X25519,
59    /// Ed25519 verification key, basic derivation (`D`).
60    #[strum(serialize = "D")]
61    Ed25519,
62    /// BLAKE3 256-bit digest, self-addressing derivation (`E`).
63    #[strum(serialize = "E")]
64    Blake3_256,
65    /// `BLAKE2b` 256-bit digest, self-addressing derivation (`F`).
66    #[strum(serialize = "F")]
67    Blake2b_256,
68    /// BLAKE2s 256-bit digest, self-addressing derivation (`G`).
69    #[strum(serialize = "G")]
70    Blake2s_256,
71    /// SHA3-256 digest, self-addressing derivation (`H`).
72    #[strum(serialize = "H")]
73    SHA3_256,
74    /// SHA2-256 digest, self-addressing derivation (`I`).
75    #[strum(serialize = "I")]
76    SHA2_256,
77    /// ECDSA secp256k1 256-bit random seed for a private key (`J`).
78    #[strum(serialize = "J")]
79    ECDSA256k1Seed,
80    /// Ed448 448-bit random seed for a private key (`K`).
81    #[strum(serialize = "K")]
82    Ed448Seed,
83    /// X448 public encryption key, converted from Ed448 (`L`).
84    #[strum(serialize = "L")]
85    X448,
86    /// Short 2-byte integer (`M`).
87    #[strum(serialize = "M")]
88    Short,
89    /// Big 8-byte integer (`N`).
90    #[strum(serialize = "N")]
91    Big,
92    /// X25519 private decryption key/seed, may be converted from Ed25519 (`O`).
93    #[strum(serialize = "O")]
94    X25519Private,
95    /// X25519 sealed box 124-char qb64 cipher of a 44-char qb64 seed (`P`).
96    #[strum(serialize = "P")]
97    X25519CipherSeed,
98    /// ECDSA secp256r1 256-bit random seed for a private key (`Q`).
99    #[strum(serialize = "Q")]
100    ECDSA256r1Seed,
101    /// Tall 5-byte integer (`R`).
102    #[strum(serialize = "R")]
103    Tall,
104    /// Large 11-byte integer (`S`).
105    #[strum(serialize = "S")]
106    Large,
107    /// Great 14-byte integer (`T`).
108    #[strum(serialize = "T")]
109    Great,
110    /// Vast 17-byte integer (`U`).
111    #[strum(serialize = "U")]
112    Vast,
113    /// Label1: 1-byte label with lead size 1 (`V`).
114    #[strum(serialize = "V")]
115    Label1,
116    /// Label2: 2-byte label with lead size 0 (`W`).
117    #[strum(serialize = "W")]
118    Label2,
119    /// Tag3: 3 Base64-encoded chars for special values (`X`).
120    #[strum(serialize = "X")]
121    Tag3,
122    /// Tag7: 7 Base64-encoded chars for special values (`Y`).
123    #[strum(serialize = "Y")]
124    Tag7,
125    /// Tag11: 11 Base64-encoded chars for special values (`Z`).
126    #[strum(serialize = "Z")]
127    Tag11,
128    /// Salt/seed/nonce/blind, 256 bits (`a`).
129    #[strum(serialize = "a")]
130    Salt256,
131    /// Salt/seed/nonce, 128 bits, or "Huge" number (`0A`).
132    #[strum(serialize = "0A")]
133    Salt128,
134    /// Ed25519 signature (`0B`).
135    #[strum(serialize = "0B")]
136    Ed25519Sig,
137    /// ECDSA secp256k1 signature (`0C`).
138    #[strum(serialize = "0C")]
139    ECDSA256k1Sig,
140    /// BLAKE3 512-bit digest, self-addressing derivation (`0D`).
141    #[strum(serialize = "0D")]
142    Blake3_512,
143    /// `BLAKE2b` 512-bit digest, self-addressing derivation (`0E`).
144    #[strum(serialize = "0E")]
145    Blake2b_512,
146    /// SHA3-512 digest, self-addressing derivation (`0F`).
147    #[strum(serialize = "0F")]
148    SHA3_512,
149    /// SHA2-512 digest, self-addressing derivation (`0G`).
150    #[strum(serialize = "0G")]
151    SHA2_512,
152    /// Long 4-byte integer (`0H`).
153    #[strum(serialize = "0H")]
154    Long,
155    /// ECDSA secp256r1 signature (`0I`).
156    #[strum(serialize = "0I")]
157    ECDSA256r1Sig,
158    /// Tag1: 1 Base64 char + 1 prepad for special values (`0J`).
159    #[strum(serialize = "0J")]
160    Tag1,
161    /// Tag2: 2 Base64-encoded chars for special values (`0K`).
162    #[strum(serialize = "0K")]
163    Tag2,
164    /// Tag5: 5 Base64-encoded chars + 1 prepad for special values (`0L`).
165    #[strum(serialize = "0L")]
166    Tag5,
167    /// Tag6: 6 Base64-encoded chars for special values (`0M`).
168    #[strum(serialize = "0M")]
169    Tag6,
170    /// Tag9: 9 Base64-encoded chars + 1 prepad for special values (`0N`).
171    #[strum(serialize = "0N")]
172    Tag9,
173    /// Tag10: 10 Base64-encoded chars for special values (`0O`).
174    #[strum(serialize = "0O")]
175    Tag10,
176    /// `GramHeadNeck`: 32 Base64 chars for a memogram head with neck (`0P`).
177    #[strum(serialize = "0P")]
178    GramHeadNeck,
179    /// `GramHead`: 28 Base64 chars for a memogram head only (`0Q`).
180    #[strum(serialize = "0Q")]
181    GramHead,
182    /// `GramHeadAIDNeck`: 76 Base64 chars for a memogram head with AID and neck (`0R`).
183    #[strum(serialize = "0R")]
184    GramHeadAIDNeck,
185    /// `GramHeadAID`: 72 Base64 chars for a memogram head with AID only (`0S`).
186    #[strum(serialize = "0S")]
187    GramHeadAID,
188    /// ECDSA secp256k1 verification key, non-transferable basic derivation (`1AAA`).
189    #[strum(serialize = "1AAA")]
190    ECDSA256k1N,
191    /// ECDSA secp256k1 public verification or encryption key, basic derivation (`1AAB`).
192    #[strum(serialize = "1AAB")]
193    ECDSA256k1,
194    /// Ed448 non-transferable public signing verification key, basic derivation (`1AAC`).
195    #[strum(serialize = "1AAC")]
196    Ed448N,
197    /// Ed448 public signing verification key, basic derivation (`1AAD`).
198    #[strum(serialize = "1AAD")]
199    Ed448,
200    /// Ed448 signature, self-signing derivation (`1AAE`).
201    #[strum(serialize = "1AAE")]
202    Ed448Sig,
203    /// Tag4: 4 Base64-encoded chars for special values (`1AAF`).
204    #[strum(serialize = "1AAF")]
205    Tag4,
206    /// Base64 custom-encoded 32-char ISO-8601 `DateTime` (`1AAG`).
207    #[strum(serialize = "1AAG")]
208    DateTime,
209    /// X25519 sealed box 100-char qb64 cipher of a 24-char qb64 salt (`1AAH`).
210    #[strum(serialize = "1AAH")]
211    X25519CipherSalt,
212    /// ECDSA secp256r1 verification key, non-transferable basic derivation (`1AAI`).
213    #[strum(serialize = "1AAI")]
214    ECDSA256r1N,
215    /// ECDSA secp256r1 verification or encryption key, basic derivation (`1AAJ`).
216    #[strum(serialize = "1AAJ")]
217    ECDSA256r1,
218    /// Null / None / empty value (`1AAK`).
219    #[strum(serialize = "1AAK")]
220    Null,
221    /// Boolean false value (`1AAL`).
222    #[strum(serialize = "1AAL")]
223    No,
224    /// Boolean true value (`1AAM`).
225    #[strum(serialize = "1AAM")]
226    Yes,
227    /// Tag8: 8 Base64-encoded chars for special values (`1AAN`).
228    #[strum(serialize = "1AAN")]
229    Tag8,
230    /// Escape code for escaping special map fields (`1AAO`).
231    #[strum(serialize = "1AAO")]
232    Escape,
233    /// Empty value for Nonce, UUID, or related fields (`1AAP`).
234    #[strum(serialize = "1AAP")]
235    Empty,
236    /// Testing only: fixed special values with non-empty raw, lead size 0 (`1__-`).
237    #[strum(serialize = "1__-")]
238    TBD0S,
239    /// Testing only: fixed with lead size 0 (`1___`).
240    #[strum(serialize = "1___")]
241    TBD0,
242    /// Testing only: fixed special values with non-empty raw, lead size 1 (`2__-`).
243    #[strum(serialize = "2__-")]
244    TBD1S,
245    /// Testing only: fixed with lead size 1 (`2___`).
246    #[strum(serialize = "2___")]
247    TBD1,
248    /// Testing only: fixed special values with non-empty raw, lead size 2 (`3__-`).
249    #[strum(serialize = "3__-")]
250    TBD2S,
251    /// Testing only: fixed with lead size 2 (`3___`).
252    #[strum(serialize = "3___")]
253    TBD2,
254    /// Variable-length Base64-only string, lead size 0 (`4A`).
255    #[strum(serialize = "4A")]
256    StrB64_L0,
257    /// Variable-length Base64-only string, lead size 1 (`5A`).
258    #[strum(serialize = "5A")]
259    StrB64_L1,
260    /// Variable-length Base64-only string, lead size 2 (`6A`).
261    #[strum(serialize = "6A")]
262    StrB64_L2,
263    /// Variable-length Base64-only string (big), lead size 0 (`7AAA`).
264    #[strum(serialize = "7AAA")]
265    StrB64Big_L0,
266    /// Variable-length Base64-only string (big), lead size 1 (`8AAA`).
267    #[strum(serialize = "8AAA")]
268    StrB64Big_L1,
269    /// Variable-length Base64-only string (big), lead size 2 (`9AAA`).
270    #[strum(serialize = "9AAA")]
271    StrB64Big_L2,
272    /// Variable-length byte string, lead size 0 (`4B`).
273    #[strum(serialize = "4B")]
274    Bytes_L0,
275    /// Variable-length byte string, lead size 1 (`5B`).
276    #[strum(serialize = "5B")]
277    Bytes_L1,
278    /// Variable-length byte string, lead size 2 (`6B`).
279    #[strum(serialize = "6B")]
280    Bytes_L2,
281    /// Variable-length byte string (big), lead size 0 (`7AAB`).
282    #[strum(serialize = "7AAB")]
283    BytesBig_L0,
284    /// Variable-length byte string (big), lead size 1 (`8AAB`).
285    #[strum(serialize = "8AAB")]
286    BytesBig_L1,
287    /// Variable-length byte string (big), lead size 2 (`9AAB`).
288    #[strum(serialize = "9AAB")]
289    BytesBig_L2,
290    /// X25519 sealed-box cipher of sniffable plaintext, lead size 0 (`4C`).
291    #[strum(serialize = "4C")]
292    X25519Cipher_L0,
293    /// X25519 sealed-box cipher of sniffable plaintext, lead size 1 (`5C`).
294    #[strum(serialize = "5C")]
295    X25519Cipher_L1,
296    /// X25519 sealed-box cipher of sniffable plaintext, lead size 2 (`6C`).
297    #[strum(serialize = "6C")]
298    X25519Cipher_L2,
299    /// X25519 sealed-box cipher of sniffable plaintext (big), lead size 0 (`7AAC`).
300    #[strum(serialize = "7AAC")]
301    X25519CipherBig_L0,
302    /// X25519 sealed-box cipher of sniffable plaintext (big), lead size 1 (`8AAC`).
303    #[strum(serialize = "8AAC")]
304    X25519CipherBig_L1,
305    /// X25519 sealed-box cipher of sniffable plaintext (big), lead size 2 (`9AAC`).
306    #[strum(serialize = "9AAC")]
307    X25519CipherBig_L2,
308    /// X25519 sealed-box cipher of qb64 plaintext, lead size 0 (`4D`).
309    #[strum(serialize = "4D")]
310    X25519CipherQB64_L0,
311    /// X25519 sealed-box cipher of qb64 plaintext, lead size 1 (`5D`).
312    #[strum(serialize = "5D")]
313    X25519CipherQB64_L1,
314    /// X25519 sealed-box cipher of qb64 plaintext, lead size 2 (`6D`).
315    #[strum(serialize = "6D")]
316    X25519CipherQB64_L2,
317    /// X25519 sealed-box cipher of qb64 plaintext (big), lead size 0 (`7AAD`).
318    #[strum(serialize = "7AAD")]
319    X25519CipherQB64Big_L0,
320    /// X25519 sealed-box cipher of qb64 plaintext (big), lead size 1 (`8AAD`).
321    #[strum(serialize = "8AAD")]
322    X25519CipherQB64Big_L1,
323    /// X25519 sealed-box cipher of qb64 plaintext (big), lead size 2 (`9AAD`).
324    #[strum(serialize = "9AAD")]
325    X25519CipherQB64Big_L2,
326    /// X25519 sealed-box cipher of qb2 plaintext, lead size 0 (`4E`).
327    #[strum(serialize = "4E")]
328    X25519CipherQB2_L0,
329    /// X25519 sealed-box cipher of qb2 plaintext, lead size 1 (`5E`).
330    #[strum(serialize = "5E")]
331    X25519CipherQB2_L1,
332    /// X25519 sealed-box cipher of qb2 plaintext, lead size 2 (`6E`).
333    #[strum(serialize = "6E")]
334    X25519CipherQB2_L2,
335    /// X25519 sealed-box cipher of qb2 plaintext (big), lead size 0 (`7AAE`).
336    #[strum(serialize = "7AAE")]
337    X25519CipherQB2Big_L0,
338    /// X25519 sealed-box cipher of qb2 plaintext (big), lead size 1 (`8AAE`).
339    #[strum(serialize = "8AAE")]
340    X25519CipherQB2Big_L1,
341    /// X25519 sealed-box cipher of qb2 plaintext (big), lead size 2 (`9AAE`).
342    #[strum(serialize = "9AAE")]
343    X25519CipherQB2Big_L2,
344    /// HPKE Base cipher of sniffable plaintext, lead size 0 (`4F`).
345    #[strum(serialize = "4F")]
346    HPKEBaseCipher_L0,
347    /// HPKE Base cipher of sniffable plaintext, lead size 1 (`5F`).
348    #[strum(serialize = "5F")]
349    HPKEBaseCipher_L1,
350    /// HPKE Base cipher of sniffable plaintext, lead size 2 (`6F`).
351    #[strum(serialize = "6F")]
352    HPKEBaseCipher_L2,
353    /// HPKE Base cipher of sniffable plaintext (big), lead size 0 (`7AAF`).
354    #[strum(serialize = "7AAF")]
355    HPKEBaseCipherBig_L0,
356    /// HPKE Base cipher of sniffable plaintext (big), lead size 1 (`8AAF`).
357    #[strum(serialize = "8AAF")]
358    HPKEBaseCipherBig_L1,
359    /// HPKE Base cipher of sniffable plaintext (big), lead size 2 (`9AAF`).
360    #[strum(serialize = "9AAF")]
361    HPKEBaseCipherBig_L2,
362    /// Decimal Base64 float/int string, lead size 0 (`4H`).
363    #[strum(serialize = "4H")]
364    Decimal_L0,
365    /// Decimal Base64 float/int string, lead size 1 (`5H`).
366    #[strum(serialize = "5H")]
367    Decimal_L1,
368    /// Decimal Base64 float/int string, lead size 2 (`6H`).
369    #[strum(serialize = "6H")]
370    Decimal_L2,
371    /// Decimal Base64 float/int string (big), lead size 0 (`7AAH`).
372    #[strum(serialize = "7AAH")]
373    DecimalBig_L0,
374    /// Decimal Base64 float/int string (big), lead size 1 (`8AAH`).
375    #[strum(serialize = "8AAH")]
376    DecimalBig_L1,
377    /// Decimal Base64 float/int string (big), lead size 2 (`9AAH`).
378    #[strum(serialize = "9AAH")]
379    DecimalBig_L2,
380}
381
382impl MatterCode {
383    /// Parses a `MatterCode` from a Base64-encoded byte stream.
384    ///
385    /// # Errors
386    ///
387    /// Returns `ParsingError` if the stream is empty, too short, contains
388    /// invalid UTF-8, or does not match a known matter code.
389    pub fn from_base64_stream(stream: &[u8]) -> Result<Self, ParsingError> {
390        let hs: usize = stream
391            .first()
392            .ok_or(ParsingError::EmptyStream)
393            .and_then(|b| {
394                get_hard_size_from_byte(*b).ok_or_else(|| ParsingError::MalformedCode {
395                    part: MatterPart::Head,
396                    found: b.to_string(),
397                })
398            })?
399            .into();
400        if stream.len() < hs {
401            return Err(ParsingError::StreamTooShort(MatterPart::Head));
402        }
403        let code_str = str::from_utf8(&stream[..hs]).map_err(ParsingError::InvalidUtf8)?;
404        let code = Self::from_str(code_str)
405            .map_err(|_| ParsingError::UnknownMatterCode(code_str.to_owned()))?;
406        Ok(code)
407    }
408
409    /// Parses a `MatterCode` from a binary (qb2) byte stream.
410    ///
411    /// # Errors
412    ///
413    /// Returns `ParsingError` if the stream is empty, too short, or does
414    /// not match a known matter code.
415    pub fn from_stream(stream: &[u8]) -> Result<Self, ParsingError> {
416        let hard_size: usize = stream
417            .first()
418            .ok_or(ParsingError::EmptyStream)
419            .map(|b| b >> 2)
420            .and_then(|b| {
421                get_hard_size_from_sextet(b).ok_or_else(|| ParsingError::MalformedCode {
422                    part: MatterPart::Head,
423                    found: b.to_string(),
424                })
425            })?
426            .into();
427
428        let hard_size_nz = NonZeroUsize::new(hard_size).ok_or(ParsingError::EmptyStream)?;
429        let bhs = (hard_size_nz.get() * 3).div_ceil(4);
430        if stream.len() < bhs {
431            return Err(ParsingError::StreamTooShort(MatterPart::Head));
432        }
433        let code_str = encode_binary(stream, hard_size_nz).map_err(ParsingError::Conversion)?;
434        let code = Self::from_str(&code_str)
435            .map_err(|_| ParsingError::UnknownMatterCode(code_str.clone()))?;
436        Ok(code)
437    }
438
439    /// `BextCodex` is codex of all variable sized Base64 Text (Bext) derivation codes.
440    /// Only provide defined codes.
441    /// Undefined are left out so that inclusion(exclusion) via 'in' operator works.
442    #[must_use]
443    pub const fn is_base64_text_code(&self) -> bool {
444        matches!(
445            self,
446            Self::StrB64_L0
447                | Self::StrB64_L1
448                | Self::StrB64_L2
449                | Self::StrB64Big_L0
450                | Self::StrB64Big_L1
451                | Self::StrB64Big_L2
452        )
453    }
454
455    /// `TextCodex` is codex of all variable sized byte string (Text) derivation codes.
456    /// Only provide defined codes.
457    /// Undefined are left out so that inclusion(exclusion) via 'in' operator works.
458    #[must_use]
459    pub const fn is_text_code(&self) -> bool {
460        matches!(
461            self,
462            Self::Bytes_L0
463                | Self::Bytes_L1
464                | Self::Bytes_L2
465                | Self::BytesBig_L0
466                | Self::BytesBig_L1
467                | Self::BytesBig_L2
468        )
469    }
470
471    /// `DecimalCodex` is codex of all variable sized Base64 String representation
472    /// of decimal numbers both signed and unsigned, float and int.
473    /// Only provide defined codes.
474    /// Undefined are left out so that inclusion(exclusion) via 'in' operator works.
475    #[must_use]
476    pub const fn is_base_64_decimal(&self) -> bool {
477        matches!(
478            self,
479            Self::Decimal_L0
480                | Self::Decimal_L1
481                | Self::Decimal_L2
482                | Self::DecimalBig_L0
483                | Self::DecimalBig_L1
484                | Self::DecimalBig_L2
485        )
486    }
487
488    /// `DigCodex` is codex all digest derivation codes. This is needed to ensure
489    /// delegated inception using a self-addressing derivation i.e. digest derivation
490    /// code.
491    /// Only provide defined codes.
492    /// Undefined are left out so that inclusion(exclusion) via 'in' operator works.
493    #[must_use]
494    pub const fn is_digest(&self) -> bool {
495        matches!(
496            self,
497            Self::Blake3_256
498                | Self::Blake2b_256
499                | Self::Blake2s_256
500                | Self::SHA3_256
501                | Self::SHA2_256
502                | Self::Blake3_512
503                | Self::Blake2b_512
504                | Self::SHA3_512
505                | Self::SHA2_512
506        )
507    }
508
509    /// `NonceCodex` is codex all derivation codes for  salty nonces (UUIDs) either
510    /// as random numbers or as digests deterministically derived from salty nonces.
511    /// Only provide defined codes.
512    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
513    #[must_use]
514    pub const fn is_nonce(&self) -> bool {
515        matches!(
516            self,
517            Self::Empty
518                | Self::Salt128
519                | Self::Salt256
520                | Self::Blake3_256
521                | Self::Blake2b_256
522                | Self::Blake2s_256
523                | Self::SHA3_256
524                | Self::SHA2_256
525                | Self::Blake3_512
526                | Self::Blake2b_512
527                | Self::SHA3_512
528                | Self::SHA2_512
529        )
530    }
531
532    /// `NumCodex` is codex of Base64 derivation codes for compactly representing
533    /// numbers across a wide rage of sizes.
534    /// Only provide defined codes.
535    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
536    #[must_use]
537    pub const fn is_base64_num(&self) -> bool {
538        matches!(
539            self,
540            Self::Short
541                | Self::Long
542                | Self::Tall
543                | Self::Big
544                | Self::Large
545                | Self::Great
546                | Self::Vast
547                | Self::Salt128 // this is Huge in keripy :/
548        )
549    }
550
551    /// `TagCodex` is codex of Base64 derivation codes for compactly representing
552    /// various small Base64 tag values as special code soft part values.
553    /// Only provide defined codes.
554    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
555    #[must_use]
556    pub const fn is_base64_tag(&self) -> bool {
557        matches!(
558            self,
559            Self::Tag1
560                | Self::Tag2
561                | Self::Tag3
562                | Self::Tag4
563                | Self::Tag5
564                | Self::Tag6
565                | Self::Tag7
566                | Self::Tag8
567                | Self::Tag9
568                | Self::Tag10
569                | Self::Tag11
570        )
571    }
572
573    /// `LabelCodex` is codex of codes to compactly ser/des labels and string values
574    /// in maps or lists.
575    /// Only provide defined codes.
576    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
577    #[must_use]
578    pub const fn is_label(&self) -> bool {
579        matches!(
580            self,
581            Self::Empty
582                | Self::Tag1
583                | Self::Tag2
584                | Self::Tag3
585                | Self::Tag4
586                | Self::Tag5
587                | Self::Tag6
588                | Self::Tag7
589                | Self::Tag8
590                | Self::Tag9
591                | Self::Tag10
592                | Self::Tag11
593                | Self::StrB64_L0
594                | Self::StrB64_L1
595                | Self::StrB64_L2
596                | Self::StrB64Big_L0
597                | Self::StrB64Big_L1
598                | Self::StrB64Big_L2
599                | Self::Label1
600                | Self::Label2
601                | Self::Bytes_L0
602                | Self::Bytes_L1
603                | Self::Bytes_L2
604                | Self::BytesBig_L0
605                | Self::BytesBig_L1
606                | Self::BytesBig_L2
607        )
608    }
609
610    /// `PreCodex` is codex all identifier prefix derivation codes.
611    /// This is needed to verify valid inception events.
612    /// Only provide defined codes.
613    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
614    #[must_use]
615    pub const fn is_prefix_derivation(&self) -> bool {
616        matches!(
617            self,
618            Self::Ed25519N
619                | Self::Ed25519
620                | Self::Blake3_256
621                | Self::Blake2b_256
622                | Self::Blake2s_256
623                | Self::SHA3_256
624                | Self::SHA2_256
625                | Self::Blake3_512
626                | Self::Blake2b_512
627                | Self::SHA3_512
628                | Self::SHA2_512
629                | Self::ECDSA256k1N
630                | Self::ECDSA256k1
631                | Self::Ed448N
632                | Self::Ed448
633                | Self::Ed448Sig
634                | Self::ECDSA256r1N
635                | Self::ECDSA256r1
636        )
637    }
638
639    /// `NonTransCodex` is codex all non-transferable derivation codes
640    ///Only provide defined codes.
641    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
642    #[must_use]
643    pub const fn is_non_transferable(&self) -> bool {
644        matches!(
645            self,
646            Self::Ed25519N | Self::ECDSA256k1N | Self::Ed448N | Self::ECDSA256r1N
647        )
648    }
649
650    /// `PreNonDigCodex` is codex all prefixive but non-digestive derivation codes
651    /// Only provide defined codes.
652    /// Undefined are left out so that inclusion(exclusion) via "in" operator works.
653    #[must_use]
654    pub const fn is_prefix_non_digestive(&self) -> bool {
655        matches!(
656            self,
657            Self::Ed25519N
658                | Self::Ed25519
659                | Self::ECDSA256k1N
660                | Self::ECDSA256k1
661                | Self::Ed448N
662                | Self::Ed448
663                | Self::ECDSA256r1N
664                | Self::ECDSA256r1
665        )
666    }
667
668    /// Returns the `Sizage` descriptor for this code.
669    #[must_use]
670    #[allow(
671        clippy::too_many_lines,
672        reason = "exhaustive lookup table over 110 CESR code variants"
673    )]
674    pub const fn get_sizage(&self) -> Sizage {
675        match self {
676            Self::Ed25519Seed
677            | Self::Ed25519N
678            | Self::X25519
679            | Self::Ed25519
680            | Self::Blake3_256
681            | Self::Blake2b_256
682            | Self::Blake2s_256
683            | Self::SHA3_256
684            | Self::SHA2_256
685            | Self::ECDSA256k1Seed
686            | Self::X25519Private
687            | Self::ECDSA256r1Seed
688            | Self::Salt256 => Sizage::new(1, 0, 0, SizeType::Fixed(44), 0),
689            Self::Ed448Seed | Self::X448 => Sizage::new(1, 0, 0, SizeType::Fixed(76), 0),
690            Self::Short | Self::Label2 => Sizage::new(1, 0, 0, SizeType::Fixed(4), 0),
691            Self::Big => Sizage::new(1, 0, 0, SizeType::Fixed(12), 0),
692            Self::X25519CipherSeed => Sizage::new(1, 0, 0, SizeType::Fixed(124), 0),
693            Self::Tall => Sizage::new(1, 0, 0, SizeType::Fixed(8), 0),
694            Self::Large => Sizage::new(1, 0, 0, SizeType::Fixed(16), 0),
695            Self::Great => Sizage::new(1, 0, 0, SizeType::Fixed(20), 0),
696            Self::Vast => Sizage::new(1, 0, 0, SizeType::Fixed(24), 0),
697            Self::Label1 => Sizage::new(1, 0, 0, SizeType::Fixed(4), 1),
698            Self::Tag3 => Sizage::new(1, 3, 0, SizeType::Fixed(4), 0),
699            Self::Tag7 => Sizage::new(1, 7, 0, SizeType::Fixed(8), 0),
700            Self::Tag11 => Sizage::new(1, 11, 0, SizeType::Fixed(12), 0),
701            Self::Salt128 => Sizage::new(2, 0, 0, SizeType::Fixed(24), 0),
702            Self::Ed25519Sig
703            | Self::ECDSA256k1Sig
704            | Self::Blake3_512
705            | Self::Blake2b_512
706            | Self::SHA3_512
707            | Self::SHA2_512
708            | Self::ECDSA256r1Sig => Sizage::new(2, 0, 0, SizeType::Fixed(88), 0),
709            Self::Long => Sizage::new(2, 0, 0, SizeType::Fixed(8), 0),
710            Self::Tag1 => Sizage::new(2, 2, 1, SizeType::Fixed(4), 0),
711            Self::Tag2 => Sizage::new(2, 2, 0, SizeType::Fixed(4), 0),
712            Self::Tag5 => Sizage::new(2, 6, 1, SizeType::Fixed(8), 0),
713            Self::Tag6 => Sizage::new(2, 6, 0, SizeType::Fixed(8), 0),
714            Self::Tag9 => Sizage::new(2, 10, 1, SizeType::Fixed(12), 0),
715            Self::Tag10 => Sizage::new(2, 10, 0, SizeType::Fixed(12), 0),
716            Self::GramHeadNeck => Sizage::new(2, 22, 0, SizeType::Fixed(32), 0),
717            Self::GramHead => Sizage::new(2, 22, 0, SizeType::Fixed(28), 0),
718            Self::GramHeadAIDNeck => Sizage::new(2, 22, 0, SizeType::Fixed(76), 0),
719            Self::GramHeadAID => Sizage::new(2, 22, 0, SizeType::Fixed(72), 0),
720            Self::ECDSA256k1N | Self::ECDSA256k1 | Self::ECDSA256r1N | Self::ECDSA256r1 => {
721                Sizage::new(4, 0, 0, SizeType::Fixed(48), 0)
722            }
723            Self::Ed448N | Self::Ed448 => Sizage::new(4, 0, 0, SizeType::Fixed(80), 0),
724            Self::Ed448Sig => Sizage::new(4, 0, 0, SizeType::Fixed(156), 0),
725            Self::Tag4 => Sizage::new(4, 4, 0, SizeType::Fixed(8), 0),
726            Self::DateTime => Sizage::new(4, 0, 0, SizeType::Fixed(36), 0),
727            Self::X25519CipherSalt => Sizage::new(4, 0, 0, SizeType::Fixed(100), 0),
728            Self::Null | Self::No | Self::Yes | Self::Escape | Self::Empty => {
729                Sizage::new(4, 0, 0, SizeType::Fixed(4), 0)
730            }
731            Self::Tag8 => Sizage::new(4, 8, 0, SizeType::Fixed(12), 0),
732            Self::TBD0S => Sizage::new(4, 2, 0, SizeType::Fixed(12), 0),
733            Self::TBD0 => Sizage::new(4, 0, 0, SizeType::Fixed(8), 0),
734            Self::TBD1S => Sizage::new(4, 2, 1, SizeType::Fixed(12), 1),
735            Self::TBD1 => Sizage::new(4, 0, 0, SizeType::Fixed(8), 1),
736            Self::TBD2S => Sizage::new(4, 2, 0, SizeType::Fixed(12), 2),
737            Self::TBD2 => Sizage::new(4, 0, 0, SizeType::Fixed(8), 2),
738            Self::StrB64_L0
739            | Self::Bytes_L0
740            | Self::X25519Cipher_L0
741            | Self::X25519CipherQB64_L0
742            | Self::X25519CipherQB2_L0
743            | Self::HPKEBaseCipher_L0
744            | Self::Decimal_L0 => Sizage::new(2, 2, 0, SizeType::Small, 0),
745            Self::StrB64_L1
746            | Self::Bytes_L1
747            | Self::X25519Cipher_L1
748            | Self::X25519CipherQB64_L1
749            | Self::X25519CipherQB2_L1
750            | Self::HPKEBaseCipher_L1
751            | Self::Decimal_L1 => Sizage::new(2, 2, 0, SizeType::Small, 1),
752            Self::StrB64_L2
753            | Self::Bytes_L2
754            | Self::X25519Cipher_L2
755            | Self::X25519CipherQB64_L2
756            | Self::X25519CipherQB2_L2
757            | Self::HPKEBaseCipher_L2
758            | Self::Decimal_L2 => Sizage::new(2, 2, 0, SizeType::Small, 2),
759            Self::StrB64Big_L0
760            | Self::BytesBig_L0
761            | Self::X25519CipherBig_L0
762            | Self::X25519CipherQB64Big_L0
763            | Self::X25519CipherQB2Big_L0
764            | Self::HPKEBaseCipherBig_L0
765            | Self::DecimalBig_L0 => Sizage::new(4, 4, 0, SizeType::Large, 0),
766            Self::StrB64Big_L1
767            | Self::BytesBig_L1
768            | Self::X25519CipherBig_L1
769            | Self::X25519CipherQB64Big_L1
770            | Self::X25519CipherQB2Big_L1
771            | Self::HPKEBaseCipherBig_L1
772            | Self::DecimalBig_L1 => Sizage::new(4, 4, 0, SizeType::Large, 1),
773            Self::StrB64Big_L2
774            | Self::BytesBig_L2
775            | Self::X25519CipherBig_L2
776            | Self::X25519CipherQB64Big_L2
777            | Self::X25519CipherQB2Big_L2
778            | Self::HPKEBaseCipherBig_L2
779            | Self::DecimalBig_L2 => Sizage::new(4, 4, 0, SizeType::Large, 2),
780        }
781    }
782
783    /// Promotes this variable-size code to the appropriate variant for the given size and lead.
784    ///
785    /// # Errors
786    ///
787    /// Returns `ValidationError` if the code cannot be promoted (e.g. fixed-size code,
788    /// size exceeds capacity, or invalid lead).
789    pub fn promote(&self, size: usize, lead: usize) -> Result<Self, ValidationError> {
790        match self.get_sizage().fs() {
791            SizeType::Small if size < 64_usize.pow(2) => {
792                promote_same_size(*self, &SMALL_VAR_SIZES, lead, 2)
793            }
794            SizeType::Small if size < 64_usize.pow(4) => {
795                promote_small_to_large(*self, &LARGE_VAR_SIZES, lead, 4)
796            }
797            SizeType::Large if size < (64_usize.pow(4) - 1) => {
798                promote_same_size(*self, &LARGE_VAR_SIZES, lead, 4)
799            }
800            _ => Err(ValidationError::IncompatiblePromotion(self.to_string())),
801        }
802    }
803
804    /// Returns raw size in bytes not including leader for a given code.
805    ///
806    /// # Errors
807    ///
808    /// Returns `ValidationError::InvalidSizingOperation` if the code is variable-size.
809    pub fn raw_size(&self) -> Result<usize, ValidationError> {
810        let Sizage { hs, ss, fs, ls, .. } = self.get_sizage();
811        if let SizeType::Fixed(v) = fs {
812            let code_size = usize::from(hs) + usize::from(ss);
813            let raw_size = ((usize::from(v) - code_size) * 3 / 4) - usize::from(ls);
814            return Ok(raw_size);
815        }
816        Err(ValidationError::InvalidSizingOperation(self.to_string()))
817    }
818
819    /// Returns `true` if this code is a special fixed-size code with soft data.
820    #[must_use]
821    pub const fn is_special(&self) -> bool {
822        let Sizage { fs, ss, .. } = self.get_sizage();
823        matches!(fs, SizeType::Fixed(_)) && ss > 0
824    }
825}
826
827// does the actual promotion of the code
828fn promote_same_size(
829    matter_code: MatterCode,
830    code_map: &[char],
831    lead: usize,
832    hs: usize,
833) -> Result<MatterCode, ValidationError> {
834    let s = code_map
835        .get(lead)
836        .ok_or_else(|| ValidationError::InvalidPromotionTarget {
837            code: matter_code.to_string(),
838            lead,
839        })?;
840    let code_str: &str = matter_code.as_ref();
841    let code = format!("{s}{}", &code_str[1..hs]);
842    MatterCode::try_from(code.as_str()).map_err(|_| ValidationError::InvalidPromotionResult {
843        from: code_str.to_owned(),
844        to: code,
845    })
846}
847
848fn promote_small_to_large(
849    matter_code: MatterCode,
850    code_map: &[char],
851    lead: usize,
852    hs: usize,
853) -> Result<MatterCode, ValidationError> {
854    let s = code_map
855        .get(lead)
856        .ok_or_else(|| ValidationError::InvalidPromotionTarget {
857            code: matter_code.to_string(),
858            lead,
859        })?;
860    let code_str: &str = matter_code.as_ref();
861    let code = format!("{s}{}{}", &"AAAA"[0..hs - 2], &code_str[1..2]);
862    MatterCode::try_from(code.as_str()).map_err(|_| ValidationError::InvalidPromotionResult {
863        from: code_str.to_owned(),
864        to: code,
865    })
866}
867
868impl Sealed for MatterCode {}
869
870impl CesrCode for MatterCode {
871    fn to_matter_code(&self) -> MatterCode {
872        *self
873    }
874
875    fn as_str(&self) -> &'static str {
876        self.into() // uses IntoStaticStr from strum
877    }
878
879    // get_sizage and raw_size use default impls which delegate to to_matter_code()
880}
881
882#[cfg(test)]
883#[allow(
884    clippy::panic,
885    clippy::too_many_arguments,
886    reason = "tests use panic for assertions; rstest parameterized tests exceed argument limit"
887)]
888mod tests {
889    use super::*;
890    use core::str::FromStr;
891    use rstest::rstest;
892    use strum::{IntoEnumIterator, ParseError};
893
894    macro_rules! with_payload {
895        ($bytes:expr) => {{
896            const PAYLOAD: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF];
897            let mut vec = Vec::from($bytes);
898            vec.extend_from_slice(PAYLOAD);
899            vec
900        }};
901    }
902
903    #[test]
904    fn test_codex_string_roundtrip() {
905        for variant in MatterCode::iter() {
906            let code_str: &'static str = variant.into();
907            let code_string = variant.to_string();
908            assert_eq!(code_str, code_string);
909
910            // Test EnumString (FromStr)
911            let parsed_variant = MatterCode::from_str(code_str).unwrap();
912            assert_eq!(variant, parsed_variant);
913
914            // Test AsRefStr
915            let code_as_ref: &str = variant.as_ref();
916            assert_eq!(code_str, code_as_ref);
917        }
918    }
919
920    #[test]
921    fn test_invalid_code_parsing() {
922        let result = MatterCode::from_str("!NOT_A_CODE!");
923        assert!(result.is_err());
924        let err = result.unwrap_err();
925        assert_eq!(err, ParseError::VariantNotFound);
926    }
927
928    #[test]
929    fn spot_check_codes() {
930        assert_eq!(MatterCode::Ed25519N.as_ref(), "B");
931    }
932
933    #[rstest]
934    #[case("A_payload", Ok(MatterCode::Ed25519Seed))]
935    #[case("B_payload", Ok(MatterCode::Ed25519N))]
936    #[case("C_payload", Ok(MatterCode::X25519))]
937    #[case("D_payload", Ok(MatterCode::Ed25519))]
938    #[case("E_payload", Ok(MatterCode::Blake3_256))]
939    #[case("F_payload", Ok(MatterCode::Blake2b_256))]
940    #[case("G_payload", Ok(MatterCode::Blake2s_256))]
941    #[case("H_payload", Ok(MatterCode::SHA3_256))]
942    #[case("I_payload", Ok(MatterCode::SHA2_256))]
943    #[case("J_payload", Ok(MatterCode::ECDSA256k1Seed))]
944    #[case("K_payload", Ok(MatterCode::Ed448Seed))]
945    #[case("L_payload", Ok(MatterCode::X448))]
946    #[case("M_payload", Ok(MatterCode::Short))]
947    #[case("N_payload", Ok(MatterCode::Big))]
948    #[case("O_payload", Ok(MatterCode::X25519Private))]
949    #[case("P_payload", Ok(MatterCode::X25519CipherSeed))]
950    #[case("Q_payload", Ok(MatterCode::ECDSA256r1Seed))]
951    #[case("R_payload", Ok(MatterCode::Tall))]
952    #[case("S_payload", Ok(MatterCode::Large))]
953    #[case("T_payload", Ok(MatterCode::Great))]
954    #[case("U_payload", Ok(MatterCode::Vast))]
955    #[case("V_payload", Ok(MatterCode::Label1))]
956    #[case("W_payload", Ok(MatterCode::Label2))]
957    #[case("X_payload", Ok(MatterCode::Tag3))]
958    #[case("Y_payload", Ok(MatterCode::Tag7))]
959    #[case("Z_payload", Ok(MatterCode::Tag11))]
960    #[case("a_payload", Ok(MatterCode::Salt256))]
961    #[case("0A_payload", Ok(MatterCode::Salt128))]
962    #[case("0B_payload", Ok(MatterCode::Ed25519Sig))]
963    #[case("0C_payload", Ok(MatterCode::ECDSA256k1Sig))]
964    #[case("0D_payload", Ok(MatterCode::Blake3_512))]
965    #[case("0E_payload", Ok(MatterCode::Blake2b_512))]
966    #[case("0F_payload", Ok(MatterCode::SHA3_512))]
967    #[case("0G_payload", Ok(MatterCode::SHA2_512))]
968    #[case("0H_payload", Ok(MatterCode::Long))]
969    #[case("0I_payload", Ok(MatterCode::ECDSA256r1Sig))]
970    #[case("0J_payload", Ok(MatterCode::Tag1))]
971    #[case("0K_payload", Ok(MatterCode::Tag2))]
972    #[case("0L_payload", Ok(MatterCode::Tag5))]
973    #[case("0M_payload", Ok(MatterCode::Tag6))]
974    #[case("0N_payload", Ok(MatterCode::Tag9))]
975    #[case("0O_payload", Ok(MatterCode::Tag10))]
976    #[case("0P_payload", Ok(MatterCode::GramHeadNeck))]
977    #[case("0Q_payload", Ok(MatterCode::GramHead))]
978    #[case("0R_payload", Ok(MatterCode::GramHeadAIDNeck))]
979    #[case("0S_payload", Ok(MatterCode::GramHeadAID))]
980    #[case("1AAA_payload", Ok(MatterCode::ECDSA256k1N))]
981    #[case("1AAB_payload", Ok(MatterCode::ECDSA256k1))]
982    #[case("1AAC_payload", Ok(MatterCode::Ed448N))]
983    #[case("1AAD_payload", Ok(MatterCode::Ed448))]
984    #[case("1AAE_payload", Ok(MatterCode::Ed448Sig))]
985    #[case("1AAF_payload", Ok(MatterCode::Tag4))]
986    #[case("1AAG_payload", Ok(MatterCode::DateTime))]
987    #[case("1AAH_payload", Ok(MatterCode::X25519CipherSalt))]
988    #[case("1AAI_payload", Ok(MatterCode::ECDSA256r1N))]
989    #[case("1AAJ_payload", Ok(MatterCode::ECDSA256r1))]
990    #[case("1AAK_payload", Ok(MatterCode::Null))]
991    #[case("1AAL_payload", Ok(MatterCode::No))]
992    #[case("1AAM_payload", Ok(MatterCode::Yes))]
993    #[case("1AAN_payload", Ok(MatterCode::Tag8))]
994    #[case("1AAO_payload", Ok(MatterCode::Escape))]
995    #[case("1AAP_payload", Ok(MatterCode::Empty))]
996    #[case("1__-_payload", Ok(MatterCode::TBD0S))]
997    #[case("1____payload", Ok(MatterCode::TBD0))]
998    #[case("2__-_payload", Ok(MatterCode::TBD1S))]
999    #[case("2____payload", Ok(MatterCode::TBD1))]
1000    #[case("3__-_payload", Ok(MatterCode::TBD2S))]
1001    #[case("3____payload", Ok(MatterCode::TBD2))]
1002    #[case("4A_payload", Ok(MatterCode::StrB64_L0))]
1003    #[case("5A_payload", Ok(MatterCode::StrB64_L1))]
1004    #[case("6A_payload", Ok(MatterCode::StrB64_L2))]
1005    #[case("7AAA_payload", Ok(MatterCode::StrB64Big_L0))]
1006    #[case("8AAA_payload", Ok(MatterCode::StrB64Big_L1))]
1007    #[case("9AAA_payload", Ok(MatterCode::StrB64Big_L2))]
1008    #[case("4B_payload", Ok(MatterCode::Bytes_L0))]
1009    #[case("5B_payload", Ok(MatterCode::Bytes_L1))]
1010    #[case("6B_payload", Ok(MatterCode::Bytes_L2))]
1011    #[case("7AAB_payload", Ok(MatterCode::BytesBig_L0))]
1012    #[case("8AAB_payload", Ok(MatterCode::BytesBig_L1))]
1013    #[case("9AAB_payload", Ok(MatterCode::BytesBig_L2))]
1014    #[case("4C_payload", Ok(MatterCode::X25519Cipher_L0))]
1015    #[case("5C_payload", Ok(MatterCode::X25519Cipher_L1))]
1016    #[case("6C_payload", Ok(MatterCode::X25519Cipher_L2))]
1017    #[case("7AAC_payload", Ok(MatterCode::X25519CipherBig_L0))]
1018    #[case("8AAC_payload", Ok(MatterCode::X25519CipherBig_L1))]
1019    #[case("9AAC_payload", Ok(MatterCode::X25519CipherBig_L2))]
1020    #[case("4D_payload", Ok(MatterCode::X25519CipherQB64_L0))]
1021    #[case("5D_payload", Ok(MatterCode::X25519CipherQB64_L1))]
1022    #[case("6D_payload", Ok(MatterCode::X25519CipherQB64_L2))]
1023    #[case("7AAD_payload", Ok(MatterCode::X25519CipherQB64Big_L0))]
1024    #[case("8AAD_payload", Ok(MatterCode::X25519CipherQB64Big_L1))]
1025    #[case("9AAD_payload", Ok(MatterCode::X25519CipherQB64Big_L2))]
1026    #[case("4E_payload", Ok(MatterCode::X25519CipherQB2_L0))]
1027    #[case("5E_payload", Ok(MatterCode::X25519CipherQB2_L1))]
1028    #[case("6E_payload", Ok(MatterCode::X25519CipherQB2_L2))]
1029    #[case("7AAE_payload", Ok(MatterCode::X25519CipherQB2Big_L0))]
1030    #[case("8AAE_payload", Ok(MatterCode::X25519CipherQB2Big_L1))]
1031    #[case("9AAE_payload", Ok(MatterCode::X25519CipherQB2Big_L2))]
1032    #[case("4F_payload", Ok(MatterCode::HPKEBaseCipher_L0))]
1033    #[case("5F_payload", Ok(MatterCode::HPKEBaseCipher_L1))]
1034    #[case("6F_payload", Ok(MatterCode::HPKEBaseCipher_L2))]
1035    #[case("7AAF_payload", Ok(MatterCode::HPKEBaseCipherBig_L0))]
1036    #[case("8AAF_payload", Ok(MatterCode::HPKEBaseCipherBig_L1))]
1037    #[case("9AAF_payload", Ok(MatterCode::HPKEBaseCipherBig_L2))]
1038    #[case("4H_payload", Ok(MatterCode::Decimal_L0))]
1039    #[case("5H_payload", Ok(MatterCode::Decimal_L1))]
1040    #[case("6H_payload", Ok(MatterCode::Decimal_L2))]
1041    #[case("7AAH_payload", Ok(MatterCode::DecimalBig_L0))]
1042    #[case("8AAH_payload", Ok(MatterCode::DecimalBig_L1))]
1043    #[case("9AAH_payload", Ok(MatterCode::DecimalBig_L2))]
1044    fn test_from_base64_stream_valid(
1045        #[case] input: &str,
1046        #[case] expected: Result<MatterCode, ParsingError>,
1047    ) {
1048        assert_eq!(
1049            MatterCode::from_base64_stream(input.as_bytes()).unwrap(),
1050            expected.unwrap()
1051        );
1052    }
1053
1054    #[rstest]
1055    // 1-char codes (1 byte)
1056    #[case(with_payload!(&[0x00]), Ok(MatterCode::Ed25519Seed))]
1057    #[case(with_payload!(&[0x04]), Ok(MatterCode::Ed25519N))]
1058    #[case(with_payload!(&[0x08]), Ok(MatterCode::X25519))]
1059    #[case(with_payload!(&[0x0C]), Ok(MatterCode::Ed25519))]
1060    #[case(with_payload!(&[0x10]), Ok(MatterCode::Blake3_256))]
1061    #[case(with_payload!(&[0x14]), Ok(MatterCode::Blake2b_256))]
1062    #[case(with_payload!(&[0x18]), Ok(MatterCode::Blake2s_256))]
1063    #[case(with_payload!(&[0x1C]), Ok(MatterCode::SHA3_256))]
1064    #[case(with_payload!(&[0x20]), Ok(MatterCode::SHA2_256))]
1065    #[case(with_payload!(&[0x24]), Ok(MatterCode::ECDSA256k1Seed))]
1066    #[case(with_payload!(&[0x28]), Ok(MatterCode::Ed448Seed))]
1067    #[case(with_payload!(&[0x2C]), Ok(MatterCode::X448))]
1068    #[case(with_payload!(&[0x30]), Ok(MatterCode::Short))]
1069    #[case(with_payload!(&[0x34]), Ok(MatterCode::Big))]
1070    #[case(with_payload!(&[0x38]), Ok(MatterCode::X25519Private))]
1071    #[case(with_payload!(&[0x3C]), Ok(MatterCode::X25519CipherSeed))]
1072    #[case(with_payload!(&[0x40]), Ok(MatterCode::ECDSA256r1Seed))]
1073    #[case(with_payload!(&[0x44]), Ok(MatterCode::Tall))]
1074    #[case(with_payload!(&[0x48]), Ok(MatterCode::Large))]
1075    #[case(with_payload!(&[0x4C]), Ok(MatterCode::Great))]
1076    #[case(with_payload!(&[0x50]), Ok(MatterCode::Vast))]
1077    #[case(with_payload!(&[0x54]), Ok(MatterCode::Label1))]
1078    #[case(with_payload!(&[0x58]), Ok(MatterCode::Label2))]
1079    #[case(with_payload!(&[0x5C]), Ok(MatterCode::Tag3))]
1080    #[case(with_payload!(&[0x60]), Ok(MatterCode::Tag7))]
1081    #[case(with_payload!(&[0x64]), Ok(MatterCode::Tag11))]
1082    #[case(with_payload!(&[0x68]), Ok(MatterCode::Salt256))]
1083    // 2-char codes (2 bytes)
1084    #[case(with_payload!(&[0xD0, 0x08]), Ok(MatterCode::Salt128))]
1085    #[case(with_payload!(&[0xD0, 0x1F]), Ok(MatterCode::Ed25519Sig))]
1086    #[case(with_payload!(&[0xD0, 0x20]), Ok(MatterCode::ECDSA256k1Sig))]
1087    #[case(with_payload!(&[0xD0, 0x30]), Ok(MatterCode::Blake3_512))]
1088    #[case(with_payload!(&[0xD0, 0x40]), Ok(MatterCode::Blake2b_512))]
1089    #[case(with_payload!(&[0xD0, 0x50]), Ok(MatterCode::SHA3_512))]
1090    #[case(with_payload!(&[0xD0, 0x60]), Ok(MatterCode::SHA2_512))]
1091    #[case(with_payload!(&[0xD0, 0x70]), Ok(MatterCode::Long))]
1092    #[case(with_payload!(&[0xD0, 0x80]), Ok(MatterCode::ECDSA256r1Sig))]
1093    #[case(with_payload!(&[0xD0, 0x90]), Ok(MatterCode::Tag1))]
1094    #[case(with_payload!(&[0xD0, 0xA0]), Ok(MatterCode::Tag2))]
1095    #[case(with_payload!(&[0xD0, 0xB0]), Ok(MatterCode::Tag5))]
1096    #[case(with_payload!(&[0xD0, 0xC0]), Ok(MatterCode::Tag6))]
1097    #[case(with_payload!(&[0xD0, 0xD0]), Ok(MatterCode::Tag9))]
1098    #[case(with_payload!(&[0xD0, 0xE0]), Ok(MatterCode::Tag10))]
1099    #[case(with_payload!(&[0xD0, 0xF0]), Ok(MatterCode::GramHeadNeck))]
1100    #[case(with_payload!(&[0xD1, 0x00]), Ok(MatterCode::GramHead))]
1101    #[case(with_payload!(&[0xD1, 0x10]), Ok(MatterCode::GramHeadAIDNeck))]
1102    #[case(with_payload!(&[0xD1, 0x20]), Ok(MatterCode::GramHeadAID))]
1103    // // 4-char codes (3 bytes)
1104    #[case(with_payload!(&[0xD4, 0x00, 0x00]), Ok(MatterCode::ECDSA256k1N))]
1105    #[case(with_payload!(&[0xD4, 0x00, 0x01]), Ok(MatterCode::ECDSA256k1))]
1106    #[case(with_payload!(&[0xD4, 0x00, 0x02]), Ok(MatterCode::Ed448N))]
1107    #[case(with_payload!(&[0xD4, 0x00, 0x03]), Ok(MatterCode::Ed448))]
1108    #[case(with_payload!(&[0xD4, 0x00, 0x04]), Ok(MatterCode::Ed448Sig))]
1109    #[case(with_payload!(&[0xD4, 0x00, 0x05]), Ok(MatterCode::Tag4))]
1110    #[case(with_payload!(&[0xD4, 0x00, 0x06]), Ok(MatterCode::DateTime))]
1111    #[case(with_payload!(&[0xD4, 0x00, 0x07]), Ok(MatterCode::X25519CipherSalt))]
1112    #[case(with_payload!(&[0xD4, 0x00, 0x08]), Ok(MatterCode::ECDSA256r1N))]
1113    #[case(with_payload!(&[0xD4, 0x00, 0x09]), Ok(MatterCode::ECDSA256r1))]
1114    #[case(with_payload!(&[0xD4, 0x00, 0x0A]), Ok(MatterCode::Null))]
1115    #[case(with_payload!(&[0xD4, 0x00, 0x0B]), Ok(MatterCode::No))]
1116    #[case(with_payload!(&[0xD4, 0x00, 0x0C]), Ok(MatterCode::Yes))]
1117    #[case(with_payload!(&[0xD4, 0x00, 0x0D]), Ok(MatterCode::Tag8))]
1118    #[case(with_payload!(&[0xD4, 0x00, 0x0E]), Ok(MatterCode::Escape))]
1119    #[case(with_payload!(&[0xD4, 0x00, 0x0F]), Ok(MatterCode::Empty))]
1120    #[case(with_payload!(&[0xD7, 0xFF, 0xFE]), Ok(MatterCode::TBD0S))]
1121    #[case(with_payload!(&[0xD7, 0xFF, 0xFF]), Ok(MatterCode::TBD0))]
1122    #[case(with_payload!(&[0xDB, 0xFF, 0xFE]), Ok(MatterCode::TBD1S))]
1123    #[case(with_payload!(&[0xDB, 0xFF, 0xFF]), Ok(MatterCode::TBD1))]
1124    #[case(with_payload!(&[0xDF, 0xFF, 0xFE]), Ok(MatterCode::TBD2S))]
1125    #[case(with_payload!(&[0xDF, 0xFF, 0xFF]), Ok(MatterCode::TBD2))]
1126    // // Variable size codes
1127    #[case(with_payload!(&[0xE0, 0x00]), Ok(MatterCode::StrB64_L0))]
1128    #[case(with_payload!(&[0xE4, 0x00]), Ok(MatterCode::StrB64_L1))]
1129    #[case(with_payload!(&[0xE8, 0x00]), Ok(MatterCode::StrB64_L2))]
1130    #[case(with_payload!(&[0xEC, 0x00, 0x00]), Ok(MatterCode::StrB64Big_L0))]
1131    #[case(with_payload!(&[0xF0, 0x00, 0x00]), Ok(MatterCode::StrB64Big_L1))]
1132    #[case(with_payload!(&[0xF4, 0x00, 0x00]), Ok(MatterCode::StrB64Big_L2))]
1133    #[case(with_payload!(&[0xE0, 0x10]), Ok(MatterCode::Bytes_L0))]
1134    #[case(with_payload!(&[0xE4, 0x10]), Ok(MatterCode::Bytes_L1))]
1135    #[case(with_payload!(&[0xE8, 0x10]), Ok(MatterCode::Bytes_L2))]
1136    #[case(with_payload!(&[0xEC, 0x00, 0x01]), Ok(MatterCode::BytesBig_L0))]
1137    #[case(with_payload!(&[0xF0, 0x00, 0x01]), Ok(MatterCode::BytesBig_L1))]
1138    #[case(with_payload!(&[0xF4, 0x00, 0x01]), Ok(MatterCode::BytesBig_L2))]
1139    #[case(with_payload!(&[0xE0, 0x20]), Ok(MatterCode::X25519Cipher_L0))]
1140    #[case(with_payload!(&[0xE4, 0x20]), Ok(MatterCode::X25519Cipher_L1))]
1141    #[case(with_payload!(&[0xE8, 0x20]), Ok(MatterCode::X25519Cipher_L2))]
1142    #[case(with_payload!(&[0xEC, 0x00, 0x02]), Ok(MatterCode::X25519CipherBig_L0))]
1143    #[case(with_payload!(&[0xF0, 0x00, 0x02]), Ok(MatterCode::X25519CipherBig_L1))]
1144    #[case(with_payload!(&[0xF4, 0x00, 0x02]), Ok(MatterCode::X25519CipherBig_L2))]
1145    #[case(with_payload!(&[0xE0, 0x30]), Ok(MatterCode::X25519CipherQB64_L0))]
1146    #[case(with_payload!(&[0xE4, 0x30]), Ok(MatterCode::X25519CipherQB64_L1))]
1147    #[case(with_payload!(&[0xE8, 0x30]), Ok(MatterCode::X25519CipherQB64_L2))]
1148    #[case(with_payload!(&[0xEC, 0x00, 0x03]), Ok(MatterCode::X25519CipherQB64Big_L0))]
1149    #[case(with_payload!(&[0xF0, 0x00, 0x03]), Ok(MatterCode::X25519CipherQB64Big_L1))]
1150    #[case(with_payload!(&[0xF4, 0x00, 0x03]), Ok(MatterCode::X25519CipherQB64Big_L2))]
1151    #[case(with_payload!(&[0xE0, 0x40]), Ok(MatterCode::X25519CipherQB2_L0))]
1152    #[case(with_payload!(&[0xE4, 0x40]), Ok(MatterCode::X25519CipherQB2_L1))]
1153    #[case(with_payload!(&[0xE8, 0x40]), Ok(MatterCode::X25519CipherQB2_L2))]
1154    #[case(with_payload!(&[0xEC, 0x00, 0x04]), Ok(MatterCode::X25519CipherQB2Big_L0))]
1155    #[case(with_payload!(&[0xF0, 0x00, 0x04]), Ok(MatterCode::X25519CipherQB2Big_L1))]
1156    #[case(with_payload!(&[0xF4, 0x00, 0x04]), Ok(MatterCode::X25519CipherQB2Big_L2))]
1157    #[case(with_payload!(&[0xE0, 0x50]), Ok(MatterCode::HPKEBaseCipher_L0))]
1158    #[case(with_payload!(&[0xE4, 0x50]), Ok(MatterCode::HPKEBaseCipher_L1))]
1159    #[case(with_payload!(&[0xE8, 0x50]), Ok(MatterCode::HPKEBaseCipher_L2))]
1160    #[case(with_payload!(&[0xEC, 0x00, 0x05]), Ok(MatterCode::HPKEBaseCipherBig_L0))]
1161    #[case(with_payload!(&[0xF0, 0x00, 0x05]), Ok(MatterCode::HPKEBaseCipherBig_L1))]
1162    #[case(with_payload!(&[0xF4, 0x00, 0x05]), Ok(MatterCode::HPKEBaseCipherBig_L2))]
1163    #[case(with_payload!(&[0xE0, 0x70]), Ok(MatterCode::Decimal_L0))]
1164    #[case(with_payload!(&[0xE4, 0x70]), Ok(MatterCode::Decimal_L1))]
1165    #[case(with_payload!(&[0xE8, 0x70]), Ok(MatterCode::Decimal_L2))]
1166    #[case(with_payload!(&[0xEC, 0x00, 0x07]), Ok(MatterCode::DecimalBig_L0))]
1167    #[case(with_payload!(&[0xF0, 0x00, 0x07]), Ok(MatterCode::DecimalBig_L1))]
1168    #[case(with_payload!(&[0xF4, 0x00, 0x07]), Ok(MatterCode::DecimalBig_L2))]
1169    fn test_all_matter_codes_from_byte_stream(
1170        #[case] input: Vec<u8>,
1171        #[case] expected: Result<MatterCode, ParsingError>,
1172    ) {
1173        assert_eq!(MatterCode::from_stream(&input), expected);
1174    }
1175
1176    #[rstest]
1177    #[case(MatterCode::StrB64_L0, 100, 1, Ok(MatterCode::StrB64_L1))]
1178    #[case(MatterCode::Bytes_L1, 200, 2, Ok(MatterCode::Bytes_L2))]
1179    #[case(MatterCode::Decimal_L2, 1, 0, Ok(MatterCode::Decimal_L0))]
1180    #[case(MatterCode::StrB64_L0, 5000, 0, Ok(MatterCode::StrB64Big_L0))] // "4A" -> "7AAA"
1181    #[case(MatterCode::Bytes_L1, 5000, 2, Ok(MatterCode::BytesBig_L2))] // "5B" -> "9AAB"
1182    #[case(MatterCode::Decimal_L2, 10000, 1, Ok(MatterCode::DecimalBig_L1))] // "6H" -> "8AAH"
1183    #[case(MatterCode::StrB64Big_L0, 20000, 1, Ok(MatterCode::StrB64Big_L1))] // "7AAA" -> "8AAA"
1184    #[case(MatterCode::BytesBig_L1, 20000, 0, Ok(MatterCode::BytesBig_L0))] // "8AAB" -> "7AAB"
1185    #[case(MatterCode::DecimalBig_L2, 20000, 2, Ok(MatterCode::DecimalBig_L2))] // "9AAH" -> "9AAH"
1186    #[case(
1187        MatterCode::Ed25519,
1188        100,
1189        0,
1190        Err(ValidationError::IncompatiblePromotion("D".to_owned()))
1191    )]
1192    #[case(
1193        MatterCode::StrB64_L0,
1194        64_usize.pow(4) + 1,
1195        0,
1196        Err(ValidationError::IncompatiblePromotion("4A".to_owned()))
1197    )]
1198    #[case(
1199        MatterCode::StrB64_L1,
1200        100,
1201        3,
1202        Err(ValidationError::InvalidPromotionTarget { code: "5A".to_owned(), lead: 3 })
1203    )]
1204    fn test_code_promotion(
1205        #[case] original: MatterCode,
1206        #[case] size: usize,
1207        #[case] lead: usize,
1208        #[case] expected: Result<MatterCode, ValidationError>,
1209    ) {
1210        assert_eq!(original.promote(size, lead), expected);
1211    }
1212
1213    #[test]
1214    fn test_codex_categorization() {
1215        assert!(MatterCode::Blake3_256.is_digest());
1216        assert!(MatterCode::Blake3_256.is_nonce());
1217        assert!(!MatterCode::Blake3_256.is_text_code());
1218        assert!(MatterCode::StrB64_L0.is_base64_text_code());
1219        assert!(MatterCode::StrB64_L0.is_label());
1220        assert!(!MatterCode::StrB64_L0.is_digest());
1221        assert!(MatterCode::Ed25519N.is_non_transferable());
1222        assert!(MatterCode::Ed25519N.is_prefix_derivation());
1223        assert!(MatterCode::Short.is_base64_num());
1224        assert!(!MatterCode::Short.is_base64_tag());
1225        assert!(MatterCode::Tag4.is_base64_tag());
1226        assert!(MatterCode::Tag4.is_label());
1227    }
1228
1229    #[rstest]
1230    #[case(MatterCode::Ed25519Seed, Ok(32))] // ((44 - 1) * 3 / 4) - 0 = 32
1231    #[case(MatterCode::Ed25519Sig, Ok(64))] // ((88 - 2) * 3 / 4) - 0 = 64
1232    #[case(MatterCode::Ed448Sig, Ok(114))] // ((156 - 4) * 3 / 4) - 0 = 114
1233    #[case(MatterCode::Label1, Ok(1))] // ((4 - 1) * 3 / 4) - 1 = 1
1234    #[case(
1235        MatterCode::StrB64_L0,
1236        Err(ValidationError::InvalidSizingOperation("4A".to_owned()))
1237    )]
1238    #[case(
1239        MatterCode::BytesBig_L1,
1240        Err(ValidationError::InvalidSizingOperation("8AAB".to_owned()))
1241    )]
1242    fn test_raw_size_calculation(
1243        #[case] code: MatterCode,
1244        #[case] expected: Result<usize, ValidationError>,
1245    ) {
1246        assert_eq!(code.raw_size(), expected);
1247    }
1248
1249    // ── Sizage conformance tests ──────────────────────────────────────
1250    // Verifies every MatterCode variant's get_sizage() output matches
1251    // the expected values from the Python KERI spec.
1252    #[rstest]
1253    // 1-char fixed codes (hs=1, ss=0)
1254    #[case(MatterCode::Ed25519Seed, 1, 0, 0, Some(44_u16), 0)]
1255    #[case(MatterCode::Ed25519N, 1, 0, 0, Some(44_u16), 0)]
1256    #[case(MatterCode::X25519, 1, 0, 0, Some(44_u16), 0)]
1257    #[case(MatterCode::Ed25519, 1, 0, 0, Some(44_u16), 0)]
1258    #[case(MatterCode::Blake3_256, 1, 0, 0, Some(44_u16), 0)]
1259    #[case(MatterCode::Blake2b_256, 1, 0, 0, Some(44_u16), 0)]
1260    #[case(MatterCode::Blake2s_256, 1, 0, 0, Some(44_u16), 0)]
1261    #[case(MatterCode::SHA3_256, 1, 0, 0, Some(44_u16), 0)]
1262    #[case(MatterCode::SHA2_256, 1, 0, 0, Some(44_u16), 0)]
1263    #[case(MatterCode::ECDSA256k1Seed, 1, 0, 0, Some(44_u16), 0)]
1264    #[case(MatterCode::Ed448Seed, 1, 0, 0, Some(76_u16), 0)]
1265    #[case(MatterCode::X448, 1, 0, 0, Some(76_u16), 0)]
1266    #[case(MatterCode::Short, 1, 0, 0, Some(4_u16), 0)]
1267    #[case(MatterCode::Big, 1, 0, 0, Some(12_u16), 0)]
1268    #[case(MatterCode::X25519Private, 1, 0, 0, Some(44_u16), 0)]
1269    #[case(MatterCode::X25519CipherSeed, 1, 0, 0, Some(124_u16), 0)]
1270    #[case(MatterCode::ECDSA256r1Seed, 1, 0, 0, Some(44_u16), 0)]
1271    #[case(MatterCode::Tall, 1, 0, 0, Some(8_u16), 0)]
1272    #[case(MatterCode::Large, 1, 0, 0, Some(16_u16), 0)]
1273    #[case(MatterCode::Great, 1, 0, 0, Some(20_u16), 0)]
1274    #[case(MatterCode::Vast, 1, 0, 0, Some(24_u16), 0)]
1275    #[case(MatterCode::Label1, 1, 0, 0, Some(4_u16), 1)]
1276    #[case(MatterCode::Label2, 1, 0, 0, Some(4_u16), 0)]
1277    // 1-char special codes (hs=1, ss>0)
1278    #[case(MatterCode::Tag3, 1, 3, 0, Some(4_u16), 0)]
1279    #[case(MatterCode::Tag7, 1, 7, 0, Some(8_u16), 0)]
1280    #[case(MatterCode::Tag11, 1, 11, 0, Some(12_u16), 0)]
1281    // 1-char fixed code (Salt256)
1282    #[case(MatterCode::Salt256, 1, 0, 0, Some(44_u16), 0)]
1283    // 2-char fixed codes (hs=2, ss=0)
1284    #[case(MatterCode::Salt128, 2, 0, 0, Some(24_u16), 0)]
1285    #[case(MatterCode::Ed25519Sig, 2, 0, 0, Some(88_u16), 0)]
1286    #[case(MatterCode::ECDSA256k1Sig, 2, 0, 0, Some(88_u16), 0)]
1287    #[case(MatterCode::Blake3_512, 2, 0, 0, Some(88_u16), 0)]
1288    #[case(MatterCode::Blake2b_512, 2, 0, 0, Some(88_u16), 0)]
1289    #[case(MatterCode::SHA3_512, 2, 0, 0, Some(88_u16), 0)]
1290    #[case(MatterCode::SHA2_512, 2, 0, 0, Some(88_u16), 0)]
1291    #[case(MatterCode::Long, 2, 0, 0, Some(8_u16), 0)]
1292    #[case(MatterCode::ECDSA256r1Sig, 2, 0, 0, Some(88_u16), 0)]
1293    // 2-char special codes (hs=2, ss>0, fixed)
1294    #[case(MatterCode::Tag1, 2, 2, 1, Some(4_u16), 0)]
1295    #[case(MatterCode::Tag2, 2, 2, 0, Some(4_u16), 0)]
1296    #[case(MatterCode::Tag5, 2, 6, 1, Some(8_u16), 0)]
1297    #[case(MatterCode::Tag6, 2, 6, 0, Some(8_u16), 0)]
1298    #[case(MatterCode::Tag9, 2, 10, 1, Some(12_u16), 0)]
1299    #[case(MatterCode::Tag10, 2, 10, 0, Some(12_u16), 0)]
1300    #[case(MatterCode::GramHeadNeck, 2, 22, 0, Some(32_u16), 0)]
1301    #[case(MatterCode::GramHead, 2, 22, 0, Some(28_u16), 0)]
1302    #[case(MatterCode::GramHeadAIDNeck, 2, 22, 0, Some(76_u16), 0)]
1303    #[case(MatterCode::GramHeadAID, 2, 22, 0, Some(72_u16), 0)]
1304    // 4-char fixed codes (hs=4, ss=0)
1305    #[case(MatterCode::ECDSA256k1N, 4, 0, 0, Some(48_u16), 0)]
1306    #[case(MatterCode::ECDSA256k1, 4, 0, 0, Some(48_u16), 0)]
1307    #[case(MatterCode::Ed448N, 4, 0, 0, Some(80_u16), 0)]
1308    #[case(MatterCode::Ed448, 4, 0, 0, Some(80_u16), 0)]
1309    #[case(MatterCode::Ed448Sig, 4, 0, 0, Some(156_u16), 0)]
1310    #[case(MatterCode::DateTime, 4, 0, 0, Some(36_u16), 0)]
1311    #[case(MatterCode::X25519CipherSalt, 4, 0, 0, Some(100_u16), 0)]
1312    #[case(MatterCode::ECDSA256r1N, 4, 0, 0, Some(48_u16), 0)]
1313    #[case(MatterCode::ECDSA256r1, 4, 0, 0, Some(48_u16), 0)]
1314    #[case(MatterCode::Null, 4, 0, 0, Some(4_u16), 0)]
1315    #[case(MatterCode::No, 4, 0, 0, Some(4_u16), 0)]
1316    #[case(MatterCode::Yes, 4, 0, 0, Some(4_u16), 0)]
1317    #[case(MatterCode::Escape, 4, 0, 0, Some(4_u16), 0)]
1318    #[case(MatterCode::Empty, 4, 0, 0, Some(4_u16), 0)]
1319    // 4-char special codes (hs=4, ss>0, fixed)
1320    #[case(MatterCode::Tag4, 4, 4, 0, Some(8_u16), 0)]
1321    #[case(MatterCode::Tag8, 4, 8, 0, Some(12_u16), 0)]
1322    #[case(MatterCode::TBD0S, 4, 2, 0, Some(12_u16), 0)]
1323    #[case(MatterCode::TBD0, 4, 0, 0, Some(8_u16), 0)]
1324    #[case(MatterCode::TBD1S, 4, 2, 1, Some(12_u16), 1)]
1325    #[case(MatterCode::TBD1, 4, 0, 0, Some(8_u16), 1)]
1326    #[case(MatterCode::TBD2S, 4, 2, 0, Some(12_u16), 2)]
1327    #[case(MatterCode::TBD2, 4, 0, 0, Some(8_u16), 2)]
1328    // Variable-size small codes (hs=2, ss=2, fs=Small)
1329    #[case(MatterCode::StrB64_L0, 2, 2, 0, None, 0)]
1330    #[case(MatterCode::StrB64_L1, 2, 2, 0, None, 1)]
1331    #[case(MatterCode::StrB64_L2, 2, 2, 0, None, 2)]
1332    #[case(MatterCode::Bytes_L0, 2, 2, 0, None, 0)]
1333    #[case(MatterCode::Bytes_L1, 2, 2, 0, None, 1)]
1334    #[case(MatterCode::Bytes_L2, 2, 2, 0, None, 2)]
1335    #[case(MatterCode::X25519Cipher_L0, 2, 2, 0, None, 0)]
1336    #[case(MatterCode::X25519Cipher_L1, 2, 2, 0, None, 1)]
1337    #[case(MatterCode::X25519Cipher_L2, 2, 2, 0, None, 2)]
1338    #[case(MatterCode::X25519CipherQB64_L0, 2, 2, 0, None, 0)]
1339    #[case(MatterCode::X25519CipherQB64_L1, 2, 2, 0, None, 1)]
1340    #[case(MatterCode::X25519CipherQB64_L2, 2, 2, 0, None, 2)]
1341    #[case(MatterCode::X25519CipherQB2_L0, 2, 2, 0, None, 0)]
1342    #[case(MatterCode::X25519CipherQB2_L1, 2, 2, 0, None, 1)]
1343    #[case(MatterCode::X25519CipherQB2_L2, 2, 2, 0, None, 2)]
1344    #[case(MatterCode::HPKEBaseCipher_L0, 2, 2, 0, None, 0)]
1345    #[case(MatterCode::HPKEBaseCipher_L1, 2, 2, 0, None, 1)]
1346    #[case(MatterCode::HPKEBaseCipher_L2, 2, 2, 0, None, 2)]
1347    #[case(MatterCode::Decimal_L0, 2, 2, 0, None, 0)]
1348    #[case(MatterCode::Decimal_L1, 2, 2, 0, None, 1)]
1349    #[case(MatterCode::Decimal_L2, 2, 2, 0, None, 2)]
1350    // Variable-size large codes (hs=4, ss=4, fs=Large)
1351    #[case(MatterCode::StrB64Big_L0, 4, 4, 0, None, 0)]
1352    #[case(MatterCode::StrB64Big_L1, 4, 4, 0, None, 1)]
1353    #[case(MatterCode::StrB64Big_L2, 4, 4, 0, None, 2)]
1354    #[case(MatterCode::BytesBig_L0, 4, 4, 0, None, 0)]
1355    #[case(MatterCode::BytesBig_L1, 4, 4, 0, None, 1)]
1356    #[case(MatterCode::BytesBig_L2, 4, 4, 0, None, 2)]
1357    #[case(MatterCode::X25519CipherBig_L0, 4, 4, 0, None, 0)]
1358    #[case(MatterCode::X25519CipherBig_L1, 4, 4, 0, None, 1)]
1359    #[case(MatterCode::X25519CipherBig_L2, 4, 4, 0, None, 2)]
1360    #[case(MatterCode::X25519CipherQB64Big_L0, 4, 4, 0, None, 0)]
1361    #[case(MatterCode::X25519CipherQB64Big_L1, 4, 4, 0, None, 1)]
1362    #[case(MatterCode::X25519CipherQB64Big_L2, 4, 4, 0, None, 2)]
1363    #[case(MatterCode::X25519CipherQB2Big_L0, 4, 4, 0, None, 0)]
1364    #[case(MatterCode::X25519CipherQB2Big_L1, 4, 4, 0, None, 1)]
1365    #[case(MatterCode::X25519CipherQB2Big_L2, 4, 4, 0, None, 2)]
1366    #[case(MatterCode::HPKEBaseCipherBig_L0, 4, 4, 0, None, 0)]
1367    #[case(MatterCode::HPKEBaseCipherBig_L1, 4, 4, 0, None, 1)]
1368    #[case(MatterCode::HPKEBaseCipherBig_L2, 4, 4, 0, None, 2)]
1369    #[case(MatterCode::DecimalBig_L0, 4, 4, 0, None, 0)]
1370    #[case(MatterCode::DecimalBig_L1, 4, 4, 0, None, 1)]
1371    #[case(MatterCode::DecimalBig_L2, 4, 4, 0, None, 2)]
1372    fn sizage_matches_keri_spec(
1373        #[case] code: MatterCode,
1374        #[case] expected_hs: usize,
1375        #[case] expected_ss: usize,
1376        #[case] expected_xs: usize,
1377        #[case] expected_fs: Option<u16>,
1378        #[case] expected_ls: usize,
1379    ) {
1380        let sizage = code.get_sizage();
1381        assert_eq!(sizage.hs(), expected_hs, "hs mismatch for {code:?}");
1382        assert_eq!(sizage.ss(), expected_ss, "ss mismatch for {code:?}");
1383        assert_eq!(sizage.xs(), expected_xs, "xs mismatch for {code:?}");
1384        assert_eq!(sizage.ls(), expected_ls, "ls mismatch for {code:?}");
1385        match (sizage.fs(), &expected_fs) {
1386            (SizeType::Fixed(a), Some(b)) => {
1387                assert_eq!(u16::from(*a), *b, "fs mismatch for {code:?}");
1388            }
1389            (SizeType::Small | SizeType::Large, None) => {}
1390            (SizeType::Fixed(_), None) | (SizeType::Small | SizeType::Large, Some(_)) => panic!(
1391                "SizeType mismatch for {code:?}: got {:?}, expected fs={expected_fs:?}",
1392                sizage.fs(),
1393            ),
1394        }
1395    }
1396
1397    #[rstest]
1398    #[case(&[], Err(ParsingError::EmptyStream))]
1399    #[case(
1400        &with_payload!(&[0xD4, 0x00, 0x00])[0..2],
1401        Err(ParsingError::StreamTooShort(MatterPart::Head))
1402    )]
1403    #[case(
1404        &with_payload!(&[0xD0, 0x1F])[0..1],
1405        Err(ParsingError::StreamTooShort(MatterPart::Head))
1406    )]
1407    #[case(&[0xFF], Err(ParsingError::MalformedCode { part: MatterPart::Head, found: "63".to_owned() }))]
1408    fn test_from_stream_errors(
1409        #[case] input: &[u8],
1410        #[case] expected: Result<MatterCode, ParsingError>,
1411    ) {
1412        assert_eq!(MatterCode::from_stream(input), expected);
1413    }
1414
1415    #[test]
1416    fn all_code_strings_are_unique() {
1417        use std::collections::HashSet;
1418        let mut seen = HashSet::new();
1419        for variant in MatterCode::iter() {
1420            let code_str: &'static str = variant.into();
1421            assert!(
1422                seen.insert(code_str),
1423                "Duplicate code string: {code_str} for {variant:?}"
1424            );
1425        }
1426    }
1427
1428    #[test]
1429    fn no_code_is_prefix_of_another() {
1430        let codes: Vec<&'static str> = MatterCode::iter()
1431            .map(|v| -> &'static str { v.into() })
1432            .collect();
1433        for (i, a) in codes.iter().enumerate() {
1434            for (j, b) in codes.iter().enumerate() {
1435                if i != j && a.len() < b.len() {
1436                    assert!(
1437                        !b.starts_with(a),
1438                        "Code '{a}' is a prefix of '{b}' — ambiguous parsing"
1439                    );
1440                }
1441            }
1442        }
1443    }
1444
1445    #[test]
1446    fn variant_count_matches_expected() {
1447        let count = MatterCode::iter().count();
1448        assert!(
1449            count >= 109,
1450            "Expected at least 109 MatterCode variants, found {count}"
1451        );
1452    }
1453
1454    // ── Structural Invariant Tests ──────────────────────────────────────
1455    // Corresponds to keripy: tests/core/test_coring.py::test_matter_class
1456    // These verify algebraic relationships that must hold for every code
1457    // in the CESR code table per the specification.
1458
1459    #[test]
1460    fn all_fixed_codes_have_fs_divisible_by_4() {
1461        // In base64, every primitive must occupy a multiple of 4 characters
1462        // so it can be cleanly decoded to bytes (4 b64 chars = 3 bytes).
1463        for code in MatterCode::iter() {
1464            let sizage = code.get_sizage();
1465            if let SizeType::Fixed(fs) = sizage.fs {
1466                assert_eq!(
1467                    fs % 4,
1468                    0,
1469                    "Code {code:?} has fs={fs} which is not divisible by 4"
1470                );
1471            }
1472        }
1473    }
1474
1475    #[test]
1476    fn all_codes_cs_not_3_mod_4() {
1477        // In base64, cs (code size = hs + ss) cannot be 3 mod 4
1478        // because that would leave 2 raw bits which cannot encode a full byte.
1479        for code in MatterCode::iter() {
1480            let sizage = code.get_sizage();
1481            let cs = sizage.hs + sizage.ss;
1482            assert_ne!(
1483                cs % 4,
1484                3,
1485                "Code {code:?} has cs={cs} (hs={}, ss={}) which is 3 mod 4",
1486                sizage.hs,
1487                sizage.ss
1488            );
1489        }
1490    }
1491
1492    #[test]
1493    fn all_codes_hs_is_1_2_or_4() {
1494        // CESR only defines 1-char, 2-char, and 4-char hard code sizes.
1495        for code in MatterCode::iter() {
1496            let sizage = code.get_sizage();
1497            assert!(
1498                sizage.hs == 1 || sizage.hs == 2 || sizage.hs == 4,
1499                "Code {code:?} has unexpected hs={}",
1500                sizage.hs
1501            );
1502        }
1503    }
1504
1505    #[test]
1506    fn fixed_non_special_codes_raw_size_consistency() {
1507        // For fixed, non-special codes (ss==0): the raw_size() method
1508        // should match the algebraic formula: ((fs - cs) * 3 / 4) - ls
1509        for code in MatterCode::iter() {
1510            let sizage = code.get_sizage();
1511            if let SizeType::Fixed(fs_val) = sizage.fs
1512                && sizage.ss == 0
1513            {
1514                let cs = usize::from(sizage.hs) + usize::from(sizage.ss);
1515                let rs = ((usize::from(fs_val) - cs) * 3 / 4) - usize::from(sizage.ls);
1516                let computed = code.raw_size().unwrap();
1517                assert_eq!(
1518                    rs, computed,
1519                    "Code {code:?}: formula gives rs={rs} but raw_size()={computed}"
1520                );
1521            }
1522        }
1523    }
1524
1525    #[test]
1526    fn fixed_codes_full_size_reconstructible() {
1527        // For fixed, non-special codes (ss==0): the data bytes (rs + ls)
1528        // must equal (fs - cs) * 3 / 4, verifying the full size is consistent
1529        // with the raw size and lead size.
1530        for code in MatterCode::iter() {
1531            let sizage = code.get_sizage();
1532            if let SizeType::Fixed(fs_val) = sizage.fs
1533                && sizage.ss == 0
1534            {
1535                let cs = usize::from(sizage.hs) + usize::from(sizage.ss);
1536                let rs = code.raw_size().unwrap();
1537                let ls = usize::from(sizage.ls);
1538                let total_data_chars = usize::from(fs_val) - cs;
1539                let data_bytes = total_data_chars * 3 / 4;
1540                assert_eq!(
1541                    data_bytes,
1542                    rs + ls,
1543                    "Code {code:?}: data_bytes={data_bytes} but rs+ls={}",
1544                    rs + ls
1545                );
1546            }
1547        }
1548    }
1549
1550    #[test]
1551    fn variable_codes_have_correct_structure() {
1552        // Variable-size codes must follow specific structural rules:
1553        // Small: hs=2, ss=2; Large: hs=4, ss=4; ls always <= 2
1554        for code in MatterCode::iter() {
1555            let sizage = code.get_sizage();
1556            match sizage.fs {
1557                SizeType::Small => {
1558                    assert_eq!(sizage.hs, 2, "Small code {code:?} should have hs=2");
1559                    assert_eq!(sizage.ss, 2, "Small code {code:?} should have ss=2");
1560                    assert!(sizage.ls <= 2, "Small code {code:?} has ls={}", sizage.ls);
1561                }
1562                SizeType::Large => {
1563                    assert_eq!(sizage.hs, 4, "Large code {code:?} should have hs=4");
1564                    assert_eq!(sizage.ss, 4, "Large code {code:?} should have ss=4");
1565                    assert!(sizage.ls <= 2, "Large code {code:?} has ls={}", sizage.ls);
1566                }
1567                SizeType::Fixed(_) => {}
1568            }
1569        }
1570    }
1571
1572    #[test]
1573    fn x25519_cipher_qb64_big_l2_is_large() {
1574        let sizage = MatterCode::X25519CipherQB64Big_L2.get_sizage();
1575        assert!(
1576            matches!(sizage.fs, SizeType::Large),
1577            "X25519CipherQB64Big_L2 must be SizeType::Large (matching keripy)"
1578        );
1579        assert_eq!(sizage.hs, 4);
1580        assert_eq!(sizage.ss, 4);
1581        assert_eq!(sizage.ls, 2);
1582    }
1583
1584    #[test]
1585    fn lead_size_is_0_1_or_2() {
1586        // Lead size (ls) represents the number of leading zero-pad bytes.
1587        // In base64, at most 2 pad characters ("==") are possible, so ls <= 2.
1588        for code in MatterCode::iter() {
1589            let sizage = code.get_sizage();
1590            assert!(
1591                sizage.ls <= 2,
1592                "Code {code:?} has ls={} which exceeds maximum of 2",
1593                sizage.ls
1594            );
1595        }
1596    }
1597
1598    #[test]
1599    fn variable_codes_come_in_l0_l1_l2_triplets() {
1600        // Every variable-size code family should have exactly one variant for
1601        // each lead size (0, 1, 2), forming balanced triplets.
1602        //
1603        // We classify codes by their hs value rather than SizeType to avoid
1604        // being affected by the known X25519CipherQB64Big_L2 SizeType
1605        // mismatch (SizeType::Small with hs=4).
1606        //
1607        // Variable codes: hs=2 are "Small" family, hs=4 are "Large" family.
1608        let is_variable = |c: &MatterCode| {
1609            let s = c.get_sizage();
1610            matches!(s.fs, SizeType::Small | SizeType::Large)
1611        };
1612
1613        // Small variable codes: hs=2, ss=2
1614        let small_codes: Vec<_> = MatterCode::iter()
1615            .filter(|c| is_variable(c) && c.get_sizage().hs == 2)
1616            .collect();
1617        let small_l0 = small_codes
1618            .iter()
1619            .filter(|c| c.get_sizage().ls == 0)
1620            .count();
1621        let small_l1 = small_codes
1622            .iter()
1623            .filter(|c| c.get_sizage().ls == 1)
1624            .count();
1625        let small_l2 = small_codes
1626            .iter()
1627            .filter(|c| c.get_sizage().ls == 2)
1628            .count();
1629        assert_eq!(
1630            small_l0, small_l1,
1631            "Small codes: L0 count ({small_l0}) != L1 count ({small_l1})"
1632        );
1633        assert_eq!(
1634            small_l1, small_l2,
1635            "Small codes: L1 count ({small_l1}) != L2 count ({small_l2})"
1636        );
1637
1638        // Large variable codes: hs=4, ss=4
1639        let large_codes: Vec<_> = MatterCode::iter()
1640            .filter(|c| is_variable(c) && c.get_sizage().hs == 4)
1641            .collect();
1642        let large_l0 = large_codes
1643            .iter()
1644            .filter(|c| c.get_sizage().ls == 0)
1645            .count();
1646        let large_l1 = large_codes
1647            .iter()
1648            .filter(|c| c.get_sizage().ls == 1)
1649            .count();
1650        let large_l2 = large_codes
1651            .iter()
1652            .filter(|c| c.get_sizage().ls == 2)
1653            .count();
1654        assert_eq!(
1655            large_l0, large_l1,
1656            "Large codes: L0 count ({large_l0}) != L1 count ({large_l1})"
1657        );
1658        assert_eq!(
1659            large_l1, large_l2,
1660            "Large codes: L1 count ({large_l1}) != L2 count ({large_l2})"
1661        );
1662    }
1663
1664    #[test]
1665    fn code_string_length_matches_hs() {
1666        // The hard-size (hs) defines the length of the code string prefix.
1667        // The strum-serialized string for each code must be exactly hs characters.
1668        for code in MatterCode::iter() {
1669            let sizage = code.get_sizage();
1670            let code_str: &str = code.as_ref();
1671            assert_eq!(
1672                code_str.len(),
1673                usize::from(sizage.hs),
1674                "Code {code:?} has string '{code_str}' (len={}) but hs={}",
1675                code_str.len(),
1676                sizage.hs
1677            );
1678        }
1679    }
1680
1681    #[test]
1682    fn xs_only_nonzero_for_expected_codes() {
1683        // xs (extra size / prepad) should only be non-zero for codes that
1684        // need an extra prepad character in their encoding. Per the code table,
1685        // these are Tag1, Tag5, Tag9 (all xs=1) and TBD1S (xs=1).
1686        for code in MatterCode::iter() {
1687            let sizage = code.get_sizage();
1688            if sizage.xs > 0 {
1689                assert!(
1690                    matches!(
1691                        code,
1692                        MatterCode::Tag1 | MatterCode::Tag5 | MatterCode::Tag9 | MatterCode::TBD1S
1693                    ),
1694                    "Code {code:?} has xs={} but is not an expected odd-tag code",
1695                    sizage.xs
1696                );
1697                assert_eq!(
1698                    sizage.xs, 1,
1699                    "Code {code:?} has xs={} but expected xs=1",
1700                    sizage.xs
1701                );
1702            }
1703        }
1704    }
1705
1706    // ── is_special predicate tests ──────────────────────────────────────
1707    // Corresponds to keripy: tests/core/test_coring.py::test_matter_class
1708    // "special" codes have fixed fs AND ss > 0 (they carry soft data
1709    // within a fixed-size frame, unlike variable-size codes).
1710
1711    #[test]
1712    fn is_special_true_for_tag_codes() {
1713        let special_codes = [
1714            MatterCode::Tag1,
1715            MatterCode::Tag2,
1716            MatterCode::Tag3,
1717            MatterCode::Tag4,
1718            MatterCode::Tag5,
1719            MatterCode::Tag6,
1720            MatterCode::Tag7,
1721            MatterCode::Tag8,
1722            MatterCode::Tag9,
1723            MatterCode::Tag10,
1724            MatterCode::Tag11,
1725        ];
1726        for code in special_codes {
1727            assert!(code.is_special(), "{code:?} should be special");
1728        }
1729    }
1730
1731    #[test]
1732    fn is_special_true_for_gram_codes() {
1733        let gram_codes = [
1734            MatterCode::GramHeadNeck,
1735            MatterCode::GramHead,
1736            MatterCode::GramHeadAIDNeck,
1737            MatterCode::GramHeadAID,
1738        ];
1739        for code in gram_codes {
1740            assert!(code.is_special(), "{code:?} should be special");
1741        }
1742    }
1743
1744    #[test]
1745    fn is_special_true_for_tbd_s_codes() {
1746        let tbd_s_codes = [MatterCode::TBD0S, MatterCode::TBD1S, MatterCode::TBD2S];
1747        for code in tbd_s_codes {
1748            assert!(code.is_special(), "{code:?} should be special");
1749        }
1750    }
1751
1752    #[test]
1753    fn is_special_false_for_basic_crypto_codes() {
1754        let non_special = [
1755            MatterCode::Ed25519Seed,
1756            MatterCode::Ed25519N,
1757            MatterCode::Ed25519,
1758            MatterCode::Blake3_256,
1759            MatterCode::SHA2_256,
1760            MatterCode::Ed25519Sig,
1761            MatterCode::ECDSA256k1Sig,
1762            MatterCode::Short,
1763            MatterCode::Long,
1764            MatterCode::Salt128,
1765        ];
1766        for code in non_special {
1767            assert!(!code.is_special(), "{code:?} should NOT be special");
1768        }
1769    }
1770
1771    #[test]
1772    fn is_special_false_for_variable_codes() {
1773        let variable = [
1774            MatterCode::StrB64_L0,
1775            MatterCode::Bytes_L1,
1776            MatterCode::BytesBig_L2,
1777        ];
1778        for code in variable {
1779            assert!(
1780                !code.is_special(),
1781                "{code:?} should NOT be special (variable)"
1782            );
1783        }
1784    }
1785
1786    #[test]
1787    fn is_special_exhaustive_agrees_with_definition() {
1788        // Exhaustively verify is_special() for all codes matches the
1789        // definition: fixed fs AND ss > 0.
1790        for code in MatterCode::iter() {
1791            let sizage = code.get_sizage();
1792            let expected = matches!(sizage.fs, SizeType::Fixed(_)) && sizage.ss > 0;
1793            assert_eq!(
1794                code.is_special(),
1795                expected,
1796                "Code {:?}: is_special()={} but expected={} (fs={:?}, ss={})",
1797                code,
1798                code.is_special(),
1799                expected,
1800                sizage.fs,
1801                sizage.ss
1802            );
1803        }
1804    }
1805}