use alloc::borrow::Cow;
#[cfg(feature = "alloc")]
#[allow(
unused_imports,
reason = "alloc prelude items; subset used per cfg/feature combination"
)]
use alloc::{format, string::String, vec, vec::Vec};
use base64::{Engine, engine::general_purpose as b64};
use super::code::IndexedSigCode;
use super::xizage::XizageSize;
const B64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
fn int_to_b64(value: u32, len: usize) -> String {
if len == 0 {
return String::new();
}
let mut buf = vec![b'A'; len];
let mut v = value;
let mut i = len;
while v > 0 && i > 0 {
i -= 1;
#[allow(clippy::as_conversions, reason = "v % 64 always fits in usize")]
let idx = (v % 64) as usize;
buf[i] = B64_CHARS[idx];
v /= 64;
}
String::from_utf8(buf.clone()).unwrap_or_default()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Indexer<'a> {
code: IndexedSigCode,
index: u32,
ondex: Option<u32>, raw: Cow<'a, [u8]>,
}
impl<'a> Indexer<'a> {
pub(crate) const fn new(
code: IndexedSigCode,
index: u32,
ondex: Option<u32>,
raw: Cow<'a, [u8]>,
) -> Self {
Self {
code,
index,
ondex,
raw,
}
}
#[must_use]
pub const fn code(&self) -> IndexedSigCode {
self.code
}
#[must_use]
pub const fn index(&self) -> u32 {
self.index
}
#[must_use]
pub const fn ondex(&self) -> Option<u32> {
self.ondex
}
#[must_use]
pub fn raw(&self) -> &[u8] {
&self.raw
}
#[must_use]
pub fn full_size(&self) -> usize {
match self.code.get_xizage().fs {
XizageSize::Fixed(n) => usize::from(n),
XizageSize::Variable => {
unreachable!("no variable-size indexed sig codes exist")
}
}
}
#[must_use]
pub fn to_qb64(&self) -> String {
let xizage = self.code.get_xizage();
let hs = usize::from(xizage.hs);
let ss = usize::from(xizage.ss);
let os = usize::from(xizage.os);
let ls = usize::from(xizage.ls);
let fs = self.full_size();
let ms = ss - os;
let ps = (3 - (self.raw.len() % 3)) % 3;
let code_str = self.code.as_str();
let index_b64 = int_to_b64(self.index, ms);
let ondex_val = self.ondex.unwrap_or(0);
let ondex_b64 = int_to_b64(ondex_val, os);
let header = format!("{code_str}{index_b64}{ondex_b64}");
debug_assert_eq!(header.len(), hs + ss);
let mut padded = vec![0u8; self.raw.len() + ps];
padded[ps..].copy_from_slice(&self.raw);
let b64_raw = b64::URL_SAFE_NO_PAD.encode(&padded);
let stripped = &b64_raw[(ps - ls)..];
let full = format!("{header}{stripped}");
assert_eq!(
full.len(),
fs,
"qb64 length {} != expected fs {} for code {:?}",
full.len(),
fs,
self.code,
);
full
}
#[must_use]
pub fn to_qb2(&self) -> Vec<u8> {
let qb64 = self.to_qb64();
b64::URL_SAFE_NO_PAD.decode(qb64).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use rstest::rstest;
use super::*;
use crate::core::indexer::builder::IndexerBuilder;
#[test]
fn accessors() {
let raw = vec![0u8; 64];
let indexer = Indexer::new(IndexedSigCode::Ed25519, 3, Some(3), Cow::Borrowed(&raw));
assert_eq!(indexer.code(), IndexedSigCode::Ed25519);
assert_eq!(indexer.index(), 3);
assert_eq!(indexer.ondex(), Some(3));
assert_eq!(indexer.raw().len(), 64);
}
#[test]
fn ondex_none_for_current_only() {
let raw = vec![0u8; 64];
let indexer = Indexer::new(IndexedSigCode::Ed25519Crt, 5, None, Cow::Borrowed(&raw));
assert_eq!(indexer.ondex(), None);
}
#[rstest]
#[case(IndexedSigCode::Ed25519, 88)]
#[case(IndexedSigCode::Ed25519Crt, 88)]
#[case(IndexedSigCode::Ed448, 156)]
#[case(IndexedSigCode::Ed25519Big, 92)]
#[case(IndexedSigCode::Ed448Big, 160)]
fn full_size(#[case] code: IndexedSigCode, #[case] expected: usize) {
let raw = vec![0u8; code.raw_size()];
let indexer = Indexer::new(code, 0, Some(0), Cow::Owned(raw));
assert_eq!(indexer.full_size(), expected);
}
#[test]
fn owned_indexer_is_static() {
let indexer: Indexer<'static> = Indexer::new(
IndexedSigCode::Ed25519,
0,
Some(0),
Cow::Owned(vec![0u8; 64]),
);
assert_eq!(indexer.code(), IndexedSigCode::Ed25519);
}
#[test]
fn clone_and_eq() {
let raw = vec![0u8; 64];
let a = Indexer::new(IndexedSigCode::Ed25519, 0, Some(0), Cow::Owned(raw));
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn to_qb64_cesride_vector() {
let qb64 = "AACdI8OSQkMJ9r-xigjEByEjIua7LHH3AOJ22PQKqljMhuhcgh9nGRcKnsz5KvKd7K_H9-1298F4Id1DxvIoEmCQ";
let qb2 = b64::URL_SAFE_NO_PAD.decode(qb64).unwrap();
assert_eq!(qb2.len(), 66);
let raw = &qb2[2..];
assert_eq!(raw.len(), 64);
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519)
.with_index(0)
.unwrap()
.with_raw(raw)
.unwrap();
assert_eq!(indexer.to_qb64(), qb64);
}
#[test]
fn to_qb64_with_nonzero_index() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519)
.with_index(5)
.unwrap()
.with_raw(&[0u8; 64])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 88);
assert!(qb64.starts_with("AF")); }
#[test]
fn to_qb64_big_code() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519Big)
.with_index(100)
.unwrap()
.with_raw(&[0u8; 64])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 92); assert!(qb64.starts_with("2A")); }
#[test]
fn to_qb2_roundtrips_with_qb64() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519)
.with_index(0)
.unwrap()
.with_raw(&[0xAB; 64])
.unwrap();
let qb64 = indexer.to_qb64();
let qb2 = indexer.to_qb2();
let decoded = b64::URL_SAFE_NO_PAD.decode(&qb64).unwrap();
assert_eq!(qb2, decoded);
}
#[rstest]
#[case(IndexedSigCode::Ed25519)]
#[case(IndexedSigCode::Ed25519Crt)]
#[case(IndexedSigCode::ECDSA256k1)]
#[case(IndexedSigCode::ECDSA256k1Crt)]
#[case(IndexedSigCode::ECDSA256r1)]
#[case(IndexedSigCode::ECDSA256r1Crt)]
#[case(IndexedSigCode::Ed448)]
#[case(IndexedSigCode::Ed448Crt)]
#[case(IndexedSigCode::Ed25519Big)]
#[case(IndexedSigCode::Ed25519BigCrt)]
#[case(IndexedSigCode::ECDSA256k1Big)]
#[case(IndexedSigCode::ECDSA256k1BigCrt)]
#[case(IndexedSigCode::ECDSA256r1Big)]
#[case(IndexedSigCode::ECDSA256r1BigCrt)]
#[case(IndexedSigCode::Ed448Big)]
#[case(IndexedSigCode::Ed448BigCrt)]
fn full_size_matches_qb64_len(#[case] code: IndexedSigCode) {
let raw = vec![0u8; code.raw_size()];
let indexer = IndexerBuilder::new()
.with_code(code)
.with_index(0)
.unwrap()
.with_raw(&raw)
.unwrap();
assert_eq!(
indexer.to_qb64().len(),
indexer.full_size(),
"qb64 length mismatch for code {code:?}",
);
}
#[test]
fn to_qb64_current_only_produces_valid_output() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519Crt)
.with_index(3)
.unwrap()
.with_raw(&[0u8; 64])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 88);
assert!(qb64.starts_with("BD")); }
#[test]
fn to_qb64_ed448_has_ondex_field() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed448)
.with_index(7)
.unwrap()
.with_raw(&[0u8; 114])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 156);
assert!(qb64.starts_with("0AHH"));
}
#[test]
fn to_qb64_ed448_with_different_ondex() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed448)
.with_indices(2, 5)
.unwrap()
.with_raw(&[0u8; 114])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 156);
assert!(qb64.starts_with("0ACF"));
}
#[test]
fn to_qb64_big_with_different_indices() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519Big)
.with_indices(10, 20)
.unwrap()
.with_raw(&[0u8; 64])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 92);
assert!(qb64.starts_with("2AAKAU"));
}
#[test]
fn to_qb2_len_is_three_quarters_of_qb64() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed25519)
.with_index(0)
.unwrap()
.with_raw(&[0u8; 64])
.unwrap();
let qb64_len = indexer.to_qb64().len();
let qb2_len = indexer.to_qb2().len();
assert_eq!(qb2_len * 4, qb64_len * 3);
}
#[test]
fn to_qb64_ed448_big_code() {
let indexer = IndexerBuilder::new()
.with_code(IndexedSigCode::Ed448Big)
.with_indices(1000, 500)
.unwrap()
.with_raw(&[0xFF; 114])
.unwrap();
let qb64 = indexer.to_qb64();
assert_eq!(qb64.len(), 160);
assert!(qb64.starts_with("3A"));
}
}