rama_net/address/domain/
builder.rs1use 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#[derive(Debug, Default)]
33pub struct DomainBuilder {
34 buf: BytesMut,
35 label_count: usize,
36 starts_with_wildcard: bool,
37}
38
39impl DomainBuilder {
40 #[must_use]
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 #[must_use]
48 pub fn label_count(&self) -> usize {
49 self.label_count
50 }
51
52 #[must_use]
54 pub fn is_empty(&self) -> bool {
55 self.buf.is_empty()
56 }
57
58 #[must_use]
60 pub fn len(&self) -> usize {
61 self.buf.len()
62 }
63
64 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 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 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 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 pub fn append<D: DomainLabels + ?Sized>(&mut self, other: &D) -> Result<&mut Self, PushError> {
142 self.push_labels(other.labels())
143 }
144
145 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 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 Ok(unsafe { Domain::from_maybe_borrowed_unchecked(self.buf.freeze()) })
177 }
178}
179
180#[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 #[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 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 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 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 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 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 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 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 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}