Skip to main content

cesr/core/indexer/
code.rs

1use super::xizage::{Xizage, XizageSize};
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, string::String};
8use thiserror::Error as ThisError;
9
10/// Whether the indexed signature carries both current and prior-next indices.
11#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
12pub enum IndexMode {
13    /// Index appears in both current signing and prior-next key lists.
14    Both,
15    /// Index for current signing key list only.
16    CurrentOnly,
17}
18
19/// Error returned when a hard code string is not a recognized indexed signature
20/// code.
21#[derive(Debug, ThisError, PartialEq, Eq)]
22pub enum CodeError {
23    /// The hard code string does not match any known indexed signature code.
24    #[error("unknown indexed sig code: '{0}'")]
25    UnknownCode(String),
26}
27
28/// The 16 CESR indexed signature codes.
29///
30/// Each variant maps to a fixed CESR hard code string, a sizage table entry,
31/// and metadata about its index mode, raw size, and capacity.
32///
33/// "Crt" (current-only) variants carry only the signer's index in the current
34/// key list. Non-Crt variants carry both a current index and a prior-next
35/// "ondex".
36///
37/// "Big" variants use a wider soft field and support larger index values than
38/// their small counterparts.
39#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
40pub enum IndexedSigCode {
41    /// Ed25519 indexed signature — `"A"`
42    Ed25519,
43    /// Ed25519 current-only indexed signature — `"B"`
44    Ed25519Crt,
45    /// ECDSA secp256k1 indexed signature — `"C"`
46    ECDSA256k1,
47    /// ECDSA secp256k1 current-only indexed signature — `"D"`
48    ECDSA256k1Crt,
49    /// ECDSA P-256 indexed signature — `"E"`
50    ECDSA256r1,
51    /// ECDSA P-256 current-only indexed signature — `"F"`
52    ECDSA256r1Crt,
53    /// Ed448 indexed signature — `"0A"`
54    Ed448,
55    /// Ed448 current-only indexed signature — `"0B"`
56    Ed448Crt,
57    /// Ed25519 big indexed signature — `"2A"`
58    Ed25519Big,
59    /// Ed25519 big current-only indexed signature — `"2B"`
60    Ed25519BigCrt,
61    /// ECDSA secp256k1 big indexed signature — `"2C"`
62    ECDSA256k1Big,
63    /// ECDSA secp256k1 big current-only indexed signature — `"2D"`
64    ECDSA256k1BigCrt,
65    /// ECDSA P-256 big indexed signature — `"2E"`
66    ECDSA256r1Big,
67    /// ECDSA P-256 big current-only indexed signature — `"2F"`
68    ECDSA256r1BigCrt,
69    /// Ed448 big indexed signature — `"3A"`
70    Ed448Big,
71    /// Ed448 big current-only indexed signature — `"3B"`
72    Ed448BigCrt,
73}
74
75impl IndexedSigCode {
76    /// Returns the CESR wire code string for this variant.
77    #[must_use]
78    pub const fn as_str(&self) -> &'static str {
79        match self {
80            Self::Ed25519 => "A",
81            Self::Ed25519Crt => "B",
82            Self::ECDSA256k1 => "C",
83            Self::ECDSA256k1Crt => "D",
84            Self::ECDSA256r1 => "E",
85            Self::ECDSA256r1Crt => "F",
86            Self::Ed448 => "0A",
87            Self::Ed448Crt => "0B",
88            Self::Ed25519Big => "2A",
89            Self::Ed25519BigCrt => "2B",
90            Self::ECDSA256k1Big => "2C",
91            Self::ECDSA256k1BigCrt => "2D",
92            Self::ECDSA256r1Big => "2E",
93            Self::ECDSA256r1BigCrt => "2F",
94            Self::Ed448Big => "3A",
95            Self::Ed448BigCrt => "3B",
96        }
97    }
98
99    /// Parses a hard code string back to the corresponding enum variant.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`CodeError::UnknownCode`] if `hard` does not match any known
104    /// indexed signature code.
105    pub fn from_hard(hard: &str) -> Result<Self, CodeError> {
106        match hard {
107            "A" => Ok(Self::Ed25519),
108            "B" => Ok(Self::Ed25519Crt),
109            "C" => Ok(Self::ECDSA256k1),
110            "D" => Ok(Self::ECDSA256k1Crt),
111            "E" => Ok(Self::ECDSA256r1),
112            "F" => Ok(Self::ECDSA256r1Crt),
113            "0A" => Ok(Self::Ed448),
114            "0B" => Ok(Self::Ed448Crt),
115            "2A" => Ok(Self::Ed25519Big),
116            "2B" => Ok(Self::Ed25519BigCrt),
117            "2C" => Ok(Self::ECDSA256k1Big),
118            "2D" => Ok(Self::ECDSA256k1BigCrt),
119            "2E" => Ok(Self::ECDSA256r1Big),
120            "2F" => Ok(Self::ECDSA256r1BigCrt),
121            "3A" => Ok(Self::Ed448Big),
122            "3B" => Ok(Self::Ed448BigCrt),
123            _ => Err(CodeError::UnknownCode(hard.to_owned())),
124        }
125    }
126
127    /// Returns the [`Xizage`] sizage entry for this code (from the cesride
128    /// reference table).
129    #[must_use]
130    pub const fn get_xizage(&self) -> Xizage {
131        match self {
132            Self::Ed25519
133            | Self::Ed25519Crt
134            | Self::ECDSA256k1
135            | Self::ECDSA256k1Crt
136            | Self::ECDSA256r1
137            | Self::ECDSA256r1Crt => Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0),
138            Self::Ed448 | Self::Ed448Crt => Xizage::new(2, 2, 1, XizageSize::Fixed(156), 0),
139            Self::Ed25519Big
140            | Self::Ed25519BigCrt
141            | Self::ECDSA256k1Big
142            | Self::ECDSA256k1BigCrt
143            | Self::ECDSA256r1Big
144            | Self::ECDSA256r1BigCrt => Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0),
145            Self::Ed448Big | Self::Ed448BigCrt => Xizage::new(2, 6, 3, XizageSize::Fixed(160), 0),
146        }
147    }
148
149    /// Returns the [`IndexMode`] for this code.
150    ///
151    /// `Both` variants carry a current index and a prior-next "ondex".
152    /// `CurrentOnly` variants carry only the current index.
153    #[must_use]
154    pub const fn mode(&self) -> IndexMode {
155        match self {
156            Self::Ed25519
157            | Self::ECDSA256k1
158            | Self::ECDSA256r1
159            | Self::Ed448
160            | Self::Ed25519Big
161            | Self::ECDSA256k1Big
162            | Self::ECDSA256r1Big
163            | Self::Ed448Big => IndexMode::Both,
164
165            Self::Ed25519Crt
166            | Self::ECDSA256k1Crt
167            | Self::ECDSA256r1Crt
168            | Self::Ed448Crt
169            | Self::Ed25519BigCrt
170            | Self::ECDSA256k1BigCrt
171            | Self::ECDSA256r1BigCrt
172            | Self::Ed448BigCrt => IndexMode::CurrentOnly,
173        }
174    }
175
176    /// Returns `true` if this is a "big" variant with a wider index field.
177    #[must_use]
178    pub const fn is_big(&self) -> bool {
179        match self {
180            Self::Ed25519
181            | Self::Ed25519Crt
182            | Self::ECDSA256k1
183            | Self::ECDSA256k1Crt
184            | Self::ECDSA256r1
185            | Self::ECDSA256r1Crt
186            | Self::Ed448
187            | Self::Ed448Crt => false,
188
189            Self::Ed25519Big
190            | Self::Ed25519BigCrt
191            | Self::ECDSA256k1Big
192            | Self::ECDSA256k1BigCrt
193            | Self::ECDSA256r1Big
194            | Self::ECDSA256r1BigCrt
195            | Self::Ed448Big
196            | Self::Ed448BigCrt => true,
197        }
198    }
199
200    /// Returns the raw signature size in bytes.
201    ///
202    /// Ed25519 and ECDSA (256-bit) signatures are 64 bytes.
203    /// Ed448 signatures are 114 bytes.
204    /// The size is the same regardless of small vs. big variants.
205    #[must_use]
206    pub const fn raw_size(&self) -> usize {
207        match self {
208            Self::Ed25519
209            | Self::Ed25519Crt
210            | Self::Ed25519Big
211            | Self::Ed25519BigCrt
212            | Self::ECDSA256k1
213            | Self::ECDSA256k1Crt
214            | Self::ECDSA256k1Big
215            | Self::ECDSA256k1BigCrt
216            | Self::ECDSA256r1
217            | Self::ECDSA256r1Crt
218            | Self::ECDSA256r1Big
219            | Self::ECDSA256r1BigCrt => 64,
220
221            Self::Ed448 | Self::Ed448Crt | Self::Ed448Big | Self::Ed448BigCrt => 114,
222        }
223    }
224
225    /// Returns the maximum index value this code can encode.
226    ///
227    /// Formula: `64^(ss - os) - 1` where `ss` is the soft size and `os` is the
228    /// ondex size from the sizage table.
229    #[must_use]
230    #[allow(clippy::as_conversions, reason = "From::from() is not const-stable")]
231    pub const fn max_index(&self) -> u32 {
232        let xizage = self.get_xizage();
233        let exp = (xizage.ss - xizage.os) as u32;
234        64u32.pow(exp) - 1
235    }
236
237    /// Returns the maximum ondex (other-index) value, or `None` for
238    /// current-only codes.
239    ///
240    /// For `Both`-mode codes with `os > 0`: `64^os - 1`.
241    /// For `Both`-mode codes with `os == 0` (small codes): the ondex must equal
242    /// the index, so the max ondex equals `max_index()`.
243    #[must_use]
244    #[allow(clippy::as_conversions, reason = "From::from() is not const-stable")]
245    pub const fn max_ondex(&self) -> Option<u32> {
246        match self.mode() {
247            IndexMode::CurrentOnly => None,
248            IndexMode::Both => {
249                let os = self.get_xizage().os as u32;
250                if os == 0 {
251                    Some(self.max_index())
252                } else {
253                    Some(64u32.pow(os) - 1)
254                }
255            }
256        }
257    }
258
259    /// Auto-upgrades a small code to its big counterpart if `index` exceeds the
260    /// small code's maximum index capacity.
261    ///
262    /// If the code is already big, or if `index` fits in the small code, returns
263    /// `self` unchanged.
264    #[must_use]
265    pub const fn for_index(self, index: u32) -> Self {
266        if self.is_big() || index <= self.max_index() {
267            return self;
268        }
269        match self {
270            Self::Ed25519 => Self::Ed25519Big,
271            Self::Ed25519Crt => Self::Ed25519BigCrt,
272            Self::ECDSA256k1 => Self::ECDSA256k1Big,
273            Self::ECDSA256k1Crt => Self::ECDSA256k1BigCrt,
274            Self::ECDSA256r1 => Self::ECDSA256r1Big,
275            Self::ECDSA256r1Crt => Self::ECDSA256r1BigCrt,
276            Self::Ed448 => Self::Ed448Big,
277            Self::Ed448Crt => Self::Ed448BigCrt,
278            // Big variants are already handled above.
279            _ => self,
280        }
281    }
282}
283
284/// Maps the first character of a CESR code to the hard (stable) size in
285/// characters.
286///
287/// Returns `None` if the character is not a valid CESR code lead.
288#[must_use]
289pub const fn hardage(c: char) -> Option<usize> {
290    match c {
291        'A'..='Z' | 'a'..='z' => Some(1),
292        '0'..='4' => Some(2),
293        _ => None,
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use rstest::rstest;
301
302    // ── as_str / from_hard roundtrip ─────────────────────────────────────
303
304    #[rstest]
305    #[case(IndexedSigCode::Ed25519, "A")]
306    #[case(IndexedSigCode::Ed25519Crt, "B")]
307    #[case(IndexedSigCode::ECDSA256k1, "C")]
308    #[case(IndexedSigCode::ECDSA256k1Crt, "D")]
309    #[case(IndexedSigCode::ECDSA256r1, "E")]
310    #[case(IndexedSigCode::ECDSA256r1Crt, "F")]
311    #[case(IndexedSigCode::Ed448, "0A")]
312    #[case(IndexedSigCode::Ed448Crt, "0B")]
313    #[case(IndexedSigCode::Ed25519Big, "2A")]
314    #[case(IndexedSigCode::Ed25519BigCrt, "2B")]
315    #[case(IndexedSigCode::ECDSA256k1Big, "2C")]
316    #[case(IndexedSigCode::ECDSA256k1BigCrt, "2D")]
317    #[case(IndexedSigCode::ECDSA256r1Big, "2E")]
318    #[case(IndexedSigCode::ECDSA256r1BigCrt, "2F")]
319    #[case(IndexedSigCode::Ed448Big, "3A")]
320    #[case(IndexedSigCode::Ed448BigCrt, "3B")]
321    fn as_str_values(#[case] code: IndexedSigCode, #[case] expected: &str) {
322        assert_eq!(code.as_str(), expected);
323    }
324
325    #[rstest]
326    #[case(IndexedSigCode::Ed25519, "A")]
327    #[case(IndexedSigCode::Ed25519Crt, "B")]
328    #[case(IndexedSigCode::ECDSA256k1, "C")]
329    #[case(IndexedSigCode::ECDSA256k1Crt, "D")]
330    #[case(IndexedSigCode::ECDSA256r1, "E")]
331    #[case(IndexedSigCode::ECDSA256r1Crt, "F")]
332    #[case(IndexedSigCode::Ed448, "0A")]
333    #[case(IndexedSigCode::Ed448Crt, "0B")]
334    #[case(IndexedSigCode::Ed25519Big, "2A")]
335    #[case(IndexedSigCode::Ed25519BigCrt, "2B")]
336    #[case(IndexedSigCode::ECDSA256k1Big, "2C")]
337    #[case(IndexedSigCode::ECDSA256k1BigCrt, "2D")]
338    #[case(IndexedSigCode::ECDSA256r1Big, "2E")]
339    #[case(IndexedSigCode::ECDSA256r1BigCrt, "2F")]
340    #[case(IndexedSigCode::Ed448Big, "3A")]
341    #[case(IndexedSigCode::Ed448BigCrt, "3B")]
342    fn from_hard_roundtrip(#[case] code: IndexedSigCode, #[case] wire: &str) {
343        assert_eq!(IndexedSigCode::from_hard(wire).unwrap(), code);
344        assert_eq!(IndexedSigCode::from_hard(code.as_str()).unwrap(), code);
345    }
346
347    #[rstest]
348    #[case("Z")]
349    #[case("0Z")]
350    #[case("XX")]
351    #[case("")]
352    fn from_hard_unknown(#[case] bad: &str) {
353        assert!(IndexedSigCode::from_hard(bad).is_err());
354        let err = IndexedSigCode::from_hard(bad).unwrap_err();
355        assert_eq!(err, CodeError::UnknownCode(bad.to_owned()));
356    }
357
358    // ── get_xizage ──────────────────────────────────────────────────────
359
360    #[rstest]
361    #[case(
362        IndexedSigCode::Ed25519,
363        Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0)
364    )]
365    #[case(
366        IndexedSigCode::Ed25519Crt,
367        Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0)
368    )]
369    #[case(
370        IndexedSigCode::ECDSA256k1,
371        Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0)
372    )]
373    #[case(
374        IndexedSigCode::ECDSA256k1Crt,
375        Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0)
376    )]
377    #[case(
378        IndexedSigCode::ECDSA256r1,
379        Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0)
380    )]
381    #[case(
382        IndexedSigCode::ECDSA256r1Crt,
383        Xizage::new(1, 1, 0, XizageSize::Fixed(88), 0)
384    )]
385    #[case(IndexedSigCode::Ed448, Xizage::new(2, 2, 1, XizageSize::Fixed(156), 0))]
386    #[case(
387        IndexedSigCode::Ed448Crt,
388        Xizage::new(2, 2, 1, XizageSize::Fixed(156), 0)
389    )]
390    #[case(
391        IndexedSigCode::Ed25519Big,
392        Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0)
393    )]
394    #[case(
395        IndexedSigCode::Ed25519BigCrt,
396        Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0)
397    )]
398    #[case(
399        IndexedSigCode::ECDSA256k1Big,
400        Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0)
401    )]
402    #[case(
403        IndexedSigCode::ECDSA256k1BigCrt,
404        Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0)
405    )]
406    #[case(
407        IndexedSigCode::ECDSA256r1Big,
408        Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0)
409    )]
410    #[case(
411        IndexedSigCode::ECDSA256r1BigCrt,
412        Xizage::new(2, 4, 2, XizageSize::Fixed(92), 0)
413    )]
414    #[case(
415        IndexedSigCode::Ed448Big,
416        Xizage::new(2, 6, 3, XizageSize::Fixed(160), 0)
417    )]
418    #[case(
419        IndexedSigCode::Ed448BigCrt,
420        Xizage::new(2, 6, 3, XizageSize::Fixed(160), 0)
421    )]
422    fn xizage_values(#[case] code: IndexedSigCode, #[case] expected: Xizage) {
423        assert_eq!(code.get_xizage(), expected);
424    }
425
426    // ── mode ────────────────────────────────────────────────────────────
427
428    #[rstest]
429    #[case(IndexedSigCode::Ed25519, IndexMode::Both)]
430    #[case(IndexedSigCode::Ed25519Crt, IndexMode::CurrentOnly)]
431    #[case(IndexedSigCode::ECDSA256k1, IndexMode::Both)]
432    #[case(IndexedSigCode::ECDSA256k1Crt, IndexMode::CurrentOnly)]
433    #[case(IndexedSigCode::ECDSA256r1, IndexMode::Both)]
434    #[case(IndexedSigCode::ECDSA256r1Crt, IndexMode::CurrentOnly)]
435    #[case(IndexedSigCode::Ed448, IndexMode::Both)]
436    #[case(IndexedSigCode::Ed448Crt, IndexMode::CurrentOnly)]
437    #[case(IndexedSigCode::Ed25519Big, IndexMode::Both)]
438    #[case(IndexedSigCode::Ed25519BigCrt, IndexMode::CurrentOnly)]
439    #[case(IndexedSigCode::ECDSA256k1Big, IndexMode::Both)]
440    #[case(IndexedSigCode::ECDSA256k1BigCrt, IndexMode::CurrentOnly)]
441    #[case(IndexedSigCode::ECDSA256r1Big, IndexMode::Both)]
442    #[case(IndexedSigCode::ECDSA256r1BigCrt, IndexMode::CurrentOnly)]
443    #[case(IndexedSigCode::Ed448Big, IndexMode::Both)]
444    #[case(IndexedSigCode::Ed448BigCrt, IndexMode::CurrentOnly)]
445    fn mode_values(#[case] code: IndexedSigCode, #[case] expected: IndexMode) {
446        assert_eq!(code.mode(), expected);
447    }
448
449    // ── is_big ──────────────────────────────────────────────────────────
450
451    #[rstest]
452    #[case(IndexedSigCode::Ed25519, false)]
453    #[case(IndexedSigCode::Ed25519Crt, false)]
454    #[case(IndexedSigCode::ECDSA256k1, false)]
455    #[case(IndexedSigCode::ECDSA256k1Crt, false)]
456    #[case(IndexedSigCode::ECDSA256r1, false)]
457    #[case(IndexedSigCode::ECDSA256r1Crt, false)]
458    #[case(IndexedSigCode::Ed448, false)]
459    #[case(IndexedSigCode::Ed448Crt, false)]
460    #[case(IndexedSigCode::Ed25519Big, true)]
461    #[case(IndexedSigCode::Ed25519BigCrt, true)]
462    #[case(IndexedSigCode::ECDSA256k1Big, true)]
463    #[case(IndexedSigCode::ECDSA256k1BigCrt, true)]
464    #[case(IndexedSigCode::ECDSA256r1Big, true)]
465    #[case(IndexedSigCode::ECDSA256r1BigCrt, true)]
466    #[case(IndexedSigCode::Ed448Big, true)]
467    #[case(IndexedSigCode::Ed448BigCrt, true)]
468    fn is_big_values(#[case] code: IndexedSigCode, #[case] expected: bool) {
469        assert_eq!(code.is_big(), expected);
470    }
471
472    // ── raw_size ────────────────────────────────────────────────────────
473
474    #[rstest]
475    #[case(IndexedSigCode::Ed25519, 64)]
476    #[case(IndexedSigCode::Ed25519Crt, 64)]
477    #[case(IndexedSigCode::ECDSA256k1, 64)]
478    #[case(IndexedSigCode::ECDSA256k1Crt, 64)]
479    #[case(IndexedSigCode::ECDSA256r1, 64)]
480    #[case(IndexedSigCode::ECDSA256r1Crt, 64)]
481    #[case(IndexedSigCode::Ed448, 114)]
482    #[case(IndexedSigCode::Ed448Crt, 114)]
483    #[case(IndexedSigCode::Ed25519Big, 64)]
484    #[case(IndexedSigCode::Ed25519BigCrt, 64)]
485    #[case(IndexedSigCode::ECDSA256k1Big, 64)]
486    #[case(IndexedSigCode::ECDSA256k1BigCrt, 64)]
487    #[case(IndexedSigCode::ECDSA256r1Big, 64)]
488    #[case(IndexedSigCode::ECDSA256r1BigCrt, 64)]
489    #[case(IndexedSigCode::Ed448Big, 114)]
490    #[case(IndexedSigCode::Ed448BigCrt, 114)]
491    fn raw_size_values(#[case] code: IndexedSigCode, #[case] expected: usize) {
492        assert_eq!(code.raw_size(), expected);
493    }
494
495    // ── max_index ───────────────────────────────────────────────────────
496
497    #[rstest]
498    // Small codes: ss=1, os=0 => 64^1 - 1 = 63
499    #[case(IndexedSigCode::Ed25519, 63)]
500    #[case(IndexedSigCode::Ed25519Crt, 63)]
501    #[case(IndexedSigCode::ECDSA256k1, 63)]
502    #[case(IndexedSigCode::ECDSA256k1Crt, 63)]
503    #[case(IndexedSigCode::ECDSA256r1, 63)]
504    #[case(IndexedSigCode::ECDSA256r1Crt, 63)]
505    // Small Ed448: ss=2, os=1 => 64^1 - 1 = 63
506    #[case(IndexedSigCode::Ed448, 63)]
507    #[case(IndexedSigCode::Ed448Crt, 63)]
508    // Big Ed25519/ECDSA: ss=4, os=2 => 64^2 - 1 = 4095
509    #[case(IndexedSigCode::Ed25519Big, 4095)]
510    #[case(IndexedSigCode::Ed25519BigCrt, 4095)]
511    #[case(IndexedSigCode::ECDSA256k1Big, 4095)]
512    #[case(IndexedSigCode::ECDSA256k1BigCrt, 4095)]
513    #[case(IndexedSigCode::ECDSA256r1Big, 4095)]
514    #[case(IndexedSigCode::ECDSA256r1BigCrt, 4095)]
515    // Big Ed448: ss=6, os=3 => 64^3 - 1 = 262_143
516    #[case(IndexedSigCode::Ed448Big, 262_143)]
517    #[case(IndexedSigCode::Ed448BigCrt, 262_143)]
518    fn max_index_values(#[case] code: IndexedSigCode, #[case] expected: u32) {
519        assert_eq!(code.max_index(), expected);
520    }
521
522    // ── max_ondex ───────────────────────────────────────────────────────
523
524    #[rstest]
525    // Small Both (os=0): ondex must equal index, so max_ondex = max_index = 63
526    #[case(IndexedSigCode::Ed25519, Some(63))]
527    #[case(IndexedSigCode::ECDSA256k1, Some(63))]
528    #[case(IndexedSigCode::ECDSA256r1, Some(63))]
529    // Small CurrentOnly: None
530    #[case(IndexedSigCode::Ed25519Crt, None)]
531    #[case(IndexedSigCode::ECDSA256k1Crt, None)]
532    #[case(IndexedSigCode::ECDSA256r1Crt, None)]
533    // Small Ed448 Both (os=1): 64^1 - 1 = 63
534    #[case(IndexedSigCode::Ed448, Some(63))]
535    #[case(IndexedSigCode::Ed448Crt, None)]
536    // Big Both Ed25519/ECDSA (os=2): 64^2 - 1 = 4095
537    #[case(IndexedSigCode::Ed25519Big, Some(4095))]
538    #[case(IndexedSigCode::Ed25519BigCrt, None)]
539    #[case(IndexedSigCode::ECDSA256k1Big, Some(4095))]
540    #[case(IndexedSigCode::ECDSA256k1BigCrt, None)]
541    #[case(IndexedSigCode::ECDSA256r1Big, Some(4095))]
542    #[case(IndexedSigCode::ECDSA256r1BigCrt, None)]
543    // Big Ed448 Both (os=3): 64^3 - 1 = 262_143
544    #[case(IndexedSigCode::Ed448Big, Some(262_143))]
545    #[case(IndexedSigCode::Ed448BigCrt, None)]
546    fn max_ondex_values(#[case] code: IndexedSigCode, #[case] expected: Option<u32>) {
547        assert_eq!(code.max_ondex(), expected);
548    }
549
550    // ── for_index ───────────────────────────────────────────────────────
551
552    #[rstest]
553    // Index fits in small code: no upgrade
554    #[case(IndexedSigCode::Ed25519, 0, IndexedSigCode::Ed25519)]
555    #[case(IndexedSigCode::Ed25519, 63, IndexedSigCode::Ed25519)]
556    #[case(IndexedSigCode::Ed25519Crt, 63, IndexedSigCode::Ed25519Crt)]
557    #[case(IndexedSigCode::ECDSA256k1, 0, IndexedSigCode::ECDSA256k1)]
558    #[case(IndexedSigCode::ECDSA256r1, 63, IndexedSigCode::ECDSA256r1)]
559    #[case(IndexedSigCode::Ed448, 63, IndexedSigCode::Ed448)]
560    // Index exceeds small max: auto-upgrade
561    #[case(IndexedSigCode::Ed25519, 64, IndexedSigCode::Ed25519Big)]
562    #[case(IndexedSigCode::Ed25519Crt, 64, IndexedSigCode::Ed25519BigCrt)]
563    #[case(IndexedSigCode::ECDSA256k1, 100, IndexedSigCode::ECDSA256k1Big)]
564    #[case(IndexedSigCode::ECDSA256k1Crt, 100, IndexedSigCode::ECDSA256k1BigCrt)]
565    #[case(IndexedSigCode::ECDSA256r1, 200, IndexedSigCode::ECDSA256r1Big)]
566    #[case(IndexedSigCode::ECDSA256r1Crt, 200, IndexedSigCode::ECDSA256r1BigCrt)]
567    #[case(IndexedSigCode::Ed448, 64, IndexedSigCode::Ed448Big)]
568    #[case(IndexedSigCode::Ed448Crt, 64, IndexedSigCode::Ed448BigCrt)]
569    // Already big: no change
570    #[case(IndexedSigCode::Ed25519Big, 0, IndexedSigCode::Ed25519Big)]
571    #[case(IndexedSigCode::Ed25519Big, 4095, IndexedSigCode::Ed25519Big)]
572    #[case(IndexedSigCode::Ed448Big, 262_143, IndexedSigCode::Ed448Big)]
573    fn for_index_values(
574        #[case] code: IndexedSigCode,
575        #[case] index: u32,
576        #[case] expected: IndexedSigCode,
577    ) {
578        assert_eq!(code.for_index(index), expected);
579    }
580
581    // ── hardage ─────────────────────────────────────────────────────────
582
583    #[rstest]
584    #[case('A', Some(1))]
585    #[case('Z', Some(1))]
586    #[case('a', Some(1))]
587    #[case('z', Some(1))]
588    #[case('M', Some(1))]
589    #[case('0', Some(2))]
590    #[case('1', Some(2))]
591    #[case('2', Some(2))]
592    #[case('3', Some(2))]
593    #[case('4', Some(2))]
594    #[case('5', None)]
595    #[case('9', None)]
596    #[case('-', None)]
597    #[case('_', None)]
598    #[case(' ', None)]
599    fn hardage_values(#[case] c: char, #[case] expected: Option<usize>) {
600        assert_eq!(hardage(c), expected);
601    }
602
603    // ── for_index preserves mode ────────────────────────────────────────
604
605    #[test]
606    fn for_index_preserves_mode() {
607        // Both stays Both
608        assert_eq!(
609            IndexedSigCode::Ed25519.for_index(64).mode(),
610            IndexMode::Both
611        );
612        // CurrentOnly stays CurrentOnly
613        assert_eq!(
614            IndexedSigCode::Ed25519Crt.for_index(64).mode(),
615            IndexMode::CurrentOnly,
616        );
617    }
618
619    // ── for_index preserves raw_size ────────────────────────────────────
620
621    #[test]
622    fn for_index_preserves_raw_size() {
623        assert_eq!(
624            IndexedSigCode::Ed25519.for_index(64).raw_size(),
625            IndexedSigCode::Ed25519.raw_size(),
626        );
627        assert_eq!(
628            IndexedSigCode::Ed448.for_index(64).raw_size(),
629            IndexedSigCode::Ed448.raw_size(),
630        );
631    }
632}