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                break; // not enough bits for even a tag — clean end-of-stream
221            }
222            // Try to parse a complete TLV entry. Any failure (truncated read,
223            // ordering violation, empty-entry-by-spec, length exceeds remaining)
224            // is treated as "trailing padding" if we can rollback cleanly. If
225            // rollback would consume <8 bits (consistent with codex32 padding)
226            // we accept it; otherwise the error propagates as a real malformed
227            // input.
228            let parse_result: Result<(), Error> = (|| {
229                let tag = r.read_bits(5)? as u8;
230                // Ordering check is INSIDE the closure so violations at end-of-
231                // stream (where padding bits form a phantom tag=0 after a real
232                // tag≥1 entry) become rollback-eligible.
233                if let Some(prev) = last_tag {
234                    if tag <= prev {
235                        return Err(Error::TlvOrderingViolation { prev, current: tag });
236                    }
237                }
238                let bit_len = read_varint(r)? as usize;
239                if bit_len > r.remaining_bits() {
240                    return Err(Error::TlvLengthExceedsRemaining {
241                        length: bit_len,
242                        remaining: r.remaining_bits(),
243                    });
244                }
245                // Reject zero-length TLVs uniformly. Encoder MUST omit empty
246                // TLVs per spec §7.5; a zero-length entry at the end of stream
247                // is treated as padding via the rollback path.
248                if bit_len == 0 {
249                    return Err(Error::EmptyTlvEntry { tag });
250                }
251                match tag {
252                    TLV_USE_SITE_PATH_OVERRIDES => {
253                        let entry = read_use_site_overrides(r, bit_len, key_index_width, n)?;
254                        section.use_site_path_overrides = Some(entry);
255                    }
256                    TLV_FINGERPRINTS => {
257                        let entry = read_fingerprints(r, bit_len, key_index_width, n)?;
258                        section.fingerprints = Some(entry);
259                    }
260                    TLV_PUBKEYS => {
261                        let entry = read_pubkeys(r, bit_len, key_index_width, n)?;
262                        section.pubkeys = Some(entry);
263                    }
264                    TLV_ORIGIN_PATH_OVERRIDES => {
265                        let entry = read_origin_path_overrides(r, bit_len, key_index_width, n)?;
266                        section.origin_path_overrides = Some(entry);
267                    }
268                    _ => {
269                        // Unknown — buffer and skip per D6 forward-compat.
270                        let mut sub = BitWriter::new();
271                        let mut remaining = bit_len;
272                        while remaining > 0 {
273                            let chunk = remaining.min(8);
274                            let bits = r.read_bits(chunk)?;
275                            sub.write_bits(bits, chunk);
276                            remaining -= chunk;
277                        }
278                        let payload = sub.into_bytes();
279                        section.unknown.push((tag, payload, bit_len));
280                    }
281                }
282                last_tag = Some(tag);
283                Ok(())
284            })();
285
286            match parse_result {
287                Ok(()) => continue,
288                Err(e) => {
289                    // Decide: rollback-as-padding or propagate error.
290                    // Rollback is acceptable iff the bits we'd be discarding
291                    // are ≤7 (consistent with codex32 padding boundary).
292                    r.restore_position(entry_start);
293                    let remaining_at_entry_start = r.remaining_bits();
294                    // Padding tolerance: ≤7 bits of trailing zeros after the
295                    // last real TLV (or after the tree if no TLVs were emitted).
296                    if remaining_at_entry_start <= 7 {
297                        break;
298                    }
299                    // More than 7 bits remained but the parse still failed —
300                    // this is genuinely malformed input. Propagate.
301                    return Err(e);
302                }
303            }
304        }
305        Ok(section)
306    }
307}
308
309/// Read one sparse `(idx, ...)` index header field: a `key_index_width`-bit
310/// `idx`, range-checked against `n`, and (if `last_idx.is_some()`) verified
311/// to be strictly greater than the previous idx. Returns the raw idx for
312/// the caller to thread into `last_idx` on the next call.
313///
314/// Used by every sparse-TLV reader (use-site-path overrides, fingerprints,
315/// pubkeys, origin-path overrides) so the range/ordering invariants are
316/// enforced uniformly in one place.
317fn read_sparse_tlv_idx(
318    r: &mut BitReader,
319    key_index_width: u8,
320    n: u8,
321    last_idx: Option<u8>,
322) -> Result<u8, Error> {
323    let idx = r.read_bits(key_index_width as usize)? as u8;
324    if idx >= n {
325        return Err(Error::PlaceholderIndexOutOfRange { idx, n });
326    }
327    if let Some(prev) = last_idx {
328        if idx <= prev {
329            return Err(Error::OverrideOrderViolation { prev, current: idx });
330        }
331    }
332    Ok(idx)
333}
334
335/// Generic sparse-TLV body reader.
336///
337/// **Per spec v0.13 §3.2 + audit follow-up L3 (v0.13.1):** bounds the
338/// `BitReader`'s `bit_limit` to `start + bit_len` for the duration of
339/// the body loop. This prevents a malformed wire from silently
340/// advancing the outer reader's cursor past the declared body boundary
341/// — any over-read errors with `BitStreamTruncated` instead of
342/// quietly consuming bits from the next TLV. On error, the inner
343/// error variant is propagated as-is (no translation) since the same
344/// failure mode is meaningful regardless of whether the offending bits
345/// were intended as a real record or as trailing slack.
346///
347/// On success: empty-entries-vec → [`Error::EmptyTlvEntry`].
348fn read_sparse_tlv_body<T, F>(
349    r: &mut BitReader,
350    bit_len: usize,
351    tag: u8,
352    key_index_width: u8,
353    n: u8,
354    mut read_value: F,
355) -> Result<Vec<(u8, T)>, Error>
356where
357    F: FnMut(&mut BitReader) -> Result<T, Error>,
358{
359    let start = r.bit_position();
360    let saved_limit = r.save_bit_limit();
361    r.set_bit_limit_for_scope(start + bit_len);
362
363    let mut entries: Vec<(u8, T)> = Vec::new();
364    let mut last_idx: Option<u8> = None;
365
366    let result = (|| -> Result<(), Error> {
367        while r.bit_position() - start < bit_len {
368            let idx = read_sparse_tlv_idx(r, key_index_width, n, last_idx)?;
369            let value = read_value(r)?;
370            last_idx = Some(idx);
371            entries.push((idx, value));
372        }
373        Ok(())
374    })();
375
376    r.restore_bit_limit(saved_limit);
377    result?;
378
379    if entries.is_empty() {
380        return Err(Error::EmptyTlvEntry { tag });
381    }
382    Ok(entries)
383}
384
385fn read_use_site_overrides(
386    r: &mut BitReader,
387    bit_len: usize,
388    key_index_width: u8,
389    n: u8,
390) -> Result<Vec<(u8, UseSitePath)>, Error> {
391    read_sparse_tlv_body(
392        r,
393        bit_len,
394        TLV_USE_SITE_PATH_OVERRIDES,
395        key_index_width,
396        n,
397        UseSitePath::read,
398    )
399}
400
401fn read_fingerprints(
402    r: &mut BitReader,
403    bit_len: usize,
404    key_index_width: u8,
405    n: u8,
406) -> Result<Vec<(u8, [u8; 4])>, Error> {
407    read_sparse_tlv_body(r, bit_len, TLV_FINGERPRINTS, key_index_width, n, |r| {
408        let mut fp = [0u8; 4];
409        for byte in &mut fp {
410            *byte = r.read_bits(8)? as u8;
411        }
412        Ok(fp)
413    })
414}
415
416fn read_pubkeys(
417    r: &mut BitReader,
418    bit_len: usize,
419    key_index_width: u8,
420    n: u8,
421) -> Result<Vec<(u8, [u8; 65])>, Error> {
422    read_sparse_tlv_body(r, bit_len, TLV_PUBKEYS, key_index_width, n, |r| {
423        let mut xpub = [0u8; 65];
424        for byte in &mut xpub {
425            *byte = r.read_bits(8)? as u8;
426        }
427        Ok(xpub)
428    })
429}
430
431fn read_origin_path_overrides(
432    r: &mut BitReader,
433    bit_len: usize,
434    key_index_width: u8,
435    n: u8,
436) -> Result<Vec<(u8, OriginPath)>, Error> {
437    // OriginPath::read is self-delimiting (depth field + that-many
438    // components) — it terminates without needing an outer length cue.
439    read_sparse_tlv_body(
440        r,
441        bit_len,
442        TLV_ORIGIN_PATH_OVERRIDES,
443        key_index_width,
444        n,
445        OriginPath::read,
446    )
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::origin_path::PathComponent;
453
454    #[test]
455    fn empty_tlv_section_round_trip() {
456        let s = TlvSection::new_empty();
457        assert!(s.is_empty());
458        let mut w = BitWriter::new();
459        s.write(&mut w, 2).unwrap();
460        assert_eq!(w.bit_len(), 0);
461    }
462
463    #[test]
464    fn use_site_path_override_round_trip() {
465        let mut s = TlvSection::new_empty();
466        s.use_site_path_overrides = Some(vec![(
467            1u8,
468            UseSitePath {
469                multipath: None,
470                wildcard_hardened: true,
471            },
472        )]);
473        let mut w = BitWriter::new();
474        s.write(&mut w, 2).unwrap();
475        let bit_len = w.bit_len();
476        let bytes = w.into_bytes();
477        let mut r = BitReader::new(&bytes);
478        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
479        assert_eq!(s2, s);
480        assert_eq!(r.bit_position(), bit_len);
481    }
482
483    #[test]
484    fn fingerprint_round_trip() {
485        let mut s = TlvSection::new_empty();
486        s.fingerprints = Some(vec![
487            (0u8, [0xaa, 0xbb, 0xcc, 0xdd]),
488            (2u8, [0x11, 0x22, 0x33, 0x44]),
489        ]);
490        let mut w = BitWriter::new();
491        s.write(&mut w, 2).unwrap();
492        let bytes = w.into_bytes();
493        let mut r = BitReader::new(&bytes);
494        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
495        assert_eq!(s2, s);
496    }
497
498    #[test]
499    fn pubkeys_round_trip() {
500        // Build two distinguishable 65-byte payloads.
501        let mut xpub_a = [0u8; 65];
502        for (i, b) in xpub_a.iter_mut().enumerate() {
503            *b = i as u8;
504        }
505        let mut xpub_b = [0u8; 65];
506        for (i, b) in xpub_b.iter_mut().enumerate() {
507            *b = (0xff - i as u8) ^ 0x5a;
508        }
509        let mut s = TlvSection::new_empty();
510        s.pubkeys = Some(vec![(0u8, xpub_a), (2u8, xpub_b)]);
511
512        let mut w = BitWriter::new();
513        s.write(&mut w, 2).unwrap();
514        let bit_len = w.bit_len();
515        let bytes = w.into_bytes();
516        let mut r = BitReader::new(&bytes);
517        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
518        assert_eq!(s2, s);
519        assert_eq!(r.bit_position(), bit_len);
520    }
521
522    #[test]
523    fn origin_path_overrides_round_trip() {
524        // Two distinct origin paths at idx 0 and idx 1.
525        let bip84 = OriginPath {
526            components: vec![
527                PathComponent {
528                    hardened: true,
529                    value: 84,
530                },
531                PathComponent {
532                    hardened: true,
533                    value: 0,
534                },
535                PathComponent {
536                    hardened: true,
537                    value: 5,
538                },
539            ],
540        };
541        let bip48 = OriginPath {
542            components: vec![
543                PathComponent {
544                    hardened: true,
545                    value: 48,
546                },
547                PathComponent {
548                    hardened: true,
549                    value: 0,
550                },
551                PathComponent {
552                    hardened: true,
553                    value: 0,
554                },
555                PathComponent {
556                    hardened: true,
557                    value: 2,
558                },
559            ],
560        };
561        let mut s = TlvSection::new_empty();
562        s.origin_path_overrides = Some(vec![(0u8, bip84), (1u8, bip48)]);
563
564        let mut w = BitWriter::new();
565        s.write(&mut w, 2).unwrap();
566        let bit_len = w.bit_len();
567        let bytes = w.into_bytes();
568        let mut r = BitReader::new(&bytes);
569        let s2 = TlvSection::read(&mut r, 2, 3).unwrap();
570        assert_eq!(s2, s);
571        assert_eq!(r.bit_position(), bit_len);
572    }
573
574    #[test]
575    fn ascending_tag_order_enforced_in_encoder() {
576        // All four sparse TLVs populated; first-on-the-wire must be tag 0x00.
577        let mut s = TlvSection::new_empty();
578        s.use_site_path_overrides = Some(vec![(
579            0,
580            UseSitePath {
581                multipath: None,
582                wildcard_hardened: false,
583            },
584        )]);
585        s.fingerprints = Some(vec![(0, [0u8; 4])]);
586        s.pubkeys = Some(vec![(0, [0u8; 65])]);
587        s.origin_path_overrides = Some(vec![(
588            0,
589            OriginPath {
590                components: vec![PathComponent {
591                    hardened: true,
592                    value: 84,
593                }],
594            },
595        )]);
596        let mut w = BitWriter::new();
597        s.write(&mut w, 2).unwrap();
598        let bytes = w.into_bytes();
599        let first_tag = (bytes[0] >> 3) & 0x1F;
600        assert_eq!(first_tag, TLV_USE_SITE_PATH_OVERRIDES);
601    }
602
603    #[test]
604    fn pubkeys_ordering_violation_rejected_at_encoder() {
605        // Non-ascending idx pair (1, 0) → encoder must reject.
606        let mut s = TlvSection::new_empty();
607        s.pubkeys = Some(vec![(1u8, [0u8; 65]), (0u8, [0u8; 65])]);
608        let mut w = BitWriter::new();
609        let result = s.write(&mut w, 2);
610        assert!(matches!(
611            result,
612            Err(Error::OverrideOrderViolation {
613                prev: 1,
614                current: 0
615            })
616        ));
617    }
618
619    #[test]
620    fn pubkeys_ordering_violation_rejected_at_decoder() {
621        // Encode (0, [0;65]) then a deliberately mis-ordered (0, [0;65]) by
622        // hand-building the bytes — exercises the read_sparse_tlv_idx
623        // helper's ascending check on the read side.
624        let mut sub = BitWriter::new();
625        // idx=1 (2-bit width) then 65 zero bytes.
626        sub.write_bits(1, 2);
627        for _ in 0..65 {
628            sub.write_bits(0, 8);
629        }
630        // idx=1 again → ordering violation (1 not > 1).
631        sub.write_bits(1, 2);
632        for _ in 0..65 {
633            sub.write_bits(0, 8);
634        }
635        let bit_len = sub.bit_len();
636        let payload_bytes = sub.into_bytes();
637
638        let mut w = BitWriter::new();
639        w.write_bits(u64::from(TLV_PUBKEYS), 5);
640        write_varint(&mut w, bit_len as u32).unwrap();
641        re_emit_bits(&mut w, &payload_bytes, bit_len).unwrap();
642        let total_bit_len = w.bit_len();
643        let bytes = w.into_bytes();
644
645        let mut r = BitReader::with_bit_limit(&bytes, total_bit_len);
646        let result = TlvSection::read(&mut r, 2, 3);
647        assert!(matches!(
648            result,
649            Err(Error::OverrideOrderViolation {
650                prev: 1,
651                current: 1
652            })
653        ));
654    }
655
656    #[test]
657    fn read_sparse_tlv_idx_out_of_range() {
658        // Build 2-bit idx=3 with n=2 → out of range.
659        let mut sub = BitWriter::new();
660        sub.write_bits(3, 2);
661        let bit_len = sub.bit_len();
662        let bytes = sub.into_bytes();
663        let mut r = BitReader::with_bit_limit(&bytes, bit_len);
664
665        let result = read_sparse_tlv_idx(&mut r, 2, 2, None);
666        assert!(matches!(
667            result,
668            Err(Error::PlaceholderIndexOutOfRange { idx: 3, n: 2 })
669        ));
670    }
671
672    #[test]
673    fn read_sparse_tlv_idx_non_ascending() {
674        let mut sub = BitWriter::new();
675        sub.write_bits(0, 2);
676        let bit_len = sub.bit_len();
677        let bytes = sub.into_bytes();
678        let mut r = BitReader::with_bit_limit(&bytes, bit_len);
679
680        let result = read_sparse_tlv_idx(&mut r, 2, 3, Some(1));
681        assert!(matches!(
682            result,
683            Err(Error::OverrideOrderViolation {
684                prev: 1,
685                current: 0
686            })
687        ));
688    }
689
690    #[test]
691    fn empty_pubkeys_vec_rejected_at_encoder() {
692        let mut s = TlvSection::new_empty();
693        s.pubkeys = Some(Vec::new());
694        let mut w = BitWriter::new();
695        let result = s.write(&mut w, 2);
696        assert!(matches!(
697            result,
698            Err(Error::EmptyTlvEntry { tag }) if tag == TLV_PUBKEYS
699        ));
700    }
701
702    #[test]
703    fn empty_origin_path_overrides_vec_rejected_at_encoder() {
704        let mut s = TlvSection::new_empty();
705        s.origin_path_overrides = Some(Vec::new());
706        let mut w = BitWriter::new();
707        let result = s.write(&mut w, 2);
708        assert!(matches!(
709            result,
710            Err(Error::EmptyTlvEntry { tag }) if tag == TLV_ORIGIN_PATH_OVERRIDES
711        ));
712    }
713
714    // ─── Strict bit_len enforcement (v0.13.1, audit L3) ───────────────
715
716    /// Hand-craft a single-TLV wire with one inflated `bit_len`. Returns
717    /// the bytes and the total bit count for `BitReader::with_bit_limit`.
718    fn craft_inflated_tlv_wire(
719        tag: u8,
720        idx: u8,
721        idx_width: u8,
722        record_payload_bits: &[(u64, usize)],
723        slack_bits: usize,
724    ) -> (Vec<u8>, usize) {
725        let mut w = BitWriter::new();
726        // Tag (5 bits).
727        w.write_bits(u64::from(tag), 5);
728        // bit_len (LP4-ext varint) — declares the actual records' bits + slack.
729        let actual_record_bits: usize =
730            (idx_width as usize) + record_payload_bits.iter().map(|(_, n)| n).sum::<usize>();
731        let declared_bit_len = actual_record_bits + slack_bits;
732        write_varint(&mut w, declared_bit_len as u32).unwrap();
733        // Records: idx + payload.
734        w.write_bits(u64::from(idx), idx_width as usize);
735        for (val, bits) in record_payload_bits {
736            w.write_bits(*val, *bits);
737        }
738        // Append slack zero-bits.
739        for _ in 0..slack_bits {
740            w.write_bits(0, 1);
741        }
742        let bit_len = w.bit_len();
743        (w.into_bytes(), bit_len)
744    }
745
746    // The four tests below exercise the L3 audit concern: a wire that
747    // declares more `bit_len` than its records actually carry must be
748    // rejected, with no silent advancement of the outer reader's
749    // cursor past the declared body. The specific error variant depends
750    // on the slack-bit pattern (typically `OverrideOrderViolation` when
751    // slack starts with zero and the previous idx was 0, or
752    // `BitStreamTruncated` when slack is too short for a phantom idx).
753    // The contract under test is "rejection happens," not the variant
754    // name. The `bit_limit` bound inside `read_sparse_tlv_body` is the
755    // load-bearing fix.
756
757    #[test]
758    fn fingerprints_with_trailing_slack_rejected() {
759        let (bytes, total_bits) =
760            craft_inflated_tlv_wire(TLV_FINGERPRINTS, 0, 1, &[(0xDEAD_BEEF, 32)], 4);
761        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
762        let result = TlvSection::read(&mut r, 1, 1);
763        assert!(
764            result.is_err(),
765            "trailing slack must be rejected, got {:?}",
766            result
767        );
768    }
769
770    #[test]
771    fn pubkeys_with_trailing_slack_rejected() {
772        let payload: Vec<(u64, usize)> = (0..65).map(|_i| (0x42u64, 8)).collect();
773        let (bytes, total_bits) = craft_inflated_tlv_wire(TLV_PUBKEYS, 0, 1, &payload, 3);
774        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
775        let result = TlvSection::read(&mut r, 1, 1);
776        assert!(
777            result.is_err(),
778            "trailing slack must be rejected, got {:?}",
779            result
780        );
781    }
782
783    #[test]
784    fn use_site_path_overrides_with_trailing_slack_rejected() {
785        let mut path_w = BitWriter::new();
786        UseSitePath::standard_multipath()
787            .write(&mut path_w)
788            .unwrap();
789        let path_bit_len = path_w.bit_len();
790        let path_bytes = path_w.into_bytes();
791        let mut path_record: Vec<(u64, usize)> = Vec::new();
792        let mut br = BitReader::new(&path_bytes);
793        let mut consumed = 0;
794        while consumed < path_bit_len {
795            let chunk = (path_bit_len - consumed).min(8);
796            path_record.push((br.read_bits(chunk).unwrap(), chunk));
797            consumed += chunk;
798        }
799        let (bytes, total_bits) =
800            craft_inflated_tlv_wire(TLV_USE_SITE_PATH_OVERRIDES, 0, 1, &path_record, 2);
801        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
802        let result = TlvSection::read(&mut r, 1, 1);
803        assert!(
804            result.is_err(),
805            "trailing slack must be rejected, got {:?}",
806            result
807        );
808    }
809
810    #[test]
811    fn origin_path_overrides_with_trailing_slack_rejected() {
812        let (bytes, total_bits) =
813            craft_inflated_tlv_wire(TLV_ORIGIN_PATH_OVERRIDES, 0, 1, &[(0, 4)], 5);
814        let mut r = BitReader::with_bit_limit(&bytes, total_bits);
815        let result = TlvSection::read(&mut r, 1, 1);
816        assert!(
817            result.is_err(),
818            "trailing slack must be rejected, got {:?}",
819            result
820        );
821    }
822}