Skip to main content

cesr/core/matter/
matter.rs

1#![allow(
2    dead_code,
3    reason = "fields are used by downstream builder and accessors"
4)]
5use super::code::{CesrCode, MatterCode};
6use super::error::ValidationError;
7use super::sizage::SizeType;
8use alloc::borrow::Cow;
9#[cfg(feature = "alloc")]
10#[allow(
11    unused_imports,
12    reason = "alloc prelude items; subset used per cfg/feature combination"
13)]
14use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
15use base64::Engine;
16use base64::engine::general_purpose::URL_SAFE_NO_PAD;
17
18/// A CESR-encoded primitive with typed code `C`, a raw payload, and an optional soft field.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct Matter<'a, C: CesrCode> {
21    code: C,
22    raw: Cow<'a, [u8]>,
23    soft: Cow<'a, str>,
24}
25
26impl<'a, C: CesrCode> Matter<'a, C> {
27    /// B64 pad char for special codes with xtra size pre-padded soft values
28    pub const PAD: &'static str = "_";
29
30    #[must_use]
31    pub(crate) const fn new(code: C, raw: Cow<'a, [u8]>, soft: Cow<'a, str>) -> Self {
32        Self { code, raw, soft }
33    }
34
35    /// Returns the CESR code of this primitive.
36    #[must_use]
37    pub const fn code(&self) -> &C {
38        &self.code
39    }
40
41    /// Returns the soft (variable-length metadata) field of this primitive.
42    #[must_use]
43    pub fn soft(&self) -> &str {
44        self.soft.as_ref()
45    }
46
47    /// Returns the raw binary payload of this primitive.
48    #[must_use]
49    pub fn raw(&self) -> &[u8] {
50        self.raw.as_ref()
51    }
52
53    /// Construct a `Matter` without validation. Only available with the
54    /// `test-utils` feature — intended for tests that need to create
55    /// intentionally malformed primitives (e.g. wrong-size signatures).
56    #[cfg(feature = "test-utils")]
57    pub const fn new_unchecked(code: C, raw: Cow<'a, [u8]>, soft: Cow<'a, str>) -> Self {
58        Self { code, raw, soft }
59    }
60}
61
62impl<C: CesrCode> Matter<'_, C> {
63    /// Encodes this primitive into its qualified Base64 (qb64) CESR wire
64    /// format as bytes (`qb64b`).
65    ///
66    /// The output is allocated once at the final size `fs`; the Base64 payload
67    /// is written directly into it, then the header (code + soft field) is
68    /// written over the first `cs` bytes. Supports all fixed- and variable-size
69    /// CESR codes.
70    ///
71    /// # Panics
72    ///
73    /// Panics only on an internal-invariant break (a corrupt sizage table or a
74    /// mis-sized output buffer) — impossible for any `Matter` built through the
75    /// validated builder. This mirrors [`Indexer::to_qb64`] and is the
76    /// programmer-bug carve-out, not a data-validation path.
77    #[must_use]
78    pub fn to_qb64b(&self) -> Vec<u8> {
79        let sizage = self.code.get_sizage();
80        let hs = sizage.hs();
81        let ss = sizage.ss();
82        let xs = sizage.xs();
83        let ls = sizage.ls();
84        let cs = hs + ss;
85        let ps = cs % 4;
86
87        let code_str = self.code.as_str();
88        let raw = self.raw();
89
90        let fs = match sizage.fs() {
91            SizeType::Fixed(fixed) => usize::from(*fixed),
92            SizeType::Small | SizeType::Large => {
93                let raw_with_lead = raw.len() + ls;
94                let quadlets = raw_with_lead.div_ceil(3);
95                (quadlets * 4) + cs
96            }
97        };
98
99        // Base64-encode `[ls+ps zero bytes] ++ raw`. The leading zero bytes
100        // realign the payload to a 3-byte boundary; their Base64 image is `ps`
101        // pad chars that land in the header region and are overwritten below.
102        let pad_len = ls + ps;
103        let mut padded = Vec::with_capacity(pad_len + raw.len());
104        padded.resize(pad_len, 0);
105        padded.extend_from_slice(raw);
106
107        let mut out = vec![0u8; fs];
108        let b64_start = cs - ps;
109        let Ok(written) = URL_SAFE_NO_PAD.encode_slice(&padded, &mut out[b64_start..]) else {
110            unreachable!("qb64 output buffer is sized to fs; base64 cannot overflow")
111        };
112        assert_eq!(
113            b64_start + written,
114            fs,
115            "qb64 length mismatch for code {code_str}: expected {fs}, got {}",
116            b64_start + written
117        );
118
119        out[..hs].copy_from_slice(code_str.as_bytes());
120        if ss > 0 {
121            out[hs..hs + xs].fill(b'_');
122            out[hs + xs..cs].copy_from_slice(self.soft().as_bytes());
123        }
124        out
125    }
126
127    /// Encodes this primitive into its qualified Base64 (qb64) CESR wire format
128    /// as a `String`.
129    ///
130    /// qb64 output is pure ASCII (URL-safe Base64 alphabet + CESR code chars),
131    /// so UTF-8 validity is guaranteed by construction.
132    ///
133    /// # Panics
134    ///
135    /// Never, in practice: see [`Self::to_qb64b`]. The `from_utf8` step cannot
136    /// fail because qb64 bytes are ASCII.
137    #[must_use]
138    pub fn to_qb64(&self) -> String {
139        let Ok(s) = String::from_utf8(self.to_qb64b()) else {
140            unreachable!("qb64 bytes are ASCII (base64 alphabet + CESR code chars)")
141        };
142        s
143    }
144}
145
146impl<C: CesrCode> Matter<'_, C> {
147    /// Convert to `Matter<'static>` by owning any borrowed fields.
148    ///
149    /// Near-zero cost: `raw` is always already owned (base64 decode produces
150    /// new bytes), so only the `soft` field (0-4 bytes for most codes) is
151    /// cloned when borrowed.
152    pub fn into_static(self) -> Matter<'static, C> {
153        let raw: Cow<'static, [u8]> = match self.raw {
154            Cow::Owned(v) => Cow::Owned(v),
155            Cow::Borrowed(b) => Cow::Owned(b.to_vec()),
156        };
157        let soft: Cow<'static, str> = match self.soft {
158            Cow::Owned(s) => Cow::Owned(s),
159            Cow::Borrowed("") => Cow::Borrowed(""),
160            Cow::Borrowed(s) => Cow::Owned(s.to_owned()),
161        };
162        Matter::new(self.code, raw, soft)
163    }
164}
165
166impl<'a> Matter<'a, MatterCode> {
167    /// Converts this untyped `Matter<MatterCode>` into a typed `Matter<C>`.
168    ///
169    /// # Errors
170    ///
171    /// Returns a [`ValidationError`] if the code cannot be narrowed to `C`.
172    pub fn narrow<C>(self) -> Result<Matter<'a, C>, ValidationError>
173    where
174        C: CesrCode + TryFrom<MatterCode, Error = ValidationError>,
175    {
176        let code = C::try_from(self.code)?;
177        Ok(Matter {
178            code,
179            raw: self.raw,
180            soft: self.soft,
181        })
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::core::matter::code::{
189        DigestCode, MatterCode, NumberCode, SeedCode, SignatureCode, VerKeyCode,
190    };
191    use alloc::borrow::Cow;
192    use rstest::rstest;
193
194    #[test]
195    fn typed_matter_holds_correct_code_type() {
196        let code = VerKeyCode::Ed25519;
197        let raw = vec![0u8; 32];
198        let m: Matter<'_, VerKeyCode> = Matter::new(code, Cow::Owned(raw), Cow::from(""));
199        assert_eq!(*m.code(), VerKeyCode::Ed25519);
200    }
201
202    #[test]
203    fn untyped_matter_holds_any_code() {
204        let code = MatterCode::Blake3_256;
205        let raw = vec![0u8; 32];
206        let m: Matter<'_, MatterCode> = Matter::new(code, Cow::Owned(raw), Cow::from(""));
207        assert_eq!(*m.code(), MatterCode::Blake3_256);
208    }
209
210    #[test]
211    fn narrow_untyped_to_verkey() {
212        let code = MatterCode::Ed25519;
213        let raw = vec![0u8; 32];
214        let untyped = Matter::new(code, Cow::Owned(raw), Cow::from(""));
215        let typed: Matter<'_, VerKeyCode> = untyped.narrow().unwrap();
216        assert_eq!(*typed.code(), VerKeyCode::Ed25519);
217    }
218
219    #[test]
220    fn narrow_rejects_wrong_code_family() {
221        let code = MatterCode::Blake3_256;
222        let raw = vec![0u8; 32];
223        let untyped = Matter::new(code, Cow::Owned(raw), Cow::from(""));
224        let result: Result<Matter<'_, VerKeyCode>, _> = untyped.narrow();
225        assert!(result.is_err());
226    }
227
228    // --- Successful narrowing for all typed code families ---
229
230    #[rstest]
231    #[case(MatterCode::Ed25519, VerKeyCode::Ed25519)]
232    #[case(MatterCode::Ed25519N, VerKeyCode::Ed25519N)]
233    #[case(MatterCode::ECDSA256k1, VerKeyCode::ECDSA256k1)]
234    #[case(MatterCode::ECDSA256k1N, VerKeyCode::ECDSA256k1N)]
235    #[case(MatterCode::Ed448, VerKeyCode::Ed448)]
236    #[case(MatterCode::Ed448N, VerKeyCode::Ed448N)]
237    #[case(MatterCode::ECDSA256r1, VerKeyCode::ECDSA256r1)]
238    #[case(MatterCode::ECDSA256r1N, VerKeyCode::ECDSA256r1N)]
239    fn narrow_to_verkey_succeeds(#[case] matter_code: MatterCode, #[case] expected: VerKeyCode) {
240        let matter = Matter::new(matter_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
241        let typed: Matter<VerKeyCode> = matter.narrow().unwrap();
242        assert_eq!(*typed.code(), expected);
243    }
244
245    #[rstest]
246    #[case(MatterCode::Blake3_256, DigestCode::Blake3_256)]
247    #[case(MatterCode::Blake2b_256, DigestCode::Blake2b_256)]
248    #[case(MatterCode::Blake2s_256, DigestCode::Blake2s_256)]
249    #[case(MatterCode::SHA3_256, DigestCode::SHA3_256)]
250    #[case(MatterCode::SHA2_256, DigestCode::SHA2_256)]
251    #[case(MatterCode::Blake3_512, DigestCode::Blake3_512)]
252    #[case(MatterCode::Blake2b_512, DigestCode::Blake2b_512)]
253    #[case(MatterCode::SHA3_512, DigestCode::SHA3_512)]
254    #[case(MatterCode::SHA2_512, DigestCode::SHA2_512)]
255    fn narrow_to_digest_succeeds(#[case] matter_code: MatterCode, #[case] expected: DigestCode) {
256        let matter = Matter::new(matter_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
257        let typed: Matter<DigestCode> = matter.narrow().unwrap();
258        assert_eq!(*typed.code(), expected);
259    }
260
261    #[rstest]
262    #[case(MatterCode::Ed25519Sig, SignatureCode::Ed25519Sig)]
263    #[case(MatterCode::ECDSA256k1Sig, SignatureCode::ECDSA256k1Sig)]
264    #[case(MatterCode::ECDSA256r1Sig, SignatureCode::ECDSA256r1Sig)]
265    #[case(MatterCode::Ed448Sig, SignatureCode::Ed448Sig)]
266    fn narrow_to_signature_succeeds(
267        #[case] matter_code: MatterCode,
268        #[case] expected: SignatureCode,
269    ) {
270        let matter = Matter::new(matter_code, Cow::Owned(vec![0u8; 64]), Cow::from(""));
271        let typed: Matter<SignatureCode> = matter.narrow().unwrap();
272        assert_eq!(*typed.code(), expected);
273    }
274
275    #[rstest]
276    #[case(MatterCode::Ed25519Seed, SeedCode::Ed25519Seed)]
277    #[case(MatterCode::ECDSA256k1Seed, SeedCode::ECDSA256k1Seed)]
278    #[case(MatterCode::Ed448Seed, SeedCode::Ed448Seed)]
279    #[case(MatterCode::ECDSA256r1Seed, SeedCode::ECDSA256r1Seed)]
280    fn narrow_to_seed_succeeds(#[case] matter_code: MatterCode, #[case] expected: SeedCode) {
281        let matter = Matter::new(matter_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
282        let typed: Matter<SeedCode> = matter.narrow().unwrap();
283        assert_eq!(*typed.code(), expected);
284    }
285
286    #[rstest]
287    #[case(MatterCode::Short, NumberCode::Short)]
288    #[case(MatterCode::Long, NumberCode::Long)]
289    #[case(MatterCode::Tall, NumberCode::Tall)]
290    #[case(MatterCode::Big, NumberCode::Big)]
291    #[case(MatterCode::Large, NumberCode::Large)]
292    #[case(MatterCode::Great, NumberCode::Great)]
293    #[case(MatterCode::Vast, NumberCode::Vast)]
294    fn narrow_to_number_succeeds(#[case] matter_code: MatterCode, #[case] expected: NumberCode) {
295        let matter = Matter::new(matter_code, Cow::Owned(vec![0u8; 2]), Cow::from(""));
296        let typed: Matter<NumberCode> = matter.narrow().unwrap();
297        assert_eq!(*typed.code(), expected);
298    }
299
300    // --- Failed narrowing — wrong family ---
301
302    #[rstest]
303    #[case(MatterCode::Blake3_256)]
304    #[case(MatterCode::Ed25519Sig)]
305    #[case(MatterCode::Short)]
306    #[case(MatterCode::Ed25519Seed)]
307    fn narrow_to_verkey_rejects_wrong_family(#[case] wrong_code: MatterCode) {
308        let matter = Matter::new(wrong_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
309        let result: Result<Matter<VerKeyCode>, _> = matter.narrow();
310        assert!(result.is_err());
311    }
312
313    #[rstest]
314    #[case(MatterCode::Ed25519)]
315    #[case(MatterCode::Ed25519Sig)]
316    #[case(MatterCode::Short)]
317    fn narrow_to_digest_rejects_wrong_family(#[case] wrong_code: MatterCode) {
318        let matter = Matter::new(wrong_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
319        let result: Result<Matter<DigestCode>, _> = matter.narrow();
320        assert!(result.is_err());
321    }
322
323    #[rstest]
324    #[case(MatterCode::Ed25519)]
325    #[case(MatterCode::Blake3_256)]
326    fn narrow_to_signature_rejects_wrong_family(#[case] wrong_code: MatterCode) {
327        let matter = Matter::new(wrong_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
328        let result: Result<Matter<SignatureCode>, _> = matter.narrow();
329        assert!(result.is_err());
330    }
331
332    #[rstest]
333    #[case(MatterCode::Ed25519)]
334    #[case(MatterCode::Blake3_256)]
335    fn narrow_to_seed_rejects_wrong_family(#[case] wrong_code: MatterCode) {
336        let matter = Matter::new(wrong_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
337        let result: Result<Matter<SeedCode>, _> = matter.narrow();
338        assert!(result.is_err());
339    }
340
341    #[rstest]
342    #[case(MatterCode::Ed25519)]
343    #[case(MatterCode::Blake3_256)]
344    fn narrow_to_number_rejects_wrong_family(#[case] wrong_code: MatterCode) {
345        let matter = Matter::new(wrong_code, Cow::Owned(vec![0u8; 32]), Cow::from(""));
346        let result: Result<Matter<NumberCode>, _> = matter.narrow();
347        assert!(result.is_err());
348    }
349
350    mod to_qb64 {
351        use super::*;
352        use crate::core::matter::builder::MatterBuilder;
353        use crate::core::matter::code::{MatterCode, VerKeyCode};
354        use alloc::format;
355        use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
356
357        fn build_and_check(expected: &[u8]) {
358            let matter = MatterBuilder::new()
359                .from_qualified_base64(expected)
360                .expect("valid qb64 should parse");
361            assert_eq!(matter.to_qb64b(), expected, "to_qb64b mismatch");
362            assert_eq!(matter.to_qb64().as_bytes(), expected, "to_qb64 mismatch");
363            assert_eq!(
364                matter.to_qb64().into_bytes(),
365                matter.to_qb64b(),
366                "to_qb64 and to_qb64b disagree"
367            );
368        }
369
370        fn fixed_qb64(code_char: &str, raw: &[u8], ps: usize) -> Vec<u8> {
371            let mut padded = vec![0u8; ps];
372            padded.extend_from_slice(raw);
373            let payload_b64 = B64.encode(&padded);
374            format!("{code_char}{}", &payload_b64[ps..]).into_bytes()
375        }
376
377        #[test]
378        fn ed25519_verkey_roundtrip() {
379            build_and_check(&fixed_qb64("D", &[0xABu8; 32], 1));
380        }
381
382        #[test]
383        fn ed25519_sig_roundtrip() {
384            build_and_check(&fixed_qb64("0B", &[0xEFu8; 64], 2));
385        }
386
387        #[test]
388        fn blake3_256_digest_roundtrip() {
389            build_and_check(&fixed_qb64("E", &[0xCDu8; 32], 1));
390        }
391
392        #[test]
393        fn short_number_roundtrip() {
394            build_and_check(b"MAAB");
395        }
396
397        #[test]
398        fn strb64_variable_soft_roundtrip() {
399            build_and_check(b"4AACnhE8oa_r");
400        }
401
402        #[test]
403        fn lead_byte_code_roundtrip() {
404            // exercises the ls>0 lead-byte path
405            // Label1 (code "V", ls=1) qb64 vector from test_vectors::FIXED_VECTORS.
406            build_and_check(b"VAAt");
407        }
408
409        #[test]
410        fn xtra_underscore_code_roundtrip() {
411            // exercises the xs>0 underscore-fill path
412            // Tag1 (code "0J", ss=2, xs=1) qb64 vector from test_vectors::FIXED_VECTORS.
413            build_and_check(b"0J_A");
414        }
415
416        #[test]
417        fn narrowed_verkey_encodes_same_as_untyped() {
418            let qb64 = b"DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
419            let untyped = MatterBuilder::new()
420                .from_qualified_base64(&qb64[..])
421                .expect("valid qb64");
422            assert_eq!(*untyped.code(), MatterCode::Ed25519);
423            let typed: Matter<'_, VerKeyCode> = untyped.narrow().expect("narrow to verkey");
424            assert_eq!(typed.to_qb64b(), qb64, "typed to_qb64b mismatch");
425        }
426    }
427}