use core::fmt;
use super::label::validate_label_bytes;
use super::{Domain, DomainLabels, Label, LabelError, MAX_NAME_LEN};
use rama_core::bytes::BytesMut;
#[derive(Debug, Default)]
pub struct DomainBuilder {
buf: BytesMut,
label_count: usize,
starts_with_wildcard: bool,
}
impl DomainBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn label_count(&self) -> usize {
self.label_count
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.buf.len()
}
pub fn push_label(&mut self, label: &str) -> Result<&mut Self, PushError> {
validate_label_bytes(label.as_bytes()).map_err(PushError::from_label)?;
self.push_validated_label(label)
}
pub fn push(&mut self, label: &Label) -> Result<&mut Self, PushError> {
self.push_validated_label(label.as_str())
}
fn push_validated_label(&mut self, label: &str) -> Result<&mut Self, PushError> {
let is_wildcard = label == "*";
if is_wildcard && !self.is_empty() {
return Err(PushError::misplaced_wildcard());
}
let added = if self.is_empty() {
label.len()
} else {
label.len() + 1
};
let new_len = self.buf.len() + added;
if new_len > MAX_NAME_LEN {
return Err(PushError::too_long(new_len));
}
if !self.is_empty() {
self.buf.extend_from_slice(b".");
} else {
self.starts_with_wildcard = is_wildcard;
}
self.buf.extend_from_slice(label.as_bytes());
self.label_count += 1;
Ok(self)
}
pub fn push_labels<'a, I: IntoIterator<Item = &'a Label>>(
&mut self,
it: I,
) -> Result<&mut Self, PushError> {
for l in it {
self.push(l)?;
}
Ok(self)
}
pub fn append<D: DomainLabels + ?Sized>(&mut self, other: &D) -> Result<&mut Self, PushError> {
self.push_labels(other.labels())
}
pub fn push_label_segments(&mut self, dotted: &str) -> Result<&mut Self, PushError> {
for part in dotted.split('.') {
if part.is_empty() {
continue;
}
self.push_label(part)?;
}
Ok(self)
}
pub fn finish(self) -> Result<Domain, PushError> {
if self.label_count == 0 {
return Err(PushError::empty());
}
if self.label_count == 1 && self.starts_with_wildcard {
return Err(PushError::misplaced_wildcard());
}
Ok(unsafe { Domain::from_maybe_borrowed_unchecked(self.buf.freeze()) })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PushError(PushErrorKind);
#[derive(Debug, Clone, PartialEq, Eq)]
enum PushErrorKind {
Empty,
Label(LabelError),
TooLong { len: usize },
MisplacedWildcard,
}
impl PushError {
#[inline]
fn empty() -> Self {
Self(PushErrorKind::Empty)
}
#[inline]
fn from_label(e: LabelError) -> Self {
Self(PushErrorKind::Label(e))
}
#[inline]
fn too_long(len: usize) -> Self {
Self(PushErrorKind::TooLong { len })
}
#[inline]
fn misplaced_wildcard() -> Self {
Self(PushErrorKind::MisplacedWildcard)
}
#[must_use]
pub fn as_label_error(&self) -> Option<&LabelError> {
match &self.0 {
PushErrorKind::Label(e) => Some(e),
_ => None,
}
}
}
impl fmt::Display for PushError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
PushErrorKind::Empty => f.write_str("no labels pushed to domain builder"),
PushErrorKind::Label(e) => write!(f, "invalid label: {e}"),
PushErrorKind::TooLong { len } => write!(
f,
"domain name would be {len} bytes long, max is {MAX_NAME_LEN}"
),
PushErrorKind::MisplacedWildcard => f.write_str(
"wildcard label '*' is only valid as the leftmost label and never alone",
),
}
}
}
impl core::error::Error for PushError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match &self.0 {
PushErrorKind::Label(e) => Some(e),
_ => None,
}
}
}
impl From<LabelError> for PushError {
fn from(e: LabelError) -> Self {
Self::from_label(e)
}
}
#[cfg(test)]
mod tests {
use super::super::Domain;
use super::*;
#[test]
fn build_single_label() {
let mut b = DomainBuilder::new();
b.push_label("com").unwrap();
assert_eq!(b.label_count(), 1);
let d = b.finish().unwrap();
assert_eq!(d.as_str(), "com");
}
#[test]
fn build_multi_label() {
let mut b = DomainBuilder::new();
b.push_label("www").unwrap();
b.push_label("example").unwrap();
b.push_label("com").unwrap();
assert_eq!(b.label_count(), 3);
let d = b.finish().unwrap();
assert_eq!(d.as_str(), "www.example.com");
}
#[test]
fn build_wildcard() {
let mut b = DomainBuilder::new();
b.push_label("*").unwrap();
b.push_label("example").unwrap();
b.push_label("com").unwrap();
let d = b.finish().unwrap();
assert_eq!(d.as_str(), "*.example.com");
assert!(d.is_wildcard());
}
#[test]
fn append_domain() {
let parent = Domain::from_static("example.com");
let mut b = DomainBuilder::new();
b.push_label("www").unwrap();
b.append(&parent).unwrap();
assert_eq!(b.finish().unwrap().as_str(), "www.example.com");
}
#[test]
fn push_label_segments_handles_dots() {
let mut b = DomainBuilder::new();
b.push_label_segments("a.b.c").unwrap();
assert_eq!(b.finish().unwrap().as_str(), "a.b.c");
let mut b = DomainBuilder::new();
b.push_label_segments(".a.b.").unwrap();
assert_eq!(b.finish().unwrap().as_str(), "a.b");
}
#[test]
fn rejects_invalid_label() {
let mut b = DomainBuilder::new();
let err = b.push_label("-bad").unwrap_err();
assert!(err.as_label_error().is_some());
assert!(b.is_empty(), "builder is unchanged after failed push");
}
#[test]
fn rejects_total_length_overflow() {
let label63 = "a".repeat(63);
let mut b = DomainBuilder::new();
b.push_label(&label63).unwrap();
b.push_label(&label63).unwrap();
b.push_label(&label63).unwrap();
let err = b.push_label(&label63).unwrap_err();
assert!(format!("{err}").contains("max is 253"));
}
#[test]
fn finish_empty_returns_err() {
let b = DomainBuilder::new();
let err = b.finish().unwrap_err();
assert!(format!("{err}").contains("no labels"));
}
#[test]
fn push_already_validated_label() {
let l = Label::from_str("example").unwrap();
let mut b = DomainBuilder::new();
b.push(l).unwrap();
b.push_label("com").unwrap();
assert_eq!(b.finish().unwrap().as_str(), "example.com");
}
#[test]
fn rejects_wildcard_at_non_leftmost_position() {
let mut b = DomainBuilder::new();
b.push_label("example").unwrap();
let err = b.push_label("*").unwrap_err();
assert!(
format!("{err}").contains("wildcard"),
"expected wildcard mention, got: {err}"
);
let mut b = DomainBuilder::new();
let err = b.push_label_segments("x.*.com").unwrap_err();
assert!(format!("{err}").contains("wildcard"), "got: {err}");
let parent = Domain::from_static("*.example.com");
let mut b = DomainBuilder::new();
b.push_label("foo").unwrap();
let err = b.append(&parent).unwrap_err();
assert!(format!("{err}").contains("wildcard"), "got: {err}");
}
#[test]
fn accepts_wildcard_as_leftmost_label() {
let mut b = DomainBuilder::new();
b.push_label("*").unwrap();
b.push_label("example").unwrap();
b.push_label("com").unwrap();
let d = b.finish().unwrap();
assert_eq!(d.as_str(), "*.example.com");
Domain::try_from(d.as_str().to_owned()).expect("builder output reparses");
}
#[test]
fn rejects_bare_wildcard_on_finish() {
let mut b = DomainBuilder::new();
b.push_label("*").unwrap();
let err = b.finish().unwrap_err();
assert!(format!("{err}").contains("wildcard"), "got: {err}");
}
#[test]
fn build_matches_validating_parser() {
let mut b = DomainBuilder::new();
b.push_label("a").unwrap();
b.push_label("_acme-challenge").unwrap();
b.push_label("example").unwrap();
b.push_label("com").unwrap();
let s = b.finish().unwrap().as_str().to_owned();
Domain::try_from(s).expect("builder output must reparse");
}
}