Skip to content

Commit dcd099e

Browse files
committed
Store HumanReadableNames in-object rather than on the heap
Since the full encoded domain name of an HRN cannot exceed the maximum length of a DNS name (255 octets), there's not a lot of reason to store the `user` and `domain` parts of an HRN on the heap via two `String`s. Instead, here, we store one byte array with the maximum size of both labels as well as the length of the `user` and `domain` parts. Because we're now avoiding heap allocations this also implies making `HumanReadableName::new` take the `user` and `domain` parts by reference as `&str`s, rather than by value as `String`s.
1 parent a1843e7 commit dcd099e

File tree

1 file changed

+26
-16
lines changed

1 file changed

+26
-16
lines changed

src/hrn.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
//! A type for storing Human Readable Names (HRNs) which can be resolved using BIP 353 and the DNS
22
//! or LNURL-Pay and LN-Address.
33
4-
use alloc::string::{String, ToString};
4+
// Note that `REQUIRED_EXTRA_LEN` includes the (implicit) trailing `.`
5+
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
56

67
/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts.
78
///
8-
/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be
9+
/// The `user` and `domain` parts, together, cannot exceed 231 bytes in length, and both must be
910
/// non-empty.
1011
///
11-
/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain
12-
/// ASCII.
12+
/// If you intend to handle non-ASCII `user` or `domain` parts, you must handle [Homograph Attacks]
13+
/// and do punycode en-/de-coding yourself. This struc will always handle only plain ASCII `user`
14+
/// and `domain` parts.
1315
///
1416
/// This struct can also be used for LN-Address recipients.
1517
///
1618
/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
1719
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1820
pub struct HumanReadableName {
19-
// TODO Remove the heap allocations given the whole data can't be more than 256 bytes.
20-
user: String,
21-
domain: String,
21+
contents: [u8; 255 - REQUIRED_EXTRA_LEN],
22+
user_len: u8,
23+
domain_len: u8,
2224
}
2325

2426
/// Check if the chars in `s` are allowed to be included in a hostname.
@@ -29,13 +31,11 @@ pub(crate) fn str_chars_allowed(s: &str) -> bool {
2931
impl HumanReadableName {
3032
/// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the
3133
/// struct-level documentation for more on the requirements on each.
32-
pub fn new(user: String, mut domain: String) -> Result<HumanReadableName, ()> {
34+
pub fn new(user: &str, mut domain: &str) -> Result<HumanReadableName, ()> {
3335
// First normalize domain and remove the optional trailing `.`
34-
if domain.ends_with(".") {
35-
domain.pop();
36+
if domain.ends_with('.') {
37+
domain = &domain[..domain.len() - 1];
3638
}
37-
// Note that `REQUIRED_EXTRA_LEN` includes the (now implicit) trailing `.`
38-
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
3939
if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 {
4040
return Err(());
4141
}
@@ -45,7 +45,14 @@ impl HumanReadableName {
4545
if !str_chars_allowed(&user) || !str_chars_allowed(&domain) {
4646
return Err(());
4747
}
48-
Ok(HumanReadableName { user, domain })
48+
let mut contents = [0; 255 - REQUIRED_EXTRA_LEN];
49+
contents[..user.len()].copy_from_slice(user.as_bytes());
50+
contents[user.len()..user.len() + domain.len()].copy_from_slice(domain.as_bytes());
51+
Ok(HumanReadableName {
52+
contents,
53+
user_len: user.len() as u8,
54+
domain_len: domain.len() as u8,
55+
})
4956
}
5057

5158
/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
@@ -55,19 +62,22 @@ impl HumanReadableName {
5562
pub fn from_encoded(encoded: &str) -> Result<HumanReadableName, ()> {
5663
if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@")
5764
{
58-
Self::new(user.to_string(), domain.to_string())
65+
Self::new(user, domain)
5966
} else {
6067
Err(())
6168
}
6269
}
6370

6471
/// Gets the `user` part of this Human Readable Name
6572
pub fn user(&self) -> &str {
66-
&self.user
73+
let bytes = &self.contents[..self.user_len as usize];
74+
core::str::from_utf8(bytes).expect("Checked in constructor")
6775
}
6876

6977
/// Gets the `domain` part of this Human Readable Name
7078
pub fn domain(&self) -> &str {
71-
&self.domain
79+
let user_len = self.user_len as usize;
80+
let bytes = &self.contents[user_len..user_len + self.domain_len as usize];
81+
core::str::from_utf8(bytes).expect("Checked in constructor")
7282
}
7383
}

0 commit comments

Comments
 (0)