Skip to main content

md_codec/
tlv.rs

1//! TLV section per spec §3.7 (extended in v0.13 §3.2 with `Pubkeys` and
2//! `OriginPathOverrides`).
3
4use crate::bitstream::{BitReader, BitWriter, re_emit_bits};
5use crate::error::Error;
6use crate::origin_path::OriginPath;
7use crate::use_site_path::UseSitePath;
8use crate::varint::{read_varint, write_varint};
9
10/// TLV tag for use-site-path overrides (per-`@N` divergent path declarations).
11pub const TLV_USE_SITE_PATH_OVERRIDES: u8 = 0x00;
12/// TLV tag for per-`@N` xpub fingerprints (4 bytes each).
13pub const TLV_FINGERPRINTS: u8 = 0x01;
14/// TLV tag for per-`@N` xpub bytes (chain-code || compressed pubkey, 65 bytes
15/// each). Per v0.13 §3.2; supersedes the v0.12 reservation `TLV_XPUBS_RESERVED_V0_12`.
16pub const TLV_PUBKEYS: u8 = 0x02;
17/// TLV tag for per-`@N` origin-path overrides (BIP-32 path differing from the
18/// canonical default for the wrapper). Per v0.13 §3.2.
19pub const TLV_ORIGIN_PATH_OVERRIDES: u8 = 0x03;
20
21/// Decoded TLV section. Fields are populated from per-tag readers; unknown
22/// tags are preserved verbatim per D6 forward-compat.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct TlvSection {
25    /// Per-`@N` use-site path overrides, if present.
26    pub use_site_path_overrides: Option<Vec<(u8, UseSitePath)>>,
27    /// Per-`@N` xpub fingerprints (4 bytes each), if present.
28    pub fingerprints: Option<Vec<(u8, [u8; 4])>>,
29    /// Per-`@N` xpub bytes (32-byte chain code || 33-byte compressed pubkey),
30    /// if present. Wallet-policy mode predicate is `pubkeys.is_some() &&
31    /// !pubkeys.unwrap().is_empty()`.
32    pub pubkeys: Option<Vec<(u8, [u8; 65])>>,
33    /// Per-`@N` origin-path overrides for wrappers whose canonical path is
34    /// either undefined or has been overridden, if present.
35    pub origin_path_overrides: Option<Vec<(u8, OriginPath)>>,
36    /// Raw payload of unknown TLVs, keyed by tag, for forward-compat round-trip.
37    /// Decoders preserve unknown TLVs verbatim through re-encoding.
38    pub unknown: Vec<(u8, Vec<u8>, usize)>,
39}
40
41impl TlvSection {
42    /// Create an empty TLV section (no entries).
43    pub fn new_empty() -> Self {
44        // Exhaustive struct construction — every field listed by name. If a
45        // future field is added, this initializer fails to compile until the
46        // author decides on its empty value, preventing accidental drift.
47        Self {
48            use_site_path_overrides: None,
49            fingerprints: None,
50            pubkeys: None,
51            origin_path_overrides: None,
52            unknown: Vec::new(),
53        }
54    }
55
56    /// Returns true if no TLV entries are present.
57    pub fn is_empty(&self) -> bool {
58        // Exhaustive destructure — adding a new field forces this method to
59        // be updated (compile error on missing pattern).
60        let Self {
61            use_site_path_overrides,
62            fingerprints,
63            pubkeys,
64            origin_path_overrides,
65            unknown,
66        } = self;
67        use_site_path_overrides.is_none()
68            && fingerprints.is_none()
69            && pubkeys.is_none()
70            && origin_path_overrides.is_none()
71            && unknown.is_empty()
72    }
73
74    /// Encode this TLV section onto `w`. Entries are emitted in ascending tag order.
75    /// `key_index_width` is the bit-width of the per-`@N` placeholder index field.
76    ///
77    /// # Errors
78    ///
79    /// - [`Error::EmptyTlvEntry`] if any of `use_site_path_overrides`,
80    ///   `fingerprints`, `pubkeys`, or `origin_path_overrides` is `Some(vec![])`.
81    ///   Empty TLVs violate the §7.5 omission discipline and are rejected at the
82    ///   encoder boundary.
83    /// - [`Error::OverrideOrderViolation`] if any entry vector is not strictly
84    ///   ascending in `idx`.
85    /// - Encoding errors from contained values (`OriginPath::write`, etc.).
86    pub fn write(&self, w: &mut BitWriter, key_index_width: u8) -> Result<(), Error> {
87        // Exhaustive destructure — same drift-protection guarantee as is_empty.
88        let Self {
89            use_site_path_overrides,
90            fingerprints,
91            pubkeys,
92            origin_path_overrides,
93            unknown,
94        } = self;
95
96        // Collect entries, sort by tag.
97        let mut entries: Vec<(u8, Vec<u8>, usize)> = Vec::new();
98
99        if let Some(overrides) = use_site_path_overrides {
100            if overrides.is_empty() {
101                return Err(Error::EmptyTlvEntry {
102                    tag: TLV_USE_SITE_PATH_OVERRIDES,
103                });
104            }
105            let mut sub = BitWriter::new();
106            let mut last_idx: Option<u8> = None;
107            for (idx, path) in overrides {
108                if let Some(prev) = last_idx {
109                    if *idx <= prev {
110                        return Err(Error::OverrideOrderViolation {
111                            prev,
112                            current: *idx,
113                        });
114                    }
115                }
116                last_idx = Some(*idx);
117                sub.write_bits(u64::from(*idx), key_index_width as usize);
118                path.write(&mut sub)?;
119            }
120            let bit_len = sub.bit_len();
121            entries.push((TLV_USE_SITE_PATH_OVERRIDES, sub.into_bytes(), bit_len));
122        }
123        if let Some(fps) = fingerprints {
124            if fps.is_empty() {
125                return Err(Error::EmptyTlvEntry {
126                    tag: TLV_FINGERPRINTS,
127                });
128            }
129            let mut sub = BitWriter::new();
130            let mut last_idx: Option<u8> = None;
131            for (idx, fp) in fps {
132                if let Some(prev) = last_idx {
133                    if *idx <= prev {
134                        return Err(Error::OverrideOrderViolation {
135                            prev,
136                            current: *idx,
137                        });
138                    }
139                }
140                last_idx = Some(*idx);
141                sub.write_bits(u64::from(*idx), key_index_width as usize);
142                for b in fp {
143                    sub.write_bits(u64::from(*b), 8);
144                }
145            }
146            let bit_len = sub.bit_len();
147            entries.push((TLV_FINGERPRINTS, sub.into_bytes(), bit_len));
148        }
149        if let Some(pks) = pubkeys {
150            if pks.is_empty() {
151                return Err(Error::EmptyTlvEntry { tag: TLV_PUBKEYS });
152            }
153            let mut sub = BitWriter::new();
154            let mut last_idx: Option<u8> = None;
155            for (idx, xpub) in pks {
156                if let Some(prev) = last_idx {
157                    if *idx <= prev {
158                        return Err(Error::OverrideOrderViolation {
159                            prev,
160                            current: *idx,
161                        });
162                    }
163                }
164                last_idx = Some(*idx);
165                sub.write_bits(u64::from(*idx), key_index_width as usize);
166                for b in xpub {
167                    sub.write_bits(u64::from(*b), 8);
168                }
169            }
170            let bit_len = sub.bit_len();
171            entries.push((TLV_PUBKEYS, sub.into_bytes(), bit_len));
172        }
173        if let Some(paths) = origin_path_overrides {
174            if paths.is_empty() {
175                return Err(Error::EmptyTlvEntry {
176                    tag: TLV_ORIGIN_PATH_OVERRIDES,
177                });
178            }
179            let mut sub = BitWriter::new();
180            let mut last_idx: Option<u8> = None;
181            for (idx, path) in paths {
182                if let Some(prev) = last_idx {
183                    if *idx <= prev {
184                        return Err(Error::OverrideOrderViolation {
185                            prev,
186                            current: *idx,
187                        });
188                    }
189                }
190                last_idx = Some(*idx);
191                sub.write_bits(u64::from(*idx), key_index_width as usize);
192                path.write(&mut sub)?;
193            }
194            let bit_len = sub.bit_len();
195            entries.push((TLV_ORIGIN_PATH_OVERRIDES, sub.into_bytes(), bit_len));
196        }
197        for (tag, payload, bit_len) in unknown {
198            entries.push((*tag, payload.clone(), *bit_len));
199        }
200        entries.sort_by_key(|(t, _, _)| *t);
201
202        for (tag, payload, bit_len) in entries {
203            w.write_bits(u64::from(tag), 5);
204            write_varint(w, bit_len as u32)?;
205            re_emit_bits(w, &payload, bit_len)?;
206        }
207        Ok(())
208    }
209
210    /// Decode a TLV section from `r`, consuming all remaining bits.
211    /// `key_index_width` is the bit-width of placeholder indices; `n` is the key count.
212    pub fn read(r: &mut BitReader, key_index_width: u8, n: u8) -> Result<Self, Error> {
213        let mut section = Self::new_empty();
214        let mut last_tag: Option<u8> = None;
215        loop {
216            // Save position so we can roll back if this would-be TLV is
217            // actually trailing codex32-padding (≤7 bits of zeros).
218            let entry_start = r.save_position();
219            if r.remaining_bits() < 5 {
220                // Fewer than 5 bits remain — trailing codex32 byte-padding.
221                // F-A8: the reference encoder always zero-pads, so a non-zero
222                // trailing bit is a malformed wire. Reject rather than silently
223                // discard it (fail-closed).
224                reject_non_zero_pad(r)?;
225                break; // clean end-of-stream
226            }
227            // Try to parse a complete TLV entry. Any failure (truncated read,
228            // ordering violation, empty-entry-by-spec, length exceeds remaining)
229            // is treated as "trailing padding" if we can rollback cleanly. If
230            // rollback would consume <8 bits (consistent with codex32 padding)
231            // we accept it; otherwise the error propagates as a real malformed
232            // input.
233            let parse_result: Result<(), Error> = (|| {
234                let tag = r.read_bits(5)? as u8;
235                // Ordering check is INSIDE the closure so violations at end-of-
236                // stream (where padding bits form a phantom tag=0 after a real
237                // tag≥1 entry) become rollback-eligible.
238                if let Some(prev) = last_tag {
239                    if tag <= prev {
240                        return Err(Error::TlvOrderingViolation { prev, current: tag });
241                    }
242                }
243                let bit_len = read_varint(r)? as usize;
244                if bit_len > r.remaining_bits() {
245                    return Err(Error::TlvLengthExceedsRemaining {
246                        length: bit_len,
247                        remaining: r.remaining_bits(),
248                    });
249                }
250                // Reject zero-length TLVs uniformly. Encoder MUST omit empty
251                // TLVs per spec §7.5; a zero-length entry at the end of stream
252                // is treated as padding via the rollback path.
253                if bit_len == 0 {
254                    return Err(Error::EmptyTlvEntry { tag });
255                }
256                match tag {
257                    TLV_USE_SITE_PATH_OVERRIDES => {
258                        let entry = read_use_site_overrides(r, bit_len, key_index_width, n)?;
259                        section.use_site_path_overrides = Some(entry);
260                    }
261                    TLV_FINGERPRINTS => {
262                        let entry = read_fingerprints(r, bit_len, key_index_width, n)?;
263                        section.fingerprints = Some(entry);
264                    }
265                    TLV_PUBKEYS => {
266                        let entry = read_pubkeys(r, bit_len, key_index_width, n)?;
267                        section.pubkeys = Some(entry);
268                    }
269                    TLV_ORIGIN_PATH_OVERRIDES => {
270                        let entry = read_origin_path_overrides(r, bit_len, key_index_width, n)?;
271                        section.origin_path_overrides = Some(entry);
272                    }
273                    _ => {
274                        // Unknown — buffer and skip per D6 forward-compat.
275                        let mut sub = BitWriter::new();
276                        let mut remaining = bit_len;
277                        while remaining > 0 {
278                            let chunk = remaining.min(8);
279                            let bits = r.read_bits(chunk)?;
280                            sub.write_bits(bits, chunk);
281                            remaining -= chunk;
282                        }
283                        let payload = sub.into_bytes();
284                        section.unknown.push((tag, payload, bit_len));
285                    }
286                }
287                last_tag = Some(tag);
288                Ok(())
289            })();
290
291            match parse_result {
292                Ok(()) => continue,
293                Err(e) => {
294                    // Decide: rollback-as-padding or propagate error.
295                    // Rollback is acceptable iff the bits we'd be discarding
296                    // are ≤7 (consistent with codex32 padding boundary).
297                    r.restore_position(entry_start);
298                    let remaining_at_entry_start = r.remaining_bits();
299                    // Padding tolerance: ≤7 bits of trailing zeros after the
300                    // last real TLV (or after the tree if no TLVs were emitted).
301                    if remaining_at_entry_start <= 7 {
302                        // F-A8: the trailing pad MUST be all-zero (the encoder
303                        // always zero-pads); a non-zero pad is malformed.
304                        reject_non_zero_pad(r)?;
305                        break;
306                    }
307                    // More than 7 bits remained but the parse still failed —
308                    // this is genuinely malformed input. Propagate.
309                    return Err(e);
310                }
311            }
312        }
313        Ok(section)
314    }
315}
316
317/// F-A8: verify the remaining ≤7 trailing bits of `r` are all zero, WITHOUT
318/// advancing the cursor (peek-and-restore). The reference encoder always
319/// zero-pads to the next byte boundary (BitWriter + `wrap_payload`), so any
320/// non-zero trailing bit is a malformed / hand-forged wire. Called at both
321/// TLV-loop break points (clean end-of-stream and parse-failure rollback)
322/// before accepting the tail as padding. Cursor-neutral so it does not perturb
323/// the `bit_position == bit_len` post-read invariant callers rely on.
324fn reject_non_zero_pad(r: &mut BitReader) -> Result<(), Error> {
325    let bits = r.remaining_bits();
326    if bits == 0 {
327        return Ok(());
328    }
329    debug_assert!(bits <= 7, "pad-tolerance band is ≤7 bits");
330    let saved = r.save_position();
331    let pad = r.read_bits(bits)?;
332    r.restore_position(saved);
333    if pad != 0 {
334        return Err(Error::MalformedPayloadPadding { bits });
335    }
336    Ok(())
337}
338
339/// Read one sparse `(idx, ...)` index header field: a `key_index_width`-bit
340/// `idx`, range-checked against `n`, and (if `last_idx.is_some()`) verified
341/// to be strictly greater than the previous idx. Returns the raw idx for
342/// the caller to thread into `last_idx` on the next call.
343///
344/// Used by every sparse-TLV reader (use-site-path overrides, fingerprints,
345/// pubkeys, origin-path overrides) so the range/ordering invariants are
346/// enforced uniformly in one place.
347fn read_sparse_tlv_idx(
348    r: &mut BitReader,
349    key_index_width: u8,
350    n: u8,
351    last_idx: Option<u8>,
352) -> Result<u8, Error> {
353    let idx = r.read_bits(key_index_width as usize)? as u8;
354    if idx >= n {
355        return Err(Error::PlaceholderIndexOutOfRange { idx, n });
356    }
357    if let Some(prev) = last_idx {
358        if idx <= prev {
359            return Err(Error::OverrideOrderViolation { prev, current: idx });
360        }
361    }
362    Ok(idx)
363}
364
365/// Generic sparse-TLV body reader.
366///
367/// **Per spec v0.13 §3.2 + audit follow-up L3 (v0.13.1):** bounds the
368/// `BitReader`'s `bit_limit` to `start + bit_len` for the duration of
369/// the body loop. This prevents a malformed wire from silently
370/// advancing the outer reader's cursor past the declared body boundary
371/// — any over-read errors with `BitStreamTruncated` instead of
372/// quietly consuming bits from the next TLV. On error, the inner
373/// error variant is propagated as-is (no translation) since the same
374/// failure mode is meaningful regardless of whether the offending bits
375/// were intended as a real record or as trailing slack.
376///
377/// On success: empty-entries-vec → [`Error::EmptyTlvEntry`].
378fn read_sparse_tlv_body<T, F>(
379    r: &mut BitReader,
380    bit_len: usize,
381    tag: u8,
382    key_index_width: u8,
383    n: u8,
384    mut read_value: F,
385) -> Result<Vec<(u8, T)>, Error>
386where
387    F: FnMut(&mut BitReader) -> Result<T, Error>,
388{
389    let start = r.bit_position();
390    let saved_limit = r.save_bit_limit();
391    r.set_bit_limit_for_scope(start + bit_len);
392
393    let mut entries: Vec<(u8, T)> = Vec::new();
394    let mut last_idx: Option<u8> = None;
395
396    let result = (|| -> Result<(), Error> {
397        while r.bit_position() - start < bit_len {
398            let idx = read_sparse_tlv_idx(r, key_index_width, n, last_idx)?;
399            let value = read_value(r)?;
400            last_idx = Some(idx);
401            entries.push((idx, value));
402        }
403        Ok(())
404    })();
405
406    r.restore_bit_limit(saved_limit);
407    result?;
408
409    if entries.is_empty() {
410        return Err(Error::EmptyTlvEntry { tag });
411    }
412    Ok(entries)
413}
414
415fn read_use_site_overrides(
416    r: &mut BitReader,
417    bit_len: usize,
418    key_index_width: u8,
419    n: u8,
420) -> Result<Vec<(u8, UseSitePath)>, Error> {
421    read_sparse_tlv_body(
422        r,
423        bit_len,
424        TLV_USE_SITE_PATH_OVERRIDES,
425        key_index_width,
426        n,
427        UseSitePath::read,
428    )
429}
430
431fn read_fingerprints(
432    r: &mut BitReader,
433    bit_len: usize,
434    key_index_width: u8,
435    n: u8,
436) -> Result<Vec<(u8, [u8; 4])>, Error> {
437    read_sparse_tlv_body(r, bit_len, TLV_FINGERPRINTS, key_index_width, n, |r| {
438        let mut fp = [0u8; 4];
439        for byte in &mut fp {
440            *byte = r.read_bits(8)? as u8;
441        }
442        Ok(fp)
443    })
444}
445
446fn read_pubkeys(
447    r: &mut BitReader,
448    bit_len: usize,
449    key_index_width: u8,
450    n: u8,
451) -> Result<Vec<(u8, [u8; 65])>, Error> {
452    read_sparse_tlv_body(r, bit_len, TLV_PUBKEYS, key_index_width, n, |r| {
453        let mut xpub = [0u8; 65];
454        for byte in &mut xpub {
455            *byte = r.read_bits(8)? as u8;
456        }
457        Ok(xpub)
458    })
459}
460
461fn read_origin_path_overrides(
462    r: &mut BitReader,
463    bit_len: usize,
464    key_index_width: u8,
465    n: u8,
466) -> Result<Vec<(u8, OriginPath)>, Error> {
467    // OriginPath::read is self-delimiting (depth field + that-many
468    // components) — it terminates without needing an outer length cue.
469    read_sparse_tlv_body(
470        r,
471        bit_len,
472        TLV_ORIGIN_PATH_OVERRIDES,
473        key_index_width,
474        n,
475        OriginPath::read,
476    )
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::origin_path::PathComponent;
483
484    #[test]
485    fn empty_tlv_section_round_trip() {
486        let s = TlvSection::new_empty();
487        assert!(s.is_empty());
488        let mut w = BitWriter::new();
489        s.write(&mut w, 2).unwrap();
490        assert_eq!(w.bit_len(), 0);
491    }
492
493    #[test]
494    fn use_site_path_override_round_trip() {
495        let mut s = TlvSection::new_empty();
496        s.use_site_path_overrides = Some(vec![(
497            1u8,
498            UseSitePath {
499                multipath: None,
500                wildcard_hardened: true,
501            },
502        )]);
503        let mut w = BitWriter::new();
504        s.write(&mut w, 2).unwrap();
505        let bit_len = w.bit_len();
506        let bytes = w.into_bytes();
507        let mut r = BitReader::new(&bytes);
508        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
509        assert_eq!(s2, s);
510        assert_eq!(r.bit_position(), bit_len);
511    }
512
513    #[test]
514    fn fingerprint_round_trip() {
515        let mut s = TlvSection::new_empty();
516        s.fingerprints = Some(vec![
517            (0u8, [0xaa, 0xbb, 0xcc, 0xdd]),
518            (2u8, [0x11, 0x22, 0x33, 0x44]),
519        ]);
520        let mut w = BitWriter::new();
521        s.write(&mut w, 2).unwrap();
522        let bytes = w.into_bytes();
523        let mut r = BitReader::new(&bytes);
524        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
525        assert_eq!(s2, s);
526    }
527
528    #[test]
529    fn pubkeys_round_trip() {
530        // Build two distinguishable 65-byte payloads.
531        let mut xpub_a = [0u8; 65];
532        for (i, b) in xpub_a.iter_mut().enumerate() {
533            *b = i as u8;
534        }
535        let mut xpub_b = [0u8; 65];
536        for (i, b) in xpub_b.iter_mut().enumerate() {
537            *b = (0xff - i as u8) ^ 0x5a;
538        }
539        let mut s = TlvSection::new_empty();
540        s.pubkeys = Some(vec![(0u8, xpub_a), (2u8, xpub_b)]);
541
542        let mut w = BitWriter::new();
543        s.write(&mut w, 2).unwrap();
544        let bit_len = w.bit_len();
545        let bytes = w.into_bytes();
546        let mut r = BitReader::new(&bytes);
547        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
548        assert_eq!(s2, s);
549        assert_eq!(r.bit_position(), bit_len);
550    }
551
552    #[test]
553    fn origin_path_overrides_round_trip() {
554        // Two distinct origin paths at idx 0 and idx 1.
555        let bip84 = OriginPath {
556            components: vec![
557                PathComponent {
558                    hardened: true,
559                    value: 84,
560                },
561                PathComponent {
562                    hardened: true,
563                    value: 0,
564                },
565                PathComponent {
566                    hardened: true,
567                    value: 5,
568                },
569            ],
570        };
571        let bip48 = OriginPath {
572            components: vec![
573                PathComponent {
574                    hardened: true,
575                    value: 48,
576                },
577                PathComponent {
578                    hardened: true,
579                    value: 0,
580                },
581                PathComponent {
582                    hardened: true,
583                    value: 0,
584                },
585                PathComponent {
586                    hardened: true,
587                    value: 2,
588                },
589            ],
590        };
591        let mut s = TlvSection::new_empty();
592        s.origin_path_overrides = Some(vec![(0u8, bip84), (1u8, bip48)]);
593
594        let mut w = BitWriter::new();
595        s.write(&mut w, 2).unwrap();
596        let bit_len = w.bit_len();
597        let bytes = w.into_bytes();
598        let mut r = BitReader::new(&bytes);
599        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
600        assert_eq!(s2, s);
601        assert_eq!(r.bit_position(), bit_len);
602    }
603
604    #[test]
605    fn ascending_tag_order_enforced_in_encoder() {
606        // All four sparse TLVs populated; first-on-the-wire must be tag 0x00.
607        let mut s = TlvSection::new_empty();
608        s.use_site_path_overrides = Some(vec![(
609            0,
610            UseSitePath {
611                multipath: None,
612                wildcard_hardened: false,
613            },
614        )]);
615        s.fingerprints = Some(vec![(0, [0u8; 4])]);
616        s.pubkeys = Some(vec![(0, [0u8; 65])]);
617        s.origin_path_overrides = Some(vec![(
618            0,
619            OriginPath {
620                components: vec![PathComponent {
621                    hardened: true,
622                    value: 84,
623                }],
624            },
625        )]);
626        let mut w = BitWriter::new();
627        s.write(&mut w, 2).unwrap();
628        let bytes = w.into_bytes();
629        let first_tag = (bytes[0] >> 3) & 0x1F;
630        assert_eq!(first_tag, TLV_USE_SITE_PATH_OVERRIDES);
631    }
632
633    #[test]
634    fn pubkeys_ordering_violation_rejected_at_encoder() {
635        // Non-ascending idx pair (1, 0) → encoder must reject.
636        let mut s = TlvSection::new_empty();
637        s.pubkeys = Some(vec![(1u8, [0u8; 65]), (0u8, [0u8; 65])]);
638        let mut w = BitWriter::new();
639        let result = s.write(&mut w, 2);
640        assert!(matches!(
641            result,
642            Err(Error::OverrideOrderViolation {
643                prev: 1,
644                current: 0
645            })
646        ));
647    }
648
649    #[test]
650    fn pubkeys_ordering_violation_rejected_at_decoder() {
651        // Encode (0, [0;65]) then a deliberately mis-ordered (0, [0;65]) by
652        // hand-building the bytes — exercises the read_sparse_tlv_idx
653        // helper's ascending check on the read side.
654        let mut sub = BitWriter::new();
655        // idx=1 (2-bit width) then 65 zero bytes.
656        sub.write_bits(1, 2);
657        for _ in 0..65 {
658            sub.write_bits(0, 8);
659        }
660        // idx=1 again → ordering violation (1 not > 1).
661        sub.write_bits(1, 2);
662        for _ in 0..65 {
663            sub.write_bits(0, 8);
664        }
665        let bit_len = sub.bit_len();
666        let payload_bytes = sub.into_bytes();
667
668        let mut w = BitWriter::new();
669        w.write_bits(u64::from(TLV_PUBKEYS), 5);
670        write_varint(&mut w, bit_len as u32).unwrap();
671        re_emit_bits(&mut w, &payload_bytes, bit_len).unwrap();
672        let total_bit_len = w.bit_len();
673        let bytes = w.into_bytes();
674
675        let mut r = BitReader::with_bit_limit(&bytes, total_bit_len);
676        let result = TlvSection::read(&mut r, 2, 3);
677        assert!(matches!(
678            result,
679            Err(Error::OverrideOrderViolation {
680                prev: 1,
681                current: 1
682            })
683        ));
684    }
685
686    #[test]
687    fn read_sparse_tlv_idx_out_of_range() {
688        // Build 2-bit idx=3 with n=2 → out of range.
689        let mut sub = BitWriter::new();
690        sub.write_bits(3, 2);
691        let bit_len = sub.bit_len();
692        let bytes = sub.into_bytes();
693        let mut r = BitReader::with_bit_limit(&bytes, bit_len);
694
695        let result = read_sparse_tlv_idx(&mut r, 2, 2, None);
696        assert!(matches!(
697            result,
698            Err(Error::PlaceholderIndexOutOfRange { idx: 3, n: 2 })
699        ));
700    }
701
702    #[test]
703    fn read_sparse_tlv_idx_non_ascending() {
704        let mut sub = BitWriter::new();
705        sub.write_bits(0, 2);
706        let bit_len = sub.bit_len();
707        let bytes = sub.into_bytes();
708        let mut r = BitReader::with_bit_limit(&bytes, bit_len);
709
710        let result = read_sparse_tlv_idx(&mut r, 2, 3, Some(1));
711        assert!(matches!(
712            result,
713            Err(Error::OverrideOrderViolation {
714                prev: 1,
715                current: 0
716            })
717        ));
718    }
719
720    #[test]
721    fn empty_pubkeys_vec_rejected_at_encoder() {
722        let mut s = TlvSection::new_empty();
723        s.pubkeys = Some(Vec::new());
724        let mut w = BitWriter::new();
725        let result = s.write(&mut w, 2);
726        assert!(matches!(
727            result,
728            Err(Error::EmptyTlvEntry { tag }) if tag == TLV_PUBKEYS
729        ));
730    }
731
732    #[test]
733    fn empty_origin_path_overrides_vec_rejected_at_encoder() {
734        let mut s = TlvSection::new_empty();
735        s.origin_path_overrides = Some(Vec::new());
736        let mut w = BitWriter::new();
737        let result = s.write(&mut w, 2);
738        assert!(matches!(
739            result,
740            Err(Error::EmptyTlvEntry { tag }) if tag == TLV_ORIGIN_PATH_OVERRIDES
741        ));
742    }
743
744    // ─── Strict bit_len enforcement (v0.13.1, audit L3) ───────────────
745
746    /// Hand-craft a single-TLV wire with one inflated `bit_len`. Returns
747    /// the bytes and the total bit count for `BitReader::with_bit_limit`.
748    fn craft_inflated_tlv_wire(
749        tag: u8,
750        idx: u8,
751        idx_width: u8,
752        record_payload_bits: &[(u64, usize)],
753        slack_bits: usize,
754    ) -> (Vec<u8>, usize) {
755        let mut w = BitWriter::new();
756        // Tag (5 bits).
757        w.write_bits(u64::from(tag), 5);
758        // bit_len (LP4-ext varint) — declares the actual records' bits + slack.
759        let actual_record_bits: usize =
760            (idx_width as usize) + record_payload_bits.iter().map(|(_, n)| n).sum::<usize>();
761        let declared_bit_len = actual_record_bits + slack_bits;
762        write_varint(&mut w, declared_bit_len as u32).unwrap();
763        // Records: idx + payload.
764        w.write_bits(u64::from(idx), idx_width as usize);
765        for (val, bits) in record_payload_bits {
766            w.write_bits(*val, *bits);
767        }
768        // Append slack zero-bits.
769        for _ in 0..slack_bits {
770            w.write_bits(0, 1);
771        }
772        let bit_len = w.bit_len();
773        (w.into_bytes(), bit_len)
774    }
775
776    // The four tests below exercise the L3 audit concern: a wire that
777    // declares more `bit_len` than its records actually carry must be
778    // rejected, with no silent advancement of the outer reader's
779    // cursor past the declared body. The specific error variant depends
780    // on the slack-bit pattern (typically `OverrideOrderViolation` when
781    // slack starts with zero and the previous idx was 0, or
782    // `BitStreamTruncated` when slack is too short for a phantom idx).
783    // The contract under test is "rejection happens," not the variant
784    // name. The `bit_limit` bound inside `read_sparse_tlv_body` is the
785    // load-bearing fix.
786
787    #[test]
788    fn fingerprints_with_trailing_slack_rejected() {
789        let (bytes, total_bits) =
790            craft_inflated_tlv_wire(TLV_FINGERPRINTS, 0, 1, &[(0xDEAD_BEEF, 32)], 4);
791        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
792        let result = TlvSection::read(&mut r, 1, 1);
793        assert!(
794            result.is_err(),
795            "trailing slack must be rejected, got {:?}",
796            result
797        );
798    }
799
800    #[test]
801    fn pubkeys_with_trailing_slack_rejected() {
802        let payload: Vec<(u64, usize)> = (0..65).map(|_i| (0x42u64, 8)).collect();
803        let (bytes, total_bits) = craft_inflated_tlv_wire(TLV_PUBKEYS, 0, 1, &payload, 3);
804        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
805        let result = TlvSection::read(&mut r, 1, 1);
806        assert!(
807            result.is_err(),
808            "trailing slack must be rejected, got {:?}",
809            result
810        );
811    }
812
813    #[test]
814    fn use_site_path_overrides_with_trailing_slack_rejected() {
815        let mut path_w = BitWriter::new();
816        UseSitePath::standard_multipath()
817            .write(&mut path_w)
818            .unwrap();
819        let path_bit_len = path_w.bit_len();
820        let path_bytes = path_w.into_bytes();
821        let mut path_record: Vec<(u64, usize)> = Vec::new();
822        let mut br = BitReader::new(&path_bytes);
823        let mut consumed = 0;
824        while consumed < path_bit_len {
825            let chunk = (path_bit_len - consumed).min(8);
826            path_record.push((br.read_bits(chunk).unwrap(), chunk));
827            consumed += chunk;
828        }
829        let (bytes, total_bits) =
830            craft_inflated_tlv_wire(TLV_USE_SITE_PATH_OVERRIDES, 0, 1, &path_record, 2);
831        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
832        let result = TlvSection::read(&mut r, 1, 1);
833        assert!(
834            result.is_err(),
835            "trailing slack must be rejected, got {:?}",
836            result
837        );
838    }
839
840    #[test]
841    fn origin_path_overrides_with_trailing_slack_rejected() {
842        let (bytes, total_bits) =
843            craft_inflated_tlv_wire(TLV_ORIGIN_PATH_OVERRIDES, 0, 1, &[(0, 4)], 5);
844        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
845        let result = TlvSection::read(&mut r, 1, 1);
846        assert!(
847            result.is_err(),
848            "trailing slack must be rejected, got {:?}",
849            result
850        );
851    }
852
853    // ─── F-A8: non-zero trailing-pad rejection ───────────────────────────
854
855    /// A TLV section with fewer than 5 trailing bits that are NOT all zero is
856    /// a malformed pad and must be rejected (the reference encoder always
857    /// zero-pads). Covers the "clean end-of-stream" break point (< 5 bits).
858    #[test]
859    fn non_zero_trailing_pad_under_5_bits_rejected() {
860        let mut w = BitWriter::new();
861        w.write_bits(0b101, 3); // 3 non-zero pad bits, no real TLV
862        let total = w.bit_len();
863        let bytes = w.into_bytes();
864        let mut r = BitReader::with_bit_limit(&bytes, total);
865        let result = TlvSection::read(&mut r, 2, 3);
866        assert!(
867            matches!(result, Err(Error::MalformedPayloadPadding { bits: 3 })),
868            "expected MalformedPayloadPadding {{ bits: 3 }}, got {result:?}"
869        );
870    }
871
872    /// A non-zero pad in the 5..=7-bit band (the parse-failure rollback break
873    /// point) is likewise rejected. 6 bits = `0b100000`: a phantom tag=16
874    /// (unknown) whose varint read truncates → rollback with 6 bits remaining.
875    #[test]
876    fn non_zero_trailing_pad_5_to_7_bits_rejected() {
877        let mut w = BitWriter::new();
878        w.write_bits(0b100000, 6);
879        let total = w.bit_len();
880        let bytes = w.into_bytes();
881        let mut r = BitReader::with_bit_limit(&bytes, total);
882        let result = TlvSection::read(&mut r, 2, 3);
883        assert!(
884            matches!(result, Err(Error::MalformedPayloadPadding { bits: 6 })),
885            "expected MalformedPayloadPadding {{ bits: 6 }}, got {result:?}"
886        );
887    }
888
889    /// All-zero trailing pad (any width ≤7) is accepted — the reference
890    /// encoder's normal output.
891    #[test]
892    fn zero_trailing_pad_accepted() {
893        for pad in 1..=7usize {
894            let mut w = BitWriter::new();
895            w.write_bits(0, pad);
896            let total = w.bit_len();
897            let bytes = w.into_bytes();
898            let mut r = BitReader::with_bit_limit(&bytes, total);
899            let result = TlvSection::read(&mut r, 2, 3);
900            assert!(
901                matches!(&result, Ok(s) if s.is_empty()),
902                "{pad}-bit zero pad must be accepted, got {result:?}"
903            );
904        }
905    }
906}