rama_net/address/domain/
label.rs1use core::hash::{Hash, Hasher};
13use core::{cmp::Ordering, fmt};
14
15#[repr(transparent)]
35pub struct Label(str);
36
37impl Label {
38 pub const MAX_LEN: usize = 63;
40
41 #[expect(
47 clippy::should_implement_trait,
48 reason = "Label is !Sized; FromStr requires Sized + returns Self by value"
49 )]
50 pub fn from_str(s: &str) -> Result<&Self, LabelError> {
51 validate_label_bytes(s.as_bytes())?;
52 Ok(unsafe { Self::from_str_unchecked(s) })
54 }
55
56 pub(crate) unsafe fn from_str_unchecked(s: &str) -> &Self {
64 unsafe { &*(s as *const str as *const Self) }
67 }
68
69 #[must_use]
71 pub fn as_str(&self) -> &str {
72 &self.0
73 }
74
75 #[expect(
80 clippy::len_without_is_empty,
81 reason = "Label is non-empty by invariant; is_empty would be trivially false"
82 )]
83 #[must_use]
84 pub fn len(&self) -> usize {
85 self.0.len()
86 }
87
88 #[must_use]
90 pub fn is_wildcard(&self) -> bool {
91 self.0.as_bytes() == b"*"
92 }
93}
94
95impl AsRef<str> for Label {
96 fn as_ref(&self) -> &str {
97 &self.0
98 }
99}
100
101impl fmt::Debug for Label {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "Label({:?})", &self.0)
104 }
105}
106
107impl fmt::Display for Label {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 self.0.fmt(f)
110 }
111}
112
113impl PartialEq for Label {
114 fn eq(&self, other: &Self) -> bool {
115 self.0.eq_ignore_ascii_case(&other.0)
116 }
117}
118
119impl Eq for Label {}
120
121impl Hash for Label {
122 fn hash<H: Hasher>(&self, state: &mut H) {
123 state.write_usize(self.0.len());
125 for b in self.0.bytes() {
126 state.write_u8(b.to_ascii_lowercase());
127 }
128 }
129}
130
131impl Ord for Label {
132 fn cmp(&self, other: &Self) -> Ordering {
133 cmp_ignore_ascii_case(&self.0, &other.0)
134 }
135}
136
137pub(super) fn cmp_ignore_ascii_case(a: &str, b: &str) -> Ordering {
142 a.bytes()
143 .map(|c| c.to_ascii_lowercase())
144 .cmp(b.bytes().map(|c| c.to_ascii_lowercase()))
145}
146
147impl PartialOrd for Label {
148 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
149 Some(self.cmp(other))
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct LabelError(LabelErrorKind);
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158enum LabelErrorKind {
159 Empty,
160 TooLong { len: usize },
161 LeadingHyphen,
162 TrailingHyphen,
163 InvalidChar { byte: u8, at: usize },
164}
165
166impl LabelError {
167 #[inline]
168 pub(crate) const fn empty() -> Self {
169 Self(LabelErrorKind::Empty)
170 }
171 #[inline]
172 pub(crate) const fn too_long(len: usize) -> Self {
173 Self(LabelErrorKind::TooLong { len })
174 }
175 #[inline]
176 pub(crate) const fn leading_hyphen() -> Self {
177 Self(LabelErrorKind::LeadingHyphen)
178 }
179 #[inline]
180 pub(crate) const fn trailing_hyphen() -> Self {
181 Self(LabelErrorKind::TrailingHyphen)
182 }
183 #[inline]
184 pub(crate) const fn invalid_char(byte: u8, at: usize) -> Self {
185 Self(LabelErrorKind::InvalidChar { byte, at })
186 }
187}
188
189impl fmt::Display for LabelError {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 match &self.0 {
192 LabelErrorKind::Empty => f.write_str("empty domain label"),
193 LabelErrorKind::TooLong { len } => write!(
194 f,
195 "domain label is {len} bytes long, max is {}",
196 Label::MAX_LEN
197 ),
198 LabelErrorKind::LeadingHyphen => f.write_str("domain label may not start with '-'"),
199 LabelErrorKind::TrailingHyphen => f.write_str("domain label may not end with '-'"),
200 LabelErrorKind::InvalidChar { byte, at } => {
201 write!(f, "invalid byte 0x{byte:02x} in domain label at index {at}")
202 }
203 }
204 }
205}
206
207impl core::error::Error for LabelError {}
208
209pub(crate) const fn validate_label_bytes(bytes: &[u8]) -> Result<(), LabelError> {
216 if bytes.is_empty() {
217 return Err(LabelError::empty());
218 }
219 if bytes.len() > Label::MAX_LEN {
220 return Err(LabelError::too_long(bytes.len()));
221 }
222
223 if bytes.len() == 1 && bytes[0] == b'*' {
226 return Ok(());
227 }
228
229 if bytes[0] == b'-' {
230 return Err(LabelError::leading_hyphen());
231 }
232 if bytes[bytes.len() - 1] == b'-' {
233 return Err(LabelError::trailing_hyphen());
234 }
235
236 let mut i = 0;
237 while i < bytes.len() {
238 let c = bytes[i];
239 if !crate::byte_sets::is_label_byte(c) {
240 return Err(LabelError::invalid_char(c, i));
241 }
242 i += 1;
243 }
244 Ok(())
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 use ahash::{HashMap, HashMapExt as _};
252
253 #[test]
254 fn valid_labels() {
255 for s in [
256 "a",
257 "A",
258 "aA1",
259 "example",
260 "_acme-challenge",
261 "_acme_challenge_",
262 "a-b-c",
263 "rr5---sn-q4fl6n6s",
264 "127",
265 "*",
266 ] {
267 Label::from_str(s).unwrap_or_else(|e| panic!("expected ok for {s:?}: {e}"));
268 }
269 }
270
271 #[test]
272 fn invalid_labels() {
273 let cases: &[(&str, &str)] = &[
274 ("", "empty"),
275 ("-foo", "leading hyphen"),
276 ("foo-", "trailing hyphen"),
277 ("-", "leading hyphen"),
278 ("foo.bar", "dot not allowed inside label"),
279 ("foo*bar", "embedded wildcard"),
280 ("*foo", "wildcard with extra"),
281 ("foo*", "wildcard with extra"),
282 ("こんにちは", "non-ascii"),
283 ("foo bar", "space"),
284 ];
285 for (s, why) in cases {
286 assert!(
287 Label::from_str(s).is_err(),
288 "expected error for {s:?} ({why})"
289 );
290 }
291
292 let too_long = "a".repeat(Label::MAX_LEN + 1);
294 let err = Label::from_str(&too_long).unwrap_err();
295 assert!(format!("{err}").contains("max is 63"));
296 }
297
298 #[test]
299 fn ascii_case_insensitive_eq_hash_ord() {
300 let a = Label::from_str("Example").unwrap();
301 let b = Label::from_str("eXaMpLe").unwrap();
302 assert_eq!(a, b);
303 assert_eq!(a.cmp(b), Ordering::Equal);
304
305 let mut m: HashMap<&Label, ()> = HashMap::new();
306 m.insert(Label::from_str("Foo").unwrap(), ());
307 assert!(m.contains_key(Label::from_str("FOO").unwrap()));
308 assert!(m.contains_key(Label::from_str("foo").unwrap()));
309 assert!(!m.contains_key(Label::from_str("foo2").unwrap()));
310 }
311
312 #[test]
313 fn ordering_lex_case_folded() {
314 let a = Label::from_str("Apple").unwrap();
315 let b = Label::from_str("banana").unwrap();
316 assert!(a < b);
317 assert!(b > a);
318
319 let pre = Label::from_str("foo").unwrap();
321 let longer = Label::from_str("foobar").unwrap();
322 assert!(pre < longer);
323 }
324
325 #[test]
326 fn wildcard_helper() {
327 assert!(Label::from_str("*").unwrap().is_wildcard());
328 assert!(!Label::from_str("foo").unwrap().is_wildcard());
329 }
330
331 #[test]
332 fn unchecked_constructor_layout() {
333 let s = "valid";
335 let l = unsafe { Label::from_str_unchecked(s) };
336 assert_eq!(l.as_str(), s);
337 assert_eq!(l.len(), s.len());
338 }
339
340 #[test]
341 fn label_byte_set_matches_predicate() {
342 for b in 0u8..=255 {
347 let expected = b.is_ascii_alphanumeric() || b == b'_' || b == b'-';
348 assert_eq!(
349 crate::byte_sets::is_label_byte(b),
350 expected,
351 "byte 0x{b:02x} ({}) — LUT disagreed with predicate",
352 if b.is_ascii_graphic() {
353 format!("{:?}", b as char)
354 } else {
355 "non-graphic".to_owned()
356 }
357 );
358 }
359 }
360}