vti_common/slip10/
path.rs1use std::fmt;
16use std::str::FromStr;
17
18#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
25pub enum ChildIndex {
26 Normal(u32),
28 Hardened(u32),
30}
31
32#[derive(Debug, Clone, thiserror::Error)]
34pub enum ChildIndexError {
35 #[error("number too large: {0}")]
37 NumberTooLarge(u32),
38}
39
40#[derive(Debug, Clone, thiserror::Error)]
42pub enum ChildIndexParseError {
43 #[error("could not parse child index: {0}")]
45 ParseInt(#[from] std::num::ParseIntError),
46 #[error("invalid child index: {0}")]
48 ChildIndex(#[from] ChildIndexError),
49}
50
51impl ChildIndex {
52 pub fn hardened(num: u32) -> Result<Self, ChildIndexError> {
54 Ok(Self::Hardened(Self::check_size(num)?))
55 }
56
57 pub fn normal(num: u32) -> Result<Self, ChildIndexError> {
59 Ok(Self::Normal(Self::check_size(num)?))
60 }
61
62 fn check_size(num: u32) -> Result<u32, ChildIndexError> {
63 if num & (1 << 31) == 0 {
64 Ok(num)
65 } else {
66 Err(ChildIndexError::NumberTooLarge(num))
67 }
68 }
69
70 #[inline]
72 pub fn to_u32(self) -> u32 {
73 match self {
74 Self::Hardened(index) | Self::Normal(index) => index,
75 }
76 }
77
78 #[inline]
83 pub fn to_bits(self) -> u32 {
84 match self {
85 Self::Hardened(index) => (1 << 31) | index,
86 Self::Normal(index) => index,
87 }
88 }
89
90 #[inline]
92 pub fn from_bits(bits: u32) -> Self {
93 if bits & (1 << 31) == 0 {
94 Self::Normal(bits)
95 } else {
96 Self::Hardened(bits & !(1 << 31))
97 }
98 }
99
100 #[inline]
102 pub fn is_hardened(self) -> bool {
103 matches!(self, Self::Hardened(_))
104 }
105
106 #[inline]
108 pub fn is_normal(self) -> bool {
109 matches!(self, Self::Normal(_))
110 }
111}
112
113impl fmt::Display for ChildIndex {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 fmt::Display::fmt(&self.to_u32(), f)?;
116 if self.is_hardened() {
117 f.write_str("'")?;
118 }
119 Ok(())
120 }
121}
122
123impl FromStr for ChildIndex {
124 type Err = ChildIndexParseError;
125
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 let mut chars = s.chars();
128 Ok(match chars.next_back() {
129 Some('\'') => Self::hardened(u32::from_str(chars.as_str())?)?,
130 _ => Self::normal(u32::from_str(s)?)?,
133 })
134 }
135}
136
137#[derive(Debug, Clone, thiserror::Error)]
139pub enum DerivationPathParseError {
140 #[error("empty")]
142 Empty,
143 #[error("invalid prefix: {0}")]
145 InvalidPrefix(String),
146 #[error("invalid child index: {0}")]
148 InvalidChildIndex(#[from] ChildIndexParseError),
149}
150
151#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct DerivationPath(Box<[ChildIndex]>);
154
155impl DerivationPath {
156 #[inline]
158 pub fn new<P: Into<Box<[ChildIndex]>>>(path: P) -> Self {
159 Self(path.into())
160 }
161
162 #[inline]
164 pub fn path(&self) -> &[ChildIndex] {
165 &self.0
166 }
167
168 #[inline]
170 pub fn len(&self) -> usize {
171 self.0.len()
172 }
173
174 #[inline]
176 pub fn is_empty(&self) -> bool {
177 self.0.is_empty()
178 }
179}
180
181impl fmt::Display for DerivationPath {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.write_str("m")?;
184 for index in self.path() {
185 f.write_str("/")?;
186 fmt::Display::fmt(index, f)?;
187 }
188 Ok(())
189 }
190}
191
192impl FromStr for DerivationPath {
193 type Err = DerivationPathParseError;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 if s.is_empty() {
197 return Err(DerivationPathParseError::Empty);
198 }
199 let mut parts = s.split('/');
200 match parts.next().expect("split yields at least one segment") {
202 "m" => (),
203 prefix => return Err(DerivationPathParseError::InvalidPrefix(prefix.to_owned())),
204 }
205 let path = parts
206 .map(|part| ChildIndex::from_str(part).map_err(DerivationPathParseError::from))
207 .collect::<Result<Box<[ChildIndex]>, _>>()?;
208 Ok(Self::new(path))
209 }
210}
211
212impl AsRef<[ChildIndex]> for DerivationPath {
213 fn as_ref(&self) -> &[ChildIndex] {
214 self.path()
215 }
216}
217
218impl<'a> IntoIterator for &'a DerivationPath {
219 type IntoIter = std::slice::Iter<'a, ChildIndex>;
220 type Item = &'a ChildIndex;
221
222 fn into_iter(self) -> Self::IntoIter {
223 self.path().iter()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn parses_the_workspace_key_hierarchy_shape() {
233 let path: DerivationPath = "m/26'/2'/0'/17'".parse().unwrap();
235 assert_eq!(
236 path.path(),
237 &[
238 ChildIndex::Hardened(26),
239 ChildIndex::Hardened(2),
240 ChildIndex::Hardened(0),
241 ChildIndex::Hardened(17),
242 ]
243 );
244 assert_eq!(path.to_string(), "m/26'/2'/0'/17'");
245 }
246
247 #[test]
248 fn round_trips_mixed_hardened_and_normal() {
249 let path: DerivationPath = "m/44'/0'/0'/1/0".parse().unwrap();
250 assert_eq!(path.path()[3], ChildIndex::Normal(1));
251 assert_eq!(path.path()[4], ChildIndex::Normal(0));
252 assert_eq!(path.to_string(), "m/44'/0'/0'/1/0");
253 }
254
255 #[test]
256 fn bare_master_path_is_empty() {
257 let path: DerivationPath = "m".parse().unwrap();
258 assert!(path.is_empty());
259 assert_eq!(path.to_string(), "m");
260 }
261
262 #[test]
263 fn rejects_empty_input() {
264 assert!(matches!(
265 "".parse::<DerivationPath>(),
266 Err(DerivationPathParseError::Empty)
267 ));
268 }
269
270 #[test]
271 fn rejects_a_missing_or_wrong_prefix() {
272 assert!(matches!(
274 "44'/0'/0'".parse::<DerivationPath>(),
275 Err(DerivationPathParseError::InvalidPrefix(_))
276 ));
277 assert!(matches!(
278 "not/a/valid/path".parse::<DerivationPath>(),
279 Err(DerivationPathParseError::InvalidPrefix(_))
280 ));
281 assert!(matches!(
282 "M/44'".parse::<DerivationPath>(),
283 Err(DerivationPathParseError::InvalidPrefix(_))
284 ));
285 }
286
287 #[test]
288 fn rejects_a_trailing_separator() {
289 assert!("m/".parse::<DerivationPath>().is_err());
291 }
292
293 #[test]
294 fn rejects_an_index_with_bit_31_set() {
295 assert!("m/2147483648'".parse::<DerivationPath>().is_err());
297 assert!("m/2147483648".parse::<DerivationPath>().is_err());
298 let path: DerivationPath = "m/2147483647'".parse().unwrap();
300 assert_eq!(path.path()[0], ChildIndex::Hardened(2147483647));
301 }
302
303 #[test]
304 fn rejects_non_numeric_segments() {
305 assert!("m/abc'".parse::<DerivationPath>().is_err());
306 assert!("m/-1".parse::<DerivationPath>().is_err());
307 assert!("m/1''".parse::<DerivationPath>().is_err());
308 }
309
310 #[test]
311 fn bit_encoding_round_trips() {
312 for index in [
313 ChildIndex::Normal(0),
314 ChildIndex::Normal(2147483647),
315 ChildIndex::Hardened(0),
316 ChildIndex::Hardened(26),
317 ChildIndex::Hardened(2147483647),
318 ] {
319 assert_eq!(ChildIndex::from_bits(index.to_bits()), index);
320 }
321 assert_eq!(ChildIndex::Hardened(0).to_bits(), 0x8000_0000);
322 assert_eq!(ChildIndex::Normal(0).to_bits(), 0);
323 }
324
325 #[test]
326 fn rejects_out_of_range_constructors() {
327 assert!(ChildIndex::hardened(1 << 31).is_err());
328 assert!(ChildIndex::normal(1 << 31).is_err());
329 assert!(ChildIndex::hardened((1 << 31) - 1).is_ok());
330 }
331}