Skip to main content

rama_net/address/domain/
builder.rs

1//! [`DomainBuilder`] — incrementally construct a [`Domain`] from labels,
2//! enforcing per-label and total-length invariants at push time.
3
4use core::fmt;
5
6use super::label::validate_label_bytes;
7use super::{Domain, DomainLabels, Label, LabelError, MAX_NAME_LEN};
8
9use rama_core::bytes::BytesMut;
10
11/// Builder for a [`Domain`].
12///
13/// Labels are pushed in DNS-natural order: leftmost (most specific) first,
14/// rightmost (TLD) last. The builder maintains the [`Domain`] invariant
15/// after every successful push.
16///
17/// Backed by [`BytesMut`], producing a [`Bytes`](rama_core::bytes::Bytes)-backed
18/// [`Domain`] on [`finish`](Self::finish).
19///
20/// # Example
21///
22/// ```
23/// use rama_net::address::domain::DomainBuilder;
24///
25/// let mut b = DomainBuilder::new();
26/// b.push_label("www").unwrap();
27/// b.push_label("example").unwrap();
28/// b.push_label("com").unwrap();
29/// let d = b.finish().unwrap();
30/// assert_eq!(d.as_str(), "www.example.com");
31/// ```
32#[derive(Debug, Default)]
33pub struct DomainBuilder {
34    buf: BytesMut,
35    label_count: usize,
36    starts_with_wildcard: bool,
37}
38
39impl DomainBuilder {
40    /// Creates an empty builder.
41    #[must_use]
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Returns the number of labels currently in the builder.
47    #[must_use]
48    pub fn label_count(&self) -> usize {
49        self.label_count
50    }
51
52    /// Returns `true` if no labels have been pushed yet.
53    #[must_use]
54    pub fn is_empty(&self) -> bool {
55        self.buf.is_empty()
56    }
57
58    /// Returns the current length in bytes (including separating dots).
59    #[must_use]
60    pub fn len(&self) -> usize {
61        self.buf.len()
62    }
63
64    /// Push a single label.
65    ///
66    /// `label` must satisfy [`Label`]'s invariants. The total name length
67    /// after the push (including the joining dot) must not exceed
68    /// `MAX_NAME_LEN` (253).
69    ///
70    /// # Errors
71    ///
72    /// Returns [`PushError`] if the label or resulting name length is invalid.
73    pub fn push_label(&mut self, label: &str) -> Result<&mut Self, PushError> {
74        validate_label_bytes(label.as_bytes()).map_err(PushError::from_label)?;
75        self.push_validated_label(label)
76    }
77
78    /// Push an already-validated [`Label`] reference.
79    ///
80    /// Still enforces the total name length cap.
81    ///
82    /// # Errors
83    ///
84    /// Returns a too-long [`PushError`] if the resulting name length would
85    /// exceed `MAX_NAME_LEN`.
86    pub fn push(&mut self, label: &Label) -> Result<&mut Self, PushError> {
87        self.push_validated_label(label.as_str())
88    }
89
90    fn push_validated_label(&mut self, label: &str) -> Result<&mut Self, PushError> {
91        // Wildcard `*` is only valid as the leftmost label. The label-level
92        // validator accepts `"*"` standalone, so the positional rule lives
93        // here in the builder.
94        let is_wildcard = label == "*";
95        if is_wildcard && !self.is_empty() {
96            return Err(PushError::misplaced_wildcard());
97        }
98
99        let added = if self.is_empty() {
100            label.len()
101        } else {
102            label.len() + 1
103        };
104        let new_len = self.buf.len() + added;
105        if new_len > MAX_NAME_LEN {
106            return Err(PushError::too_long(new_len));
107        }
108        if !self.is_empty() {
109            self.buf.extend_from_slice(b".");
110        } else {
111            self.starts_with_wildcard = is_wildcard;
112        }
113        self.buf.extend_from_slice(label.as_bytes());
114        self.label_count += 1;
115        Ok(self)
116    }
117
118    /// Push every label from `it` in iteration order.
119    ///
120    /// # Errors
121    ///
122    /// Returns the first [`PushError`] encountered. On error, the builder
123    /// retains the labels that pushed successfully — the caller may still
124    /// inspect or discard it.
125    pub fn push_labels<'a, I: IntoIterator<Item = &'a Label>>(
126        &mut self,
127        it: I,
128    ) -> Result<&mut Self, PushError> {
129        for l in it {
130            self.push(l)?;
131        }
132        Ok(self)
133    }
134
135    /// Append every label from another label-aware value (e.g. a [`Domain`]
136    /// or [`Host`](super::super::Host)).
137    ///
138    /// # Errors
139    ///
140    /// Returns the first [`PushError`] encountered.
141    pub fn append<D: DomainLabels + ?Sized>(&mut self, other: &D) -> Result<&mut Self, PushError> {
142        self.push_labels(other.labels())
143    }
144
145    /// Parse `dotted` as a sequence of labels separated by `.`, ignoring
146    /// empty segments (i.e. leading and trailing FQDN dots are accepted).
147    ///
148    /// # Errors
149    ///
150    /// Returns [`PushError`] on the first invalid label or length overflow.
151    pub fn push_label_segments(&mut self, dotted: &str) -> Result<&mut Self, PushError> {
152        for part in dotted.split('.') {
153            if part.is_empty() {
154                continue;
155            }
156            self.push_label(part)?;
157        }
158        Ok(self)
159    }
160
161    /// Consume the builder and produce a [`Domain`].
162    ///
163    /// # Errors
164    ///
165    /// Returns a [`PushError`] if the builder is empty, or if the only pushed
166    /// label is the bare wildcard `"*"` (which is never a valid standalone
167    /// domain).
168    pub fn finish(self) -> Result<Domain, PushError> {
169        if self.label_count == 0 {
170            return Err(PushError::empty());
171        }
172        if self.label_count == 1 && self.starts_with_wildcard {
173            return Err(PushError::misplaced_wildcard());
174        }
175        // Safety: builder maintained the Domain invariant at every push.
176        Ok(unsafe { Domain::from_maybe_borrowed_unchecked(self.buf.freeze()) })
177    }
178}
179
180/// Error returned by [`DomainBuilder`] when a push would violate the
181/// [`Domain`] invariant.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct PushError(PushErrorKind);
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186enum PushErrorKind {
187    Empty,
188    Label(LabelError),
189    TooLong { len: usize },
190    MisplacedWildcard,
191}
192
193impl PushError {
194    #[inline]
195    fn empty() -> Self {
196        Self(PushErrorKind::Empty)
197    }
198    #[inline]
199    fn from_label(e: LabelError) -> Self {
200        Self(PushErrorKind::Label(e))
201    }
202    #[inline]
203    fn too_long(len: usize) -> Self {
204        Self(PushErrorKind::TooLong { len })
205    }
206    #[inline]
207    fn misplaced_wildcard() -> Self {
208        Self(PushErrorKind::MisplacedWildcard)
209    }
210
211    /// Returns the underlying [`LabelError`] if this is a label-validation
212    /// failure.
213    #[must_use]
214    pub fn as_label_error(&self) -> Option<&LabelError> {
215        match &self.0 {
216            PushErrorKind::Label(e) => Some(e),
217            _ => None,
218        }
219    }
220}
221
222impl fmt::Display for PushError {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        match &self.0 {
225            PushErrorKind::Empty => f.write_str("no labels pushed to domain builder"),
226            PushErrorKind::Label(e) => write!(f, "invalid label: {e}"),
227            PushErrorKind::TooLong { len } => write!(
228                f,
229                "domain name would be {len} bytes long, max is {MAX_NAME_LEN}"
230            ),
231            PushErrorKind::MisplacedWildcard => f.write_str(
232                "wildcard label '*' is only valid as the leftmost label and never alone",
233            ),
234        }
235    }
236}
237
238impl core::error::Error for PushError {
239    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
240        match &self.0 {
241            PushErrorKind::Label(e) => Some(e),
242            _ => None,
243        }
244    }
245}
246
247impl From<LabelError> for PushError {
248    fn from(e: LabelError) -> Self {
249        Self::from_label(e)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::super::Domain;
256    use super::*;
257
258    #[test]
259    fn build_single_label() {
260        let mut b = DomainBuilder::new();
261        b.push_label("com").unwrap();
262        assert_eq!(b.label_count(), 1);
263        let d = b.finish().unwrap();
264        assert_eq!(d.as_str(), "com");
265    }
266
267    #[test]
268    fn build_multi_label() {
269        let mut b = DomainBuilder::new();
270        b.push_label("www").unwrap();
271        b.push_label("example").unwrap();
272        b.push_label("com").unwrap();
273        assert_eq!(b.label_count(), 3);
274        let d = b.finish().unwrap();
275        assert_eq!(d.as_str(), "www.example.com");
276    }
277
278    #[test]
279    fn build_wildcard() {
280        let mut b = DomainBuilder::new();
281        b.push_label("*").unwrap();
282        b.push_label("example").unwrap();
283        b.push_label("com").unwrap();
284        let d = b.finish().unwrap();
285        assert_eq!(d.as_str(), "*.example.com");
286        assert!(d.is_wildcard());
287    }
288
289    #[test]
290    fn append_domain() {
291        let parent = Domain::from_static("example.com");
292        let mut b = DomainBuilder::new();
293        b.push_label("www").unwrap();
294        b.append(&parent).unwrap();
295        assert_eq!(b.finish().unwrap().as_str(), "www.example.com");
296    }
297
298    #[test]
299    fn push_label_segments_handles_dots() {
300        let mut b = DomainBuilder::new();
301        b.push_label_segments("a.b.c").unwrap();
302        assert_eq!(b.finish().unwrap().as_str(), "a.b.c");
303
304        // leading/trailing/duplicate dots are squashed (split + filter empty)
305        let mut b = DomainBuilder::new();
306        b.push_label_segments(".a.b.").unwrap();
307        assert_eq!(b.finish().unwrap().as_str(), "a.b");
308    }
309
310    #[test]
311    fn rejects_invalid_label() {
312        let mut b = DomainBuilder::new();
313        let err = b.push_label("-bad").unwrap_err();
314        assert!(err.as_label_error().is_some());
315        assert!(b.is_empty(), "builder is unchanged after failed push");
316    }
317
318    #[test]
319    fn rejects_total_length_overflow() {
320        // 63 + 1 + 63 + 1 + 63 + 1 + 63 = 255 > 253. Three 63-byte labels fit
321        // (191), a fourth doesn't.
322        let label63 = "a".repeat(63);
323        let mut b = DomainBuilder::new();
324        b.push_label(&label63).unwrap();
325        b.push_label(&label63).unwrap();
326        b.push_label(&label63).unwrap();
327        let err = b.push_label(&label63).unwrap_err();
328        assert!(format!("{err}").contains("max is 253"));
329    }
330
331    #[test]
332    fn finish_empty_returns_err() {
333        let b = DomainBuilder::new();
334        let err = b.finish().unwrap_err();
335        assert!(format!("{err}").contains("no labels"));
336    }
337
338    #[test]
339    fn push_already_validated_label() {
340        let l = Label::from_str("example").unwrap();
341        let mut b = DomainBuilder::new();
342        b.push(l).unwrap();
343        b.push_label("com").unwrap();
344        assert_eq!(b.finish().unwrap().as_str(), "example.com");
345    }
346
347    #[test]
348    fn rejects_wildcard_at_non_leftmost_position() {
349        // Pushed after a regular label.
350        let mut b = DomainBuilder::new();
351        b.push_label("example").unwrap();
352        let err = b.push_label("*").unwrap_err();
353        assert!(
354            format!("{err}").contains("wildcard"),
355            "expected wildcard mention, got: {err}"
356        );
357
358        // Pushed via push_label_segments.
359        let mut b = DomainBuilder::new();
360        let err = b.push_label_segments("x.*.com").unwrap_err();
361        assert!(format!("{err}").contains("wildcard"), "got: {err}");
362
363        // Pushed via append (Domain whose first label is `*`).
364        let parent = Domain::from_static("*.example.com");
365        let mut b = DomainBuilder::new();
366        b.push_label("foo").unwrap();
367        let err = b.append(&parent).unwrap_err();
368        assert!(format!("{err}").contains("wildcard"), "got: {err}");
369    }
370
371    #[test]
372    fn accepts_wildcard_as_leftmost_label() {
373        let mut b = DomainBuilder::new();
374        b.push_label("*").unwrap();
375        b.push_label("example").unwrap();
376        b.push_label("com").unwrap();
377        let d = b.finish().unwrap();
378        assert_eq!(d.as_str(), "*.example.com");
379        // And the output reparses (no broken invariant).
380        Domain::try_from(d.as_str().to_owned()).expect("builder output reparses");
381    }
382
383    #[test]
384    fn rejects_bare_wildcard_on_finish() {
385        let mut b = DomainBuilder::new();
386        b.push_label("*").unwrap();
387        // Only one label, and it's `*` — not a valid domain on its own.
388        let err = b.finish().unwrap_err();
389        assert!(format!("{err}").contains("wildcard"), "got: {err}");
390    }
391
392    #[test]
393    fn build_matches_validating_parser() {
394        // The buffer the builder produces is parseable as a Domain — i.e. the
395        // builder's invariant matches the parser's.
396        let mut b = DomainBuilder::new();
397        b.push_label("a").unwrap();
398        b.push_label("_acme-challenge").unwrap();
399        b.push_label("example").unwrap();
400        b.push_label("com").unwrap();
401        let s = b.finish().unwrap().as_str().to_owned();
402        Domain::try_from(s).expect("builder output must reparse");
403    }
404}