use core::hash::{Hash, Hasher};
use core::{cmp::Ordering, fmt};
#[repr(transparent)]
pub struct Label(str);
impl Label {
pub const MAX_LEN: usize = 63;
#[expect(
clippy::should_implement_trait,
reason = "Label is !Sized; FromStr requires Sized + returns Self by value"
)]
pub fn from_str(s: &str) -> Result<&Self, LabelError> {
validate_label_bytes(s.as_bytes())?;
Ok(unsafe { Self::from_str_unchecked(s) })
}
pub(crate) unsafe fn from_str_unchecked(s: &str) -> &Self {
unsafe { &*(s as *const str as *const Self) }
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[expect(
clippy::len_without_is_empty,
reason = "Label is non-empty by invariant; is_empty would be trivially false"
)]
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_wildcard(&self) -> bool {
self.0.as_bytes() == b"*"
}
}
impl AsRef<str> for Label {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Label({:?})", &self.0)
}
}
impl fmt::Display for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq for Label {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for Label {}
impl Hash for Label {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_usize(self.0.len());
for b in self.0.bytes() {
state.write_u8(b.to_ascii_lowercase());
}
}
}
impl Ord for Label {
fn cmp(&self, other: &Self) -> Ordering {
cmp_ignore_ascii_case(&self.0, &other.0)
}
}
pub(super) fn cmp_ignore_ascii_case(a: &str, b: &str) -> Ordering {
a.bytes()
.map(|c| c.to_ascii_lowercase())
.cmp(b.bytes().map(|c| c.to_ascii_lowercase()))
}
impl PartialOrd for Label {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LabelError(LabelErrorKind);
#[derive(Debug, Clone, PartialEq, Eq)]
enum LabelErrorKind {
Empty,
TooLong { len: usize },
LeadingHyphen,
TrailingHyphen,
InvalidChar { byte: u8, at: usize },
}
impl LabelError {
#[inline]
pub(crate) const fn empty() -> Self {
Self(LabelErrorKind::Empty)
}
#[inline]
pub(crate) const fn too_long(len: usize) -> Self {
Self(LabelErrorKind::TooLong { len })
}
#[inline]
pub(crate) const fn leading_hyphen() -> Self {
Self(LabelErrorKind::LeadingHyphen)
}
#[inline]
pub(crate) const fn trailing_hyphen() -> Self {
Self(LabelErrorKind::TrailingHyphen)
}
#[inline]
pub(crate) const fn invalid_char(byte: u8, at: usize) -> Self {
Self(LabelErrorKind::InvalidChar { byte, at })
}
}
impl fmt::Display for LabelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
LabelErrorKind::Empty => f.write_str("empty domain label"),
LabelErrorKind::TooLong { len } => write!(
f,
"domain label is {len} bytes long, max is {}",
Label::MAX_LEN
),
LabelErrorKind::LeadingHyphen => f.write_str("domain label may not start with '-'"),
LabelErrorKind::TrailingHyphen => f.write_str("domain label may not end with '-'"),
LabelErrorKind::InvalidChar { byte, at } => {
write!(f, "invalid byte 0x{byte:02x} in domain label at index {at}")
}
}
}
}
impl core::error::Error for LabelError {}
pub(crate) const fn validate_label_bytes(bytes: &[u8]) -> Result<(), LabelError> {
if bytes.is_empty() {
return Err(LabelError::empty());
}
if bytes.len() > Label::MAX_LEN {
return Err(LabelError::too_long(bytes.len()));
}
if bytes.len() == 1 && bytes[0] == b'*' {
return Ok(());
}
if bytes[0] == b'-' {
return Err(LabelError::leading_hyphen());
}
if bytes[bytes.len() - 1] == b'-' {
return Err(LabelError::trailing_hyphen());
}
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if !crate::byte_sets::is_label_byte(c) {
return Err(LabelError::invalid_char(c, i));
}
i += 1;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use ahash::{HashMap, HashMapExt as _};
#[test]
fn valid_labels() {
for s in [
"a",
"A",
"aA1",
"example",
"_acme-challenge",
"_acme_challenge_",
"a-b-c",
"rr5---sn-q4fl6n6s",
"127",
"*",
] {
Label::from_str(s).unwrap_or_else(|e| panic!("expected ok for {s:?}: {e}"));
}
}
#[test]
fn invalid_labels() {
let cases: &[(&str, &str)] = &[
("", "empty"),
("-foo", "leading hyphen"),
("foo-", "trailing hyphen"),
("-", "leading hyphen"),
("foo.bar", "dot not allowed inside label"),
("foo*bar", "embedded wildcard"),
("*foo", "wildcard with extra"),
("foo*", "wildcard with extra"),
("こんにちは", "non-ascii"),
("foo bar", "space"),
];
for (s, why) in cases {
assert!(
Label::from_str(s).is_err(),
"expected error for {s:?} ({why})"
);
}
let too_long = "a".repeat(Label::MAX_LEN + 1);
let err = Label::from_str(&too_long).unwrap_err();
assert!(format!("{err}").contains("max is 63"));
}
#[test]
fn ascii_case_insensitive_eq_hash_ord() {
let a = Label::from_str("Example").unwrap();
let b = Label::from_str("eXaMpLe").unwrap();
assert_eq!(a, b);
assert_eq!(a.cmp(b), Ordering::Equal);
let mut m: HashMap<&Label, ()> = HashMap::new();
m.insert(Label::from_str("Foo").unwrap(), ());
assert!(m.contains_key(Label::from_str("FOO").unwrap()));
assert!(m.contains_key(Label::from_str("foo").unwrap()));
assert!(!m.contains_key(Label::from_str("foo2").unwrap()));
}
#[test]
fn ordering_lex_case_folded() {
let a = Label::from_str("Apple").unwrap();
let b = Label::from_str("banana").unwrap();
assert!(a < b);
assert!(b > a);
let pre = Label::from_str("foo").unwrap();
let longer = Label::from_str("foobar").unwrap();
assert!(pre < longer);
}
#[test]
fn wildcard_helper() {
assert!(Label::from_str("*").unwrap().is_wildcard());
assert!(!Label::from_str("foo").unwrap().is_wildcard());
}
#[test]
fn unchecked_constructor_layout() {
let s = "valid";
let l = unsafe { Label::from_str_unchecked(s) };
assert_eq!(l.as_str(), s);
assert_eq!(l.len(), s.len());
}
#[test]
fn label_byte_set_matches_predicate() {
for b in 0u8..=255 {
let expected = b.is_ascii_alphanumeric() || b == b'_' || b == b'-';
assert_eq!(
crate::byte_sets::is_label_byte(b),
expected,
"byte 0x{b:02x} ({}) — LUT disagreed with predicate",
if b.is_ascii_graphic() {
format!("{:?}", b as char)
} else {
"non-graphic".to_owned()
}
);
}
}
}