Skip to main content

md_codec/
use_site_path.rs

1//! Use-site-path-decl block per spec §3.5.
2//!
3//! Block format:
4//!   [has-multipath: 1 bit]
5//!   [if has-multipath:
6//!     [alt-count: 3 bits, encoded count-2; range 2..9]
7//!     [alternative × count]
8//!   ]
9//!   [wildcard-hardened: 1 bit]
10//!
11//! alternative: [hardened: 1 bit][value: LP4-ext varint]
12
13use crate::bitstream::{BitReader, BitWriter};
14use crate::error::Error;
15use crate::varint::{read_varint, write_varint};
16
17/// One alternative within a multipath substitution group.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Alternative {
20    /// Whether this alternative is a hardened child index.
21    pub hardened: bool,
22    /// Child-number value of this alternative.
23    pub value: u32,
24}
25
26impl Alternative {
27    /// Encode this alternative onto the bit stream `w`.
28    pub fn write(&self, w: &mut BitWriter) -> Result<(), Error> {
29        w.write_bits(u64::from(self.hardened), 1);
30        write_varint(w, self.value)?;
31        Ok(())
32    }
33
34    /// Decode a single alternative from the bit stream `r`.
35    pub fn read(r: &mut BitReader) -> Result<Self, Error> {
36        let hardened = r.read_bits(1)? != 0;
37        let value = read_varint(r)?;
38        Ok(Self { hardened, value })
39    }
40}
41
42/// Minimum number of alternatives in a multipath group.
43pub const MIN_ALT_COUNT: usize = 2;
44/// Maximum number of alternatives in a multipath group (3-bit field + 2).
45pub const MAX_ALT_COUNT: usize = 9;
46
47/// Use-site path declaration: an optional multipath group plus a wildcard.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct UseSitePath {
50    /// Optional multipath alternatives (`Some` ⇒ has-multipath bit set).
51    pub multipath: Option<Vec<Alternative>>,
52    /// Whether the trailing wildcard is hardened (`*h`).
53    pub wildcard_hardened: bool,
54}
55
56impl UseSitePath {
57    /// The dominant `<0;1>/*` form.
58    pub fn standard_multipath() -> Self {
59        Self {
60            multipath: Some(vec![
61                Alternative {
62                    hardened: false,
63                    value: 0,
64                },
65                Alternative {
66                    hardened: false,
67                    value: 1,
68                },
69            ]),
70            wildcard_hardened: false,
71        }
72    }
73
74    /// Encode this use-site path onto the bit stream `w`.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`Error::AltCountOutOfRange`] if `self.multipath` contains fewer
79    /// than [`MIN_ALT_COUNT`] or more than [`MAX_ALT_COUNT`] alternatives.
80    pub fn write(&self, w: &mut BitWriter) -> Result<(), Error> {
81        if let Some(alts) = &self.multipath {
82            if !(MIN_ALT_COUNT..=MAX_ALT_COUNT).contains(&alts.len()) {
83                return Err(Error::AltCountOutOfRange { got: alts.len() });
84            }
85            w.write_bits(1, 1);
86            // Encode alt-count - 2 in 3 bits per spec §4.2.
87            w.write_bits((alts.len() - MIN_ALT_COUNT) as u64, 3);
88            for a in alts {
89                a.write(w)?;
90            }
91        } else {
92            w.write_bits(0, 1);
93        }
94        w.write_bits(u64::from(self.wildcard_hardened), 1);
95        Ok(())
96    }
97
98    /// Decode a use-site path from the bit stream `r`.
99    pub fn read(r: &mut BitReader) -> Result<Self, Error> {
100        let has_multipath = r.read_bits(1)? != 0;
101        let multipath = if has_multipath {
102            let alt_count = (r.read_bits(3)? as usize) + MIN_ALT_COUNT;
103            let mut alts = Vec::with_capacity(alt_count);
104            for _ in 0..alt_count {
105                alts.push(Alternative::read(r)?);
106            }
107            Some(alts)
108        } else {
109            None
110        };
111        let wildcard_hardened = r.read_bits(1)? != 0;
112        Ok(Self {
113            multipath,
114            wildcard_hardened,
115        })
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn use_site_path_standard_round_trip() {
125        let p = UseSitePath::standard_multipath();
126        let mut w = BitWriter::new();
127        p.write(&mut w).unwrap();
128        let bytes = w.into_bytes();
129        let mut r = BitReader::new(&bytes);
130        assert_eq!(UseSitePath::read(&mut r).unwrap(), p);
131    }
132
133    #[test]
134    fn use_site_path_standard_bit_cost() {
135        // has-mp(1) + count=2(3) + alt0 (1+4) + alt1 (1+5) + wildcard(1) = 16 bits
136        let p = UseSitePath::standard_multipath();
137        let mut w = BitWriter::new();
138        p.write(&mut w).unwrap();
139        assert_eq!(w.bit_len(), 16);
140    }
141
142    #[test]
143    fn use_site_path_bare_star_round_trip() {
144        let p = UseSitePath {
145            multipath: None,
146            wildcard_hardened: false,
147        };
148        let mut w = BitWriter::new();
149        p.write(&mut w).unwrap();
150        let bytes = w.into_bytes();
151        let mut r = BitReader::new(&bytes);
152        assert_eq!(UseSitePath::read(&mut r).unwrap(), p);
153    }
154
155    #[test]
156    fn use_site_path_bare_star_bit_cost() {
157        // has-mp(0) + wildcard(0) = 2 bits
158        let p = UseSitePath {
159            multipath: None,
160            wildcard_hardened: false,
161        };
162        let mut w = BitWriter::new();
163        p.write(&mut w).unwrap();
164        assert_eq!(w.bit_len(), 2);
165    }
166
167    #[test]
168    fn use_site_path_hardened_wildcard_round_trip() {
169        let p = UseSitePath {
170            multipath: None,
171            wildcard_hardened: true,
172        };
173        let mut w = BitWriter::new();
174        p.write(&mut w).unwrap();
175        let bytes = w.into_bytes();
176        let mut r = BitReader::new(&bytes);
177        assert_eq!(UseSitePath::read(&mut r).unwrap(), p);
178    }
179
180    #[test]
181    fn use_site_path_alt_count_too_small_rejected() {
182        let p = UseSitePath {
183            multipath: Some(vec![Alternative {
184                hardened: false,
185                value: 0,
186            }]),
187            wildcard_hardened: false,
188        };
189        let mut w = BitWriter::new();
190        assert!(matches!(
191            p.write(&mut w),
192            Err(Error::AltCountOutOfRange { got: 1 })
193        ));
194    }
195
196    #[test]
197    fn use_site_path_alt_count_too_large_rejected() {
198        let p = UseSitePath {
199            multipath: Some(
200                (0..10)
201                    .map(|i| Alternative {
202                        hardened: false,
203                        value: i,
204                    })
205                    .collect(),
206            ),
207            wildcard_hardened: false,
208        };
209        let mut w = BitWriter::new();
210        assert!(matches!(
211            p.write(&mut w),
212            Err(Error::AltCountOutOfRange { got: 10 })
213        ));
214    }
215}