Skip to content

Commit 2f3d2bd

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 2f3d2bd

File tree

1 file changed

+27
-15
lines changed

1 file changed

+27
-15
lines changed

src/hrn.rs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@
33
44
use alloc::string::{String, ToString};
55

6+
// Note that `REQUIRED_EXTRA_LEN` includes the (implicit) trailing `.`
7+
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
8+
69
/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts.
710
///
8-
/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be
11+
/// The `user` and `domain` parts, together, cannot exceed 231 bytes in length, and both must be
912
/// non-empty.
1013
///
11-
/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain
12-
/// ASCII.
14+
/// If you intend to handle non-ASCII `user` or `domain` parts, you must handle [Homograph Attacks]
15+
/// and do punycode en-/de-coding yourself. This struc will always handle only plain ASCII `user`
16+
/// and `domain` parts.
1317
///
1418
/// This struct can also be used for LN-Address recipients.
1519
///
1620
/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
1721
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1822
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,
23+
contents: [u8; 255 - REQUIRED_EXTRA_LEN],
24+
user_len: u8,
25+
domain_len: u8,
2226
}
2327

2428
/// Check if the chars in `s` are allowed to be included in a hostname.
@@ -29,13 +33,11 @@ pub(crate) fn str_chars_allowed(s: &str) -> bool {
2933
impl HumanReadableName {
3034
/// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the
3135
/// struct-level documentation for more on the requirements on each.
32-
pub fn new(user: String, mut domain: String) -> Result<HumanReadableName, ()> {
36+
pub fn new(user: &str, mut domain: &str) -> Result<HumanReadableName, ()> {
3337
// First normalize domain and remove the optional trailing `.`
34-
if domain.ends_with(".") {
35-
domain.pop();
38+
if domain.ends_with('.') {
39+
domain = &domain[..domain.len() - 1];
3640
}
37-
// Note that `REQUIRED_EXTRA_LEN` includes the (now implicit) trailing `.`
38-
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
3941
if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 {
4042
return Err(());
4143
}
@@ -45,7 +47,14 @@ impl HumanReadableName {
4547
if !str_chars_allowed(&user) || !str_chars_allowed(&domain) {
4648
return Err(());
4749
}
48-
Ok(HumanReadableName { user, domain })
50+
let mut contents = [0; 255 - REQUIRED_EXTRA_LEN];
51+
contents[..user.len()].copy_from_slice(user.as_bytes());
52+
contents[user.len()..user.len() + domain.len()].copy_from_slice(domain.as_bytes());
53+
Ok(HumanReadableName {
54+
contents,
55+
user_len: user.len() as u8,
56+
domain_len: domain.len() as u8,
57+
})
4958
}
5059

5160
/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
@@ -55,19 +64,22 @@ impl HumanReadableName {
5564
pub fn from_encoded(encoded: &str) -> Result<HumanReadableName, ()> {
5665
if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@")
5766
{
58-
Self::new(user.to_string(), domain.to_string())
67+
Self::new(user, domain)
5968
} else {
6069
Err(())
6170
}
6271
}
6372

6473
/// Gets the `user` part of this Human Readable Name
6574
pub fn user(&self) -> &str {
66-
&self.user
75+
let bytes = &self.contents[..self.user_len as usize];
76+
core::str::from_utf8(bytes).expect("Checked in constructor")
6777
}
6878

6979
/// Gets the `domain` part of this Human Readable Name
7080
pub fn domain(&self) -> &str {
71-
&self.domain
81+
let user_len = self.user_len as usize;
82+
let bytes = &self.contents[user_len..user_len + self.domain_len as usize];
83+
core::str::from_utf8(bytes).expect("Checked in constructor")
7284
}
7385
}

0 commit comments

Comments
 (0)