1use alloc::borrow::Cow;
2#[cfg(feature = "alloc")]
3#[allow(
4 unused_imports,
5 reason = "alloc prelude items; subset used per cfg/feature combination"
6)]
7use alloc::{borrow::ToOwned, format, vec, vec::Vec};
8use core::num::NonZeroUsize;
9
10use base64::{Engine, engine::general_purpose as b64};
11
12use super::code::{IndexMode, IndexedSigCode, hardage};
13use super::error::{IndexerParseError, IndexerValidationError};
14use super::indexer::Indexer;
15use super::xizage::XizageSize;
16use crate::b64::{decode_int, encode_binary};
17
18#[derive(Debug)]
22pub struct IStart;
23
24#[derive(Debug)]
26pub struct IWithCode {
27 code: IndexedSigCode,
28}
29
30#[derive(Debug)]
32pub struct IWithIndex {
33 code: IndexedSigCode,
34 index: u32,
35 ondex: Option<u32>,
36}
37
38#[derive(Debug)]
42pub struct IndexerBuilder<S> {
43 state: S,
44}
45
46impl Default for IndexerBuilder<IStart> {
47 fn default() -> Self {
48 Self { state: IStart }
49 }
50}
51
52impl IndexerBuilder<IStart> {
53 #[must_use]
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 #[must_use]
61 pub const fn with_code(self, code: IndexedSigCode) -> IndexerBuilder<IWithCode> {
62 IndexerBuilder {
63 state: IWithCode { code },
64 }
65 }
66
67 #[allow(
77 clippy::too_many_lines,
78 reason = "sequential parsing steps that are clearer together"
79 )]
80 pub fn from_qb64(self, stream: &[u8]) -> Result<(Indexer<'static>, usize), IndexerParseError> {
81 let &first_byte = stream.first().ok_or(IndexerParseError::EmptyStream)?;
82
83 let first_char = char::from(first_byte);
84 let hard_size = hardage(first_char)
85 .ok_or_else(|| IndexerParseError::UnknownCode(format!("{first_char}")))?;
86
87 if stream.len() < hard_size {
88 return Err(IndexerParseError::StreamTooShort {
89 need: hard_size,
90 got: stream.len(),
91 });
92 }
93
94 let hard = core::str::from_utf8(&stream[..hard_size])
95 .map_err(|_| IndexerParseError::InvalidBase64)?;
96 let code = IndexedSigCode::from_hard(hard).map_err(IndexerParseError::from)?;
97
98 let xizage = code.get_xizage();
99 let hs = usize::from(xizage.hs);
100 let ss = usize::from(xizage.ss);
101 let os = usize::from(xizage.os);
102 let ls = usize::from(xizage.ls);
103 let cs = hs + ss;
104 let ms = ss - os;
105
106 if stream.len() < cs {
107 return Err(IndexerParseError::StreamTooShort {
108 need: cs,
109 got: stream.len(),
110 });
111 }
112
113 let index_str = core::str::from_utf8(&stream[hs..hs + ms])
114 .map_err(|_| IndexerParseError::InvalidBase64)?;
115 let index: u32 = decode_int(index_str).map_err(IndexerParseError::from)?;
116
117 let ondex = match code.mode() {
118 IndexMode::CurrentOnly => {
119 if os > 0 {
120 let ondex_str = core::str::from_utf8(&stream[hs + ms..hs + ms + os])
121 .map_err(|_| IndexerParseError::InvalidBase64)?;
122 let ondex_val: u32 = decode_int(ondex_str).map_err(IndexerParseError::from)?;
123 if ondex_val != 0 {
124 return Err(IndexerParseError::OndexNotZeroForCurrentOnly(ondex_val));
125 }
126 }
127 None
128 }
129 IndexMode::Both => {
130 if os > 0 {
131 let ondex_str = core::str::from_utf8(&stream[hs + ms..hs + ms + os])
132 .map_err(|_| IndexerParseError::InvalidBase64)?;
133 let ondex_val: u32 = decode_int(ondex_str).map_err(IndexerParseError::from)?;
134 Some(ondex_val)
135 } else {
136 Some(index)
137 }
138 }
139 };
140
141 let fs = match xizage.fs {
142 XizageSize::Fixed(n) => usize::from(n),
143 XizageSize::Variable => {
144 #[allow(
145 clippy::as_conversions,
146 reason = "u32 to usize is a safe widening cast"
147 )]
148 let idx = index as usize;
149 compute_full_size(idx, cs)?
150 }
151 };
152
153 if stream.len() < fs {
154 return Err(IndexerParseError::StreamTooShort {
155 need: fs,
156 got: stream.len(),
157 });
158 }
159
160 let ps = cs % 4;
161 let payload = &stream[cs..fs];
162 let mut temp = Vec::with_capacity(ps + payload.len());
163 temp.extend(core::iter::repeat_n(b'A', ps));
164 temp.extend_from_slice(payload);
165 let decoded = b64::URL_SAFE_NO_PAD
166 .decode(&temp)
167 .map_err(|_| IndexerParseError::InvalidBase64)?;
168 let skip = if ps != 0 { ps } else { ls };
169 let raw = decoded[skip..].to_vec();
170
171 Ok((Indexer::new(code, index, ondex, Cow::Owned(raw)), fs))
172 }
173
174 pub fn from_qb2(self, stream: &[u8]) -> Result<(Indexer<'static>, usize), IndexerParseError> {
184 let &first_byte = stream.first().ok_or(IndexerParseError::EmptyStream)?;
185
186 let first_sextet = first_byte >> 2;
187 let hs: usize = match first_sextet {
188 0..=51 => 1,
189 52..=56 => 2,
190 _ => {
191 return Err(IndexerParseError::UnknownCode(format!(
192 "binary lead byte 0x{first_byte:02x}",
193 )));
194 }
195 };
196
197 let bhs = (hs * 3).div_ceil(4);
198 if stream.len() < bhs {
199 return Err(IndexerParseError::StreamTooShort {
200 need: bhs,
201 got: stream.len(),
202 });
203 }
204
205 let char_len = NonZeroUsize::new(hs)
206 .ok_or_else(|| IndexerParseError::UnknownCode("zero hard size".to_owned()))?;
207 let hard_b64 = encode_binary(&stream[..bhs], char_len).map_err(IndexerParseError::from)?;
208 let hard = &hard_b64[..hs];
209 let code = IndexedSigCode::from_hard(hard).map_err(IndexerParseError::from)?;
210
211 let xizage = code.get_xizage();
212 let ss = usize::from(xizage.ss);
213 let cs = hs + ss;
214
215 let bcs = (cs * 3).div_ceil(4);
216 if stream.len() < bcs {
217 return Err(IndexerParseError::StreamTooShort {
218 need: bcs,
219 got: stream.len(),
220 });
221 }
222
223 let cs_nz = NonZeroUsize::new(cs)
224 .ok_or_else(|| IndexerParseError::UnknownCode("zero code size".to_owned()))?;
225 let both_b64 = encode_binary(&stream[..bcs], cs_nz).map_err(IndexerParseError::from)?;
226
227 let soft = &both_b64[hs..cs];
228 let os = usize::from(xizage.os);
229 let ms = ss - os;
230
231 let index: u32 = decode_int(&soft[..ms]).map_err(IndexerParseError::from)?;
232
233 let fs: usize = match xizage.fs {
234 XizageSize::Fixed(n) => usize::from(n),
235 XizageSize::Variable => {
236 #[allow(
237 clippy::as_conversions,
238 reason = "u32 to usize is a safe widening cast"
239 )]
240 let idx = index as usize;
241 compute_full_size(idx, cs)?
242 }
243 };
244
245 let bfs = fs * 3 / 4;
246 if stream.len() < bfs {
247 return Err(IndexerParseError::StreamTooShort {
248 need: bfs,
249 got: stream.len(),
250 });
251 }
252
253 let qb64 = b64::URL_SAFE_NO_PAD.encode(&stream[..bfs]);
254 let (indexer, _) = Self::new().from_qb64(qb64.as_bytes())?;
255
256 Ok((indexer, bfs))
257 }
258}
259
260#[inline]
263fn compute_full_size(index: usize, cs: usize) -> Result<usize, IndexerParseError> {
264 index
265 .checked_mul(4)
266 .and_then(|quad| quad.checked_add(cs))
267 .ok_or(IndexerParseError::SizeOverflow)
268}
269
270impl IndexedSigCode {
271 pub fn frame_size(stream: &[u8]) -> Result<usize, IndexerParseError> {
277 let &first = stream.first().ok_or(IndexerParseError::EmptyStream)?;
278 let hard_size = hardage(char::from(first))
279 .ok_or_else(|| IndexerParseError::UnknownCode(format!("{}", char::from(first))))?;
280 if stream.len() < hard_size {
281 return Err(IndexerParseError::StreamTooShort {
282 need: hard_size,
283 got: stream.len(),
284 });
285 }
286 let hard = core::str::from_utf8(&stream[..hard_size])
287 .map_err(|_| IndexerParseError::InvalidBase64)?;
288 Self::from_hard(hard)
289 .map_err(IndexerParseError::from)?
290 .frame_size_of(stream)
291 }
292
293 pub(crate) fn frame_size_of(self, stream: &[u8]) -> Result<usize, IndexerParseError> {
296 let xizage = self.get_xizage();
297 let hs = usize::from(xizage.hs);
298 let ss = usize::from(xizage.ss);
299 let os = usize::from(xizage.os);
300 let cs = hs + ss;
301 let ms = ss - os;
302 match xizage.fs {
303 XizageSize::Fixed(n) => Ok(usize::from(n)),
304 XizageSize::Variable => {
305 if stream.len() < cs {
306 return Err(IndexerParseError::StreamTooShort {
307 need: cs,
308 got: stream.len(),
309 });
310 }
311 let index_str = core::str::from_utf8(&stream[hs..hs + ms])
312 .map_err(|_| IndexerParseError::InvalidBase64)?;
313 let index: usize = decode_int(index_str).map_err(IndexerParseError::from)?;
314 compute_full_size(index, cs)
315 }
316 }
317 }
318}
319
320impl IndexerBuilder<IWithCode> {
321 pub const fn with_index(
331 self,
332 index: u32,
333 ) -> Result<IndexerBuilder<IWithIndex>, IndexerValidationError> {
334 if index > self.state.code.max_index() {
335 return Err(IndexerValidationError::IndexTooLarge {
336 code: self.state.code,
337 index,
338 max: self.state.code.max_index(),
339 });
340 }
341 let ondex = match self.state.code.mode() {
342 IndexMode::Both => Some(index),
343 IndexMode::CurrentOnly => None,
344 };
345 Ok(IndexerBuilder {
346 state: IWithIndex {
347 code: self.state.code,
348 index,
349 ondex,
350 },
351 })
352 }
353
354 pub fn with_indices(
364 self,
365 index: u32,
366 ondex: u32,
367 ) -> Result<IndexerBuilder<IWithIndex>, IndexerValidationError> {
368 if self.state.code.mode() == IndexMode::CurrentOnly {
370 return Err(IndexerValidationError::OndexOnCurrentOnly(self.state.code));
371 }
372 if index > self.state.code.max_index() {
374 return Err(IndexerValidationError::IndexTooLarge {
375 code: self.state.code,
376 index,
377 max: self.state.code.max_index(),
378 });
379 }
380 if let Some(max_ondex) = self.state.code.max_ondex()
382 && ondex > max_ondex
383 {
384 return Err(IndexerValidationError::OndexTooLarge {
385 code: self.state.code,
386 ondex,
387 max: max_ondex,
388 });
389 }
390 if self.state.code.get_xizage().os() == 0 && ondex != index {
393 return Err(IndexerValidationError::OndexMustEqualIndex {
394 code: self.state.code,
395 index,
396 ondex,
397 });
398 }
399 Ok(IndexerBuilder {
400 state: IWithIndex {
401 code: self.state.code,
402 index,
403 ondex: Some(ondex),
404 },
405 })
406 }
407}
408
409impl IndexerBuilder<IWithIndex> {
410 pub fn with_raw<'a>(
418 self,
419 raw: impl Into<Cow<'a, [u8]>>,
420 ) -> Result<Indexer<'a>, IndexerValidationError> {
421 let raw_bytes = raw.into();
422 let expected = self.state.code.raw_size();
423 if raw_bytes.len() != expected {
424 return Err(IndexerValidationError::UnexpectedRawSize {
425 code: self.state.code,
426 expected,
427 got: raw_bytes.len(),
428 });
429 }
430 Ok(Indexer::new(
431 self.state.code,
432 self.state.index,
433 self.state.ondex,
434 raw_bytes,
435 ))
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use alloc::string::String;
442
443 use rstest::rstest;
444
445 use super::*;
446
447 #[test]
451 fn ed25519_both_index_0() {
452 let indexer = IndexerBuilder::new()
453 .with_code(IndexedSigCode::Ed25519)
454 .with_index(0)
455 .unwrap()
456 .with_raw(&[0u8; 64])
457 .unwrap();
458 assert_eq!(indexer.code(), IndexedSigCode::Ed25519);
459 assert_eq!(indexer.index(), 0);
460 assert_eq!(indexer.ondex(), Some(0));
461 assert_eq!(indexer.raw().len(), 64);
462 }
463
464 #[test]
466 fn ed25519crt_current_only() {
467 let indexer = IndexerBuilder::new()
468 .with_code(IndexedSigCode::Ed25519Crt)
469 .with_index(5)
470 .unwrap()
471 .with_raw(&[0u8; 64])
472 .unwrap();
473 assert_eq!(indexer.code(), IndexedSigCode::Ed25519Crt);
474 assert_eq!(indexer.index(), 5);
475 assert_eq!(indexer.ondex(), None);
476 }
477
478 #[test]
480 fn ed448_raw_size() {
481 let indexer = IndexerBuilder::new()
482 .with_code(IndexedSigCode::Ed448)
483 .with_index(4)
484 .unwrap()
485 .with_raw(&[0u8; 114])
486 .unwrap();
487 assert_eq!(indexer.raw().len(), 114);
488 }
489
490 #[test]
492 fn big_code_large_index() {
493 let indexer = IndexerBuilder::new()
494 .with_code(IndexedSigCode::Ed25519Big)
495 .with_index(4000)
496 .unwrap()
497 .with_raw(&[0u8; 64])
498 .unwrap();
499 assert_eq!(indexer.index(), 4000);
500 assert_eq!(indexer.ondex(), Some(4000));
501 }
502
503 #[test]
505 fn max_valid_index_small() {
506 let result = IndexerBuilder::new()
507 .with_code(IndexedSigCode::Ed25519)
508 .with_index(63);
509 assert!(result.is_ok());
510 }
511
512 #[test]
514 fn explicit_ondex_big() {
515 let indexer = IndexerBuilder::new()
516 .with_code(IndexedSigCode::Ed448Big)
517 .with_indices(5, 10)
518 .unwrap()
519 .with_raw(&[0u8; 114])
520 .unwrap();
521 assert_eq!(indexer.index(), 5);
522 assert_eq!(indexer.ondex(), Some(10));
523 }
524
525 #[test]
527 fn with_indices_small_both_same() {
528 let indexer = IndexerBuilder::new()
529 .with_code(IndexedSigCode::Ed25519)
530 .with_indices(7, 7)
531 .unwrap()
532 .with_raw(&[0u8; 64])
533 .unwrap();
534 assert_eq!(indexer.index(), 7);
535 assert_eq!(indexer.ondex(), Some(7));
536 }
537
538 #[test]
542 fn index_too_large() {
543 let result = IndexerBuilder::new()
544 .with_code(IndexedSigCode::Ed25519)
545 .with_index(64);
546 assert!(result.is_err());
547 }
548
549 #[test]
551 fn wrong_raw_size() {
552 let result = IndexerBuilder::new()
553 .with_code(IndexedSigCode::Ed25519)
554 .with_index(0)
555 .unwrap()
556 .with_raw(&[0u8; 32]); assert!(result.is_err());
558 }
559
560 #[test]
562 fn ondex_on_current_only() {
563 let result = IndexerBuilder::new()
564 .with_code(IndexedSigCode::Ed25519Crt)
565 .with_indices(0, 0);
566 assert!(result.is_err());
567 }
568
569 #[test]
571 fn ondex_too_large() {
572 let result = IndexerBuilder::new()
574 .with_code(IndexedSigCode::Ed448Big)
575 .with_indices(0, 262_144);
576 assert!(result.is_err());
577 }
578
579 #[test]
581 fn index_too_large_with_indices() {
582 let result = IndexerBuilder::new()
583 .with_code(IndexedSigCode::Ed25519Big)
584 .with_indices(4096, 0); assert!(result.is_err());
586 }
587
588 #[test]
591 fn index_too_large_error_variant() {
592 let err = IndexerBuilder::new()
593 .with_code(IndexedSigCode::Ed25519)
594 .with_index(64)
595 .err()
596 .unwrap();
597 assert_eq!(
598 err,
599 IndexerValidationError::IndexTooLarge {
600 code: IndexedSigCode::Ed25519,
601 index: 64,
602 max: 63,
603 }
604 );
605 }
606
607 #[test]
608 fn wrong_raw_size_error_variant() {
609 let err = IndexerBuilder::new()
610 .with_code(IndexedSigCode::Ed448)
611 .with_index(0)
612 .unwrap()
613 .with_raw(&[0u8; 64])
614 .err()
615 .unwrap();
616 assert_eq!(
617 err,
618 IndexerValidationError::UnexpectedRawSize {
619 code: IndexedSigCode::Ed448,
620 expected: 114,
621 got: 64,
622 }
623 );
624 }
625
626 #[test]
627 fn ondex_on_current_only_error_variant() {
628 let err = IndexerBuilder::new()
629 .with_code(IndexedSigCode::ECDSA256k1Crt)
630 .with_indices(0, 0)
631 .err()
632 .unwrap();
633 assert_eq!(
634 err,
635 IndexerValidationError::OndexOnCurrentOnly(IndexedSigCode::ECDSA256k1Crt)
636 );
637 }
638
639 #[test]
640 fn ondex_too_large_error_variant() {
641 let err = IndexerBuilder::new()
642 .with_code(IndexedSigCode::Ed25519Big)
643 .with_indices(0, 4096) .err()
645 .unwrap();
646 assert_eq!(
647 err,
648 IndexerValidationError::OndexTooLarge {
649 code: IndexedSigCode::Ed25519Big,
650 ondex: 4096,
651 max: 4095,
652 }
653 );
654 }
655
656 #[test]
660 fn max_valid_index_big() {
661 let result = IndexerBuilder::new()
662 .with_code(IndexedSigCode::Ed25519Big)
663 .with_index(4095);
664 assert!(result.is_ok());
665 }
666
667 #[test]
669 fn max_valid_ondex_big() {
670 let result = IndexerBuilder::new()
671 .with_code(IndexedSigCode::Ed448Big)
672 .with_indices(0, 262_143);
673 assert!(result.is_ok());
674 }
675
676 #[test]
678 fn ecdsa_codes() {
679 for code in [
680 IndexedSigCode::ECDSA256k1,
681 IndexedSigCode::ECDSA256r1,
682 IndexedSigCode::ECDSA256k1Big,
683 IndexedSigCode::ECDSA256r1Big,
684 ] {
685 let indexer = IndexerBuilder::new()
686 .with_code(code)
687 .with_index(0)
688 .unwrap()
689 .with_raw(&[0u8; 64])
690 .unwrap();
691 assert_eq!(indexer.code(), code);
692 assert_eq!(indexer.ondex(), Some(0));
693 }
694 }
695
696 #[test]
698 fn all_current_only_ondex_none() {
699 for code in [
700 IndexedSigCode::Ed25519Crt,
701 IndexedSigCode::ECDSA256k1Crt,
702 IndexedSigCode::ECDSA256r1Crt,
703 IndexedSigCode::Ed448Crt,
704 IndexedSigCode::Ed25519BigCrt,
705 IndexedSigCode::ECDSA256k1BigCrt,
706 IndexedSigCode::ECDSA256r1BigCrt,
707 IndexedSigCode::Ed448BigCrt,
708 ] {
709 let raw_size = code.raw_size();
710 let raw = vec![0u8; raw_size];
711 let indexer = IndexerBuilder::new()
712 .with_code(code)
713 .with_index(0)
714 .unwrap()
715 .with_raw(&raw)
716 .unwrap();
717 assert_eq!(
718 indexer.ondex(),
719 None,
720 "code {code:?} should have ondex=None"
721 );
722 }
723 }
724
725 #[test]
728 fn indexer_compute_full_size_rejects_overflow() {
729 assert_eq!(compute_full_size(1, 4).unwrap(), 8);
733 assert!(compute_full_size(usize::MAX / 4, 4).is_err());
734 assert!(compute_full_size(usize::MAX, 0).is_err());
735 }
736
737 #[test]
740 fn indexer_frame_size_fixed_and_truncated() {
741 let full = String::from("A") + &"A".repeat(87);
743 assert_eq!(IndexedSigCode::frame_size(full.as_bytes()).unwrap(), 88);
744 assert!(IndexedSigCode::frame_size(b"").is_err());
745 assert!(IndexedSigCode::frame_size(b"9").is_err()); }
747
748 #[test]
752 fn from_qb64_cesride_vector() {
753 let qb64 = "AACdI8OSQkMJ9r-xigjEByEjIua7LHH3AOJ22PQKqljMhuhcgh9nGRcKnsz5KvKd7K_H9-1298F4Id1DxvIoEmCQ";
754 let (indexer, consumed) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
755 assert_eq!(consumed, 88);
756 assert_eq!(indexer.code(), IndexedSigCode::Ed25519);
757 assert_eq!(indexer.index(), 0);
758 assert_eq!(indexer.ondex(), Some(0));
759 assert_eq!(indexer.to_qb64(), qb64);
761 }
762
763 #[test]
765 fn from_qb64_with_index() {
766 let original = IndexerBuilder::new()
767 .with_code(IndexedSigCode::Ed25519)
768 .with_index(5)
769 .unwrap()
770 .with_raw(&[0xAB; 64])
771 .unwrap();
772 let qb64 = original.to_qb64();
773 let (parsed, consumed) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
774 assert_eq!(consumed, 88);
775 assert_eq!(parsed.code(), IndexedSigCode::Ed25519);
776 assert_eq!(parsed.index(), 5);
777 assert_eq!(parsed.ondex(), Some(5));
778 assert_eq!(parsed.raw(), original.raw());
779 }
780
781 #[test]
783 fn from_qb64_big_variant() {
784 let original = IndexerBuilder::new()
785 .with_code(IndexedSigCode::Ed25519Big)
786 .with_index(100)
787 .unwrap()
788 .with_raw(&[0xCD; 64])
789 .unwrap();
790 let qb64 = original.to_qb64();
791 let (parsed, _) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
792 assert_eq!(parsed.code(), IndexedSigCode::Ed25519Big);
793 assert_eq!(parsed.index(), 100);
794 assert_eq!(parsed.ondex(), Some(100));
795 }
796
797 #[test]
799 fn from_qb64_ed448_separate_ondex() {
800 let original = IndexerBuilder::new()
801 .with_code(IndexedSigCode::Ed448)
802 .with_indices(2, 5)
803 .unwrap()
804 .with_raw(&[0xEE; 114])
805 .unwrap();
806 let qb64 = original.to_qb64();
807 let (parsed, _) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
808 assert_eq!(parsed.index(), 2);
809 assert_eq!(parsed.ondex(), Some(5));
810 }
811
812 #[test]
814 fn from_qb64_empty_stream() {
815 let result = IndexerBuilder::new().from_qb64(b"");
816 assert!(result.is_err());
817 }
818
819 #[test]
821 fn from_qb64_stream_too_short() {
822 let result = IndexerBuilder::new().from_qb64(b"A");
823 assert!(result.is_err());
824 }
825
826 #[test]
828 fn from_qb64_unknown_code() {
829 let result = IndexerBuilder::new().from_qb64(b"#invalid");
830 assert!(result.is_err());
831 }
832
833 #[test]
835 fn from_qb64_consumes_exact_bytes() {
836 let original = IndexerBuilder::new()
837 .with_code(IndexedSigCode::Ed25519)
838 .with_index(0)
839 .unwrap()
840 .with_raw(&[0u8; 64])
841 .unwrap();
842 let mut stream = original.to_qb64().into_bytes();
843 stream.extend_from_slice(b"EXTRA_STUFF");
844 let (_, consumed) = IndexerBuilder::new().from_qb64(&stream).unwrap();
845 assert_eq!(consumed, 88);
846 }
847
848 #[test]
850 fn from_qb64_current_only_ondex_none() {
851 let original = IndexerBuilder::new()
852 .with_code(IndexedSigCode::Ed25519Crt)
853 .with_index(3)
854 .unwrap()
855 .with_raw(&[0u8; 64])
856 .unwrap();
857 let qb64 = original.to_qb64();
858 let (parsed, _) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
859 assert_eq!(parsed.code(), IndexedSigCode::Ed25519Crt);
860 assert_eq!(parsed.index(), 3);
861 assert_eq!(parsed.ondex(), None);
862 }
863
864 #[rstest]
866 #[case(IndexedSigCode::Ed25519)]
867 #[case(IndexedSigCode::Ed25519Crt)]
868 #[case(IndexedSigCode::ECDSA256k1)]
869 #[case(IndexedSigCode::ECDSA256k1Crt)]
870 #[case(IndexedSigCode::ECDSA256r1)]
871 #[case(IndexedSigCode::ECDSA256r1Crt)]
872 #[case(IndexedSigCode::Ed448)]
873 #[case(IndexedSigCode::Ed448Crt)]
874 #[case(IndexedSigCode::Ed25519Big)]
875 #[case(IndexedSigCode::Ed25519BigCrt)]
876 #[case(IndexedSigCode::ECDSA256k1Big)]
877 #[case(IndexedSigCode::ECDSA256k1BigCrt)]
878 #[case(IndexedSigCode::ECDSA256r1Big)]
879 #[case(IndexedSigCode::ECDSA256r1BigCrt)]
880 #[case(IndexedSigCode::Ed448Big)]
881 #[case(IndexedSigCode::Ed448BigCrt)]
882 fn from_qb64_roundtrip_all_codes(#[case] code: IndexedSigCode) {
883 let raw = vec![0xAB_u8; code.raw_size()];
884 let original = IndexerBuilder::new()
885 .with_code(code)
886 .with_index(0)
887 .unwrap()
888 .with_raw(&raw)
889 .unwrap();
890 let qb64 = original.to_qb64();
891 let (parsed, _) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
892 assert_eq!(parsed.code(), original.code());
893 assert_eq!(parsed.raw(), original.raw());
894 assert_eq!(parsed.to_qb64(), qb64);
895 }
896
897 #[test]
899 fn from_qb64_ed448_big_distinct_indices() {
900 let original = IndexerBuilder::new()
901 .with_code(IndexedSigCode::Ed448Big)
902 .with_indices(1000, 500)
903 .unwrap()
904 .with_raw(&[0xFF; 114])
905 .unwrap();
906 let qb64 = original.to_qb64();
907 let (parsed, consumed) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
908 assert_eq!(consumed, 160);
909 assert_eq!(parsed.index(), 1000);
910 assert_eq!(parsed.ondex(), Some(500));
911 assert_eq!(parsed.raw(), original.raw());
912 }
913
914 #[test]
918 fn from_qb2_roundtrip() {
919 let original = IndexerBuilder::new()
920 .with_code(IndexedSigCode::Ed25519)
921 .with_index(0)
922 .unwrap()
923 .with_raw(&[0xAB; 64])
924 .unwrap();
925 let qb2 = original.to_qb2();
926 let (parsed, consumed) = IndexerBuilder::new().from_qb2(&qb2).unwrap();
927 assert_eq!(consumed, 66); assert_eq!(parsed.code(), original.code());
929 assert_eq!(parsed.index(), original.index());
930 assert_eq!(parsed.raw(), original.raw());
931 }
932
933 #[test]
935 fn from_qb2_empty_stream() {
936 let result = IndexerBuilder::new().from_qb2(b"");
937 assert!(result.is_err());
938 }
939
940 #[rstest]
942 #[case(IndexedSigCode::Ed25519)]
943 #[case(IndexedSigCode::Ed25519Crt)]
944 #[case(IndexedSigCode::ECDSA256k1)]
945 #[case(IndexedSigCode::ECDSA256k1Crt)]
946 #[case(IndexedSigCode::ECDSA256r1)]
947 #[case(IndexedSigCode::ECDSA256r1Crt)]
948 #[case(IndexedSigCode::Ed448)]
949 #[case(IndexedSigCode::Ed448Crt)]
950 #[case(IndexedSigCode::Ed25519Big)]
951 #[case(IndexedSigCode::Ed25519BigCrt)]
952 #[case(IndexedSigCode::ECDSA256k1Big)]
953 #[case(IndexedSigCode::ECDSA256k1BigCrt)]
954 #[case(IndexedSigCode::ECDSA256r1Big)]
955 #[case(IndexedSigCode::ECDSA256r1BigCrt)]
956 #[case(IndexedSigCode::Ed448Big)]
957 #[case(IndexedSigCode::Ed448BigCrt)]
958 fn from_qb2_roundtrip_all_codes(#[case] code: IndexedSigCode) {
959 let raw = vec![0xCD_u8; code.raw_size()];
960 let original = IndexerBuilder::new()
961 .with_code(code)
962 .with_index(0)
963 .unwrap()
964 .with_raw(&raw)
965 .unwrap();
966 let qb2 = original.to_qb2();
967 let qb64_len = original.to_qb64().len();
968 let expected_bfs = qb64_len * 3 / 4;
969 let (parsed, consumed) = IndexerBuilder::new().from_qb2(&qb2).unwrap();
970 assert_eq!(
971 consumed, expected_bfs,
972 "consumed mismatch for code {code:?}"
973 );
974 assert_eq!(parsed.code(), original.code());
975 assert_eq!(parsed.raw(), original.raw());
976 }
977
978 #[test]
980 fn from_qb2_consumes_exact_bytes() {
981 let original = IndexerBuilder::new()
982 .with_code(IndexedSigCode::Ed25519)
983 .with_index(0)
984 .unwrap()
985 .with_raw(&[0u8; 64])
986 .unwrap();
987 let mut qb2 = original.to_qb2();
988 qb2.extend_from_slice(&[0xFF; 20]);
989 let (_, consumed) = IndexerBuilder::new().from_qb2(&qb2).unwrap();
990 assert_eq!(consumed, 66); }
992
993 #[test]
995 fn from_qb2_big_code() {
996 let original = IndexerBuilder::new()
997 .with_code(IndexedSigCode::Ed25519Big)
998 .with_index(100)
999 .unwrap()
1000 .with_raw(&[0xAB; 64])
1001 .unwrap();
1002 let qb2 = original.to_qb2();
1003 let (parsed, consumed) = IndexerBuilder::new().from_qb2(&qb2).unwrap();
1004 assert_eq!(consumed, 69); assert_eq!(parsed.code(), IndexedSigCode::Ed25519Big);
1006 assert_eq!(parsed.index(), 100);
1007 }
1008
1009 #[test]
1012 fn cross_format_roundtrip() {
1013 let original = IndexerBuilder::new()
1014 .with_code(IndexedSigCode::Ed448)
1015 .with_indices(7, 3)
1016 .unwrap()
1017 .with_raw(&[0x42; 114])
1018 .unwrap();
1019
1020 let qb64 = original.to_qb64();
1022 let (from_b64, _) = IndexerBuilder::new().from_qb64(qb64.as_bytes()).unwrap();
1023 assert_eq!(from_b64.raw(), original.raw());
1024 assert_eq!(from_b64.index(), 7);
1025 assert_eq!(from_b64.ondex(), Some(3));
1026
1027 let qb2 = original.to_qb2();
1029 let (from_b2, _) = IndexerBuilder::new().from_qb2(&qb2).unwrap();
1030 assert_eq!(from_b2.raw(), original.raw());
1031 assert_eq!(from_b2.index(), 7);
1032 assert_eq!(from_b2.ondex(), Some(3));
1033
1034 assert_eq!(from_b64, from_b2);
1036 }
1037}