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#[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 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 #[must_use]
37 pub const fn code(&self) -> &C {
38 &self.code
39 }
40
41 #[must_use]
43 pub fn soft(&self) -> &str {
44 self.soft.as_ref()
45 }
46
47 #[must_use]
49 pub fn raw(&self) -> &[u8] {
50 self.raw.as_ref()
51 }
52
53 #[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 #[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 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 #[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 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 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 #[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 #[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 build_and_check(b"VAAt");
407 }
408
409 #[test]
410 fn xtra_underscore_code_roundtrip() {
411 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}