Skip to content

Commit 11d6cb4

Browse files
committed
hrp: add constructor from an arbitrary T: Display
Fixes #195
1 parent 49ddb1c commit 11d6cb4

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed

src/primitives/hrp.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,80 @@ impl Hrp {
119119
Ok(new)
120120
}
121121

122+
/// Parses the human-readable part from an object which can be formatted.
123+
///
124+
/// The formatted form of the object is subject to all the same rules as [`Self::parse`].
125+
/// This method is semantically equivalent to `Hrp::parse(&data.to_string())` but avoids
126+
/// allocating an intermediate string.
127+
pub fn parse_display<T: core::fmt::Display>(data: T) -> Result<Self, Error> {
128+
use Error::*;
129+
130+
struct ByteFormatter {
131+
arr: [u8; MAX_HRP_LEN],
132+
index: usize,
133+
error: Option<Error>,
134+
}
135+
136+
impl core::fmt::Write for ByteFormatter {
137+
fn write_str(&mut self, s: &str) -> fmt::Result {
138+
let mut has_lower: bool = false;
139+
let mut has_upper: bool = false;
140+
for ch in s.chars() {
141+
let b = ch as u8; // cast ok, `b` unused until `ch` is checked to be ASCII
142+
143+
// Break after finding an error so that we report the first invalid
144+
// character, not the last.
145+
if !ch.is_ascii() {
146+
self.error = Some(Error::NonAsciiChar(ch));
147+
break;
148+
} else if !(33..=126).contains(&b) {
149+
self.error = Some(InvalidAsciiByte(b));
150+
break;
151+
}
152+
153+
if ch.is_ascii_lowercase() {
154+
if has_upper {
155+
self.error = Some(MixedCase);
156+
break;
157+
}
158+
has_lower = true;
159+
} else if ch.is_ascii_uppercase() {
160+
if has_lower {
161+
self.error = Some(MixedCase);
162+
break;
163+
}
164+
has_upper = true;
165+
};
166+
}
167+
168+
// However, an invalid length error will take priority over an
169+
// invalid character error.
170+
if self.index + s.len() > self.arr.len() {
171+
self.error = Some(Error::TooLong(self.index + s.len()));
172+
} else {
173+
// Only do the actual copy if we passed the index check.
174+
self.arr[self.index..self.index + s.len()].copy_from_slice(s.as_bytes());
175+
}
176+
177+
// Unconditionally update self.index so that in the case of a too-long
178+
// string, our error return will reflect the full length.
179+
self.index += s.len();
180+
Ok(())
181+
}
182+
}
183+
184+
let mut byte_formatter = ByteFormatter { arr: [0; MAX_HRP_LEN], index: 0, error: None };
185+
186+
write!(byte_formatter, "{}", data).expect("custom Formatter cannot fail");
187+
if byte_formatter.index == 0 {
188+
Err(Empty)
189+
} else if let Some(err) = byte_formatter.error {
190+
Err(err)
191+
} else {
192+
Ok(Self { buf: byte_formatter.arr, size: byte_formatter.index })
193+
}
194+
}
195+
122196
/// Parses the human-readable part (see [`Hrp::parse`] for full docs).
123197
///
124198
/// Does not check that `hrp` is valid according to BIP-173 but does check for valid ASCII
@@ -424,6 +498,7 @@ mod tests {
424498
#[test]
425499
fn $test_name() {
426500
assert!(Hrp::parse($hrp).is_ok());
501+
assert!(Hrp::parse_display($hrp).is_ok());
427502
}
428503
)*
429504
}
@@ -445,6 +520,7 @@ mod tests {
445520
#[test]
446521
fn $test_name() {
447522
assert!(Hrp::parse($hrp).is_err());
523+
assert!(Hrp::parse_display($hrp).is_err());
448524
}
449525
)*
450526
}
@@ -538,4 +614,45 @@ mod tests {
538614
let hrp = Hrp::parse_unchecked(s);
539615
assert_eq!(hrp.as_bytes(), s.as_bytes());
540616
}
617+
618+
#[test]
619+
fn parse_display() {
620+
let hrp = Hrp::parse_display(format_args!("{}_{}", 123, "abc")).unwrap();
621+
assert_eq!(hrp.as_str(), "123_abc");
622+
623+
let hrp = Hrp::parse_display(format_args!("{:083}", 1)).unwrap();
624+
assert_eq!(
625+
hrp.as_str(),
626+
"00000000000000000000000000000000000000000000000000000000000000000000000000000000001"
627+
);
628+
629+
assert_eq!(Hrp::parse_display(format_args!("{:084}", 1)), Err(Error::TooLong(84)),);
630+
631+
assert_eq!(
632+
Hrp::parse_display(format_args!("{:83}", 1)),
633+
Err(Error::InvalidAsciiByte(b' ')),
634+
);
635+
}
636+
637+
#[test]
638+
fn parse_non_ascii() {
639+
assert_eq!(Hrp::parse("❤").unwrap_err(), Error::NonAsciiChar('❤'));
640+
}
641+
642+
#[test]
643+
fn parse_display_non_ascii() {
644+
assert_eq!(Hrp::parse_display("❤").unwrap_err(), Error::NonAsciiChar('❤'));
645+
}
646+
647+
#[test]
648+
fn parse_display_returns_first_error() {
649+
assert_eq!(Hrp::parse_display("❤ ").unwrap_err(), Error::NonAsciiChar('❤'));
650+
}
651+
652+
// This test shows that the error does not contain heart.
653+
#[test]
654+
fn parse_display_iterates_chars() {
655+
assert_eq!(Hrp::parse_display(" ❤").unwrap_err(), Error::InvalidAsciiByte(b' '));
656+
assert_eq!(Hrp::parse_display("_❤").unwrap_err(), Error::NonAsciiChar('❤'));
657+
}
541658
}

0 commit comments

Comments
 (0)