1use core::fmt;
2
3#[derive(Clone, Copy, Debug)]
4pub struct Error;
5
6impl From<fmt::Error> for Error {
7 fn from(_: fmt::Error) -> Self {
8 Self
9 }
10}
11
12pub type Result = core::result::Result<(), Error>;
13
14pub enum DisplayKind {
15 Simple,
16 Tabbed,
17 Multiline,
18}
19
20pub struct ZoneFileDisplay<'a, T: ?Sized> {
21 inner: &'a T,
22 kind: DisplayKind,
23}
24
25impl<T: ZonefileFmt + ?Sized> fmt::Display for ZoneFileDisplay<'_, T> {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self.kind {
28 DisplayKind::Simple => self
29 .inner
30 .fmt(&mut SimpleWriter::new(f))
31 .map_err(|_| fmt::Error),
32 DisplayKind::Tabbed => self
33 .inner
34 .fmt(&mut TabbedWriter::new(f))
35 .map_err(|_| fmt::Error),
36 DisplayKind::Multiline => self
37 .inner
38 .fmt(&mut MultiLineWriter::new(f))
39 .map_err(|_| fmt::Error),
40 }
41 }
42}
43
44pub trait ZonefileFmt {
46 fn fmt(&self, p: &mut impl Formatter) -> Result;
50
51 fn display_zonefile(
56 &self,
57 display_kind: DisplayKind,
58 ) -> ZoneFileDisplay<'_, Self> {
59 ZoneFileDisplay {
60 inner: self,
61 kind: display_kind,
62 }
63 }
64}
65
66impl<T: ZonefileFmt> ZonefileFmt for &T {
67 fn fmt(&self, p: &mut impl Formatter) -> Result {
68 T::fmt(self, p)
69 }
70}
71
72pub trait FormatWriter: Sized {
74 fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result;
76
77 fn begin_block(&mut self) -> Result;
82
83 fn end_block(&mut self) -> Result;
88
89 fn fmt_comment(&mut self, args: fmt::Arguments<'_>) -> Result;
93
94 fn newline(&mut self) -> Result;
96}
97
98struct SimpleWriter<W> {
103 first: bool,
104 writer: W,
105}
106
107impl<W: fmt::Write> SimpleWriter<W> {
108 fn new(writer: W) -> Self {
109 Self {
110 first: true,
111 writer,
112 }
113 }
114}
115
116impl<W: fmt::Write> FormatWriter for SimpleWriter<W> {
117 fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result {
118 if !self.first {
119 self.writer.write_char(' ')?;
120 }
121 self.first = false;
122 self.writer.write_fmt(args)?;
123 Ok(())
124 }
125
126 fn begin_block(&mut self) -> Result {
127 Ok(())
128 }
129
130 fn end_block(&mut self) -> Result {
131 Ok(())
132 }
133
134 fn fmt_comment(&mut self, _args: fmt::Arguments<'_>) -> Result {
135 Ok(())
136 }
137
138 fn newline(&mut self) -> Result {
139 self.writer.write_char('\n')?;
140 self.first = true;
141 Ok(())
142 }
143}
144
145struct TabbedWriter<W> {
147 first: bool,
148 first_block: bool,
149 blocks: usize,
150 writer: W,
151}
152
153impl<W> TabbedWriter<W> {
154 fn new(writer: W) -> Self {
155 Self {
156 first: true,
157 first_block: true,
158 blocks: 0,
159 writer,
160 }
161 }
162}
163
164impl<W: fmt::Write> FormatWriter for TabbedWriter<W> {
165 fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result {
166 if !self.first {
167 let c = if self.blocks == 0 {
168 '\t'
169 } else if self.first_block {
170 self.first_block = false;
171 '\t'
172 } else {
173 ' '
174 };
175 self.writer.write_char(c)?;
176 }
177 self.first = false;
178 self.first_block = false;
179 self.writer.write_fmt(args)?;
180 Ok(())
181 }
182
183 fn begin_block(&mut self) -> Result {
184 self.blocks += 1;
185
186 if self.blocks == 1 {
188 self.first_block = true;
189 }
190
191 Ok(())
192 }
193
194 fn end_block(&mut self) -> Result {
195 self.blocks -= 1;
196 Ok(())
197 }
198
199 fn fmt_comment(&mut self, _args: fmt::Arguments<'_>) -> Result {
200 Ok(())
201 }
202
203 fn newline(&mut self) -> Result {
204 self.writer.write_char('\n')?;
205 self.first = true;
206 self.first_block = true;
207
208 debug_assert_eq!(self.blocks, 0);
209
210 Ok(())
211 }
212}
213
214struct MultiLineWriter<W> {
215 current_column: usize,
216 block_indent: Option<usize>,
217 first: bool,
218 writer: W,
219}
220
221impl<W> MultiLineWriter<W> {
222 fn new(writer: W) -> Self {
223 Self {
224 first: true,
225 current_column: 0,
226 block_indent: None,
227 writer,
228 }
229 }
230}
231
232impl<W: fmt::Write> FormatWriter for MultiLineWriter<W> {
233 fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result {
234 use fmt::Write;
235 if !self.first {
236 self.write_str(" ")?;
237 }
238 self.first = false;
239 self.write_fmt(args)?;
240 Ok(())
241 }
242
243 fn begin_block(&mut self) -> Result {
244 self.fmt_token(format_args!("("))?;
245 self.block_indent = Some(self.current_column + 1);
246 Ok(())
247 }
248
249 fn end_block(&mut self) -> Result {
250 self.block_indent = None;
251 self.fmt_token(format_args!(")"))
252 }
253
254 fn fmt_comment(&mut self, args: fmt::Arguments<'_>) -> Result {
255 if self.block_indent.is_some() {
256 write!(self.writer, "\t; {}", args)?;
257 self.newline()
258 } else {
259 Ok(())
262 }
263 }
264
265 fn newline(&mut self) -> Result {
266 use fmt::Write;
267 self.writer.write_char('\n')?;
268 self.current_column = 0;
269 if let Some(x) = self.block_indent {
270 for _ in 0..x {
271 self.write_str(" ")?;
272 }
273 }
274 self.first = true;
275 Ok(())
276 }
277}
278
279impl<W: fmt::Write> fmt::Write for MultiLineWriter<W> {
280 fn write_str(&mut self, x: &str) -> fmt::Result {
281 self.current_column += x.len();
282 self.writer.write_str(x)
283 }
284}
285
286pub trait Formatter: FormatWriter {
288 fn block(&mut self, f: impl Fn(&mut Self) -> Result) -> Result {
292 self.begin_block()?;
293 f(self)?;
294 self.end_block()
295 }
296
297 fn write_token(&mut self, token: impl fmt::Display) -> Result {
299 self.fmt_token(format_args!("{token}"))
300 }
301
302 fn write_show(&mut self, item: impl ZonefileFmt) -> Result {
304 item.fmt(self)
305 }
306
307 fn write_comment(&mut self, s: impl fmt::Display) -> Result {
311 self.fmt_comment(format_args!("{s}"))
312 }
313}
314
315impl<T: FormatWriter> Formatter for T {}
316
317#[cfg(all(test, feature = "std"))]
318mod test {
319 use std::string::ToString as _;
320 use std::vec::Vec;
321
322 use crate::base::iana::{Class, DigestAlgorithm, SecurityAlgorithm};
323 use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt};
324 use crate::base::{Name, Record, Ttl};
325 use crate::rdata::{Cds, Cname, Ds, Mx, Txt, A};
326
327 fn create_record<Data>(data: Data) -> Record<&'static Name<[u8]>, Data> {
328 let name = Name::from_slice(b"\x07example\x03com\x00").unwrap();
329 Record::new(name, Class::IN, Ttl::from_secs(3600), data)
330 }
331
332 #[test]
333 fn a_record() {
334 let record = create_record(A::new("128.140.76.106".parse().unwrap()));
335 assert_eq!(
336 "example.com. 3600 IN A 128.140.76.106",
337 record.display_zonefile(DisplayKind::Simple).to_string()
338 );
339 }
340
341 #[test]
342 fn cname_record() {
343 let record = create_record(Cname::new(
344 Name::from_slice(b"\x07example\x03com\x00").unwrap(),
345 ));
346 assert_eq!(
347 "example.com. 3600 IN CNAME example.com.",
348 record.display_zonefile(DisplayKind::Simple).to_string()
349 );
350 }
351
352 #[test]
353 fn ds_key_record() {
354 let record = create_record(
355 Ds::new(
356 5414,
357 SecurityAlgorithm::ED25519,
358 DigestAlgorithm::SHA256,
359 &[0xDE, 0xAD, 0xBE, 0xEF],
360 )
361 .unwrap(),
362 );
363 assert_eq!(
364 "example.com. 3600 IN DS 5414 15 2 DEADBEEF",
365 record.display_zonefile(DisplayKind::Simple).to_string()
366 );
367 assert_eq!(
368 "example.com.\t3600\tIN\tDS\t5414 15 2 DEADBEEF",
369 record.display_zonefile(DisplayKind::Tabbed).to_string()
370 );
371 assert_eq!(
372 [
373 "example.com. 3600 IN DS ( 5414\t; key tag",
374 " 15\t; algorithm: ED25519",
375 " 2\t; digest type: SHA-256",
376 " DEADBEEF )",
377 ]
378 .join("\n"),
379 record.display_zonefile(DisplayKind::Multiline).to_string()
380 );
381 }
382
383 #[test]
384 fn only_ds_data() {
385 let rdata = Ds::new(
386 5414,
387 SecurityAlgorithm::ED25519,
388 DigestAlgorithm::SHA256,
389 &[0xDE, 0xAD, 0xBE, 0xEF],
390 )
391 .unwrap();
392
393 assert_eq!(
395 "5414 15 2 DEADBEEF",
396 rdata.display_zonefile(DisplayKind::Tabbed).to_string()
397 );
398 }
399
400 #[test]
401 fn cds_record() {
402 let record = create_record(
403 Cds::new(
404 5414,
405 SecurityAlgorithm::ED25519,
406 DigestAlgorithm::SHA256,
407 &[0xDE, 0xAD, 0xBE, 0xEF],
408 )
409 .unwrap(),
410 );
411 assert_eq!(
412 "example.com. 3600 IN CDS 5414 15 2 DEADBEEF",
413 record.display_zonefile(DisplayKind::Simple).to_string()
414 );
415 }
416
417 #[test]
418 fn mx_record() {
419 let record = create_record(Mx::new(
420 20,
421 Name::from_slice(b"\x07example\x03com\x00").unwrap(),
422 ));
423 assert_eq!(
424 "example.com. 3600 IN MX 20 example.com.",
425 record.display_zonefile(DisplayKind::Simple).to_string()
426 );
427 }
428
429 #[test]
430 fn txt_record() {
431 let record = create_record(Txt::<Vec<u8>>::build_from_slice(
432 b"this is a string that is longer than 255 characters if I just \
433 type a little bit more to pad this test out and then write some \
434 more like a silly monkey with a typewriter accidentally writing \
435 some shakespeare along the way but it feels like I have to type \
436 even longer to hit that limit!\
437 ").unwrap());
438 assert_eq!(
439 "example.com. 3600 IN TXT \
440 \"this is a string that is longer than 255 characters if I just \
441 type a little bit more to pad this test out and then write some \
442 more like a silly monkey with a typewriter accidentally writing \
443 some shakespeare along the way but it feels like I have to type \
444 e\" \"ven longer to hit that limit!\"",
445 record.display_zonefile(DisplayKind::Simple).to_string()
446 );
447 }
448
449 #[test]
450 fn hinfo_record() {
451 use crate::rdata::Hinfo;
452 let record = create_record(Hinfo::<Vec<u8>>::new(
453 "Windows".parse().unwrap(),
454 "Windows Server".parse().unwrap(),
455 ));
456 assert_eq!(
457 "example.com. 3600 IN HINFO \"Windows\" \"Windows Server\"",
458 record.display_zonefile(DisplayKind::Simple).to_string()
459 );
460 }
461
462 #[test]
463 fn naptr_record() {
464 use crate::rdata::Naptr;
465 let record = create_record(Naptr::<Vec<u8>, &Name<[u8]>>::new(
466 100,
467 50,
468 "a".parse().unwrap(),
469 "z3950+N2L+N2C".parse().unwrap(),
470 r#"!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i"#.parse().unwrap(),
471 Name::from_slice(b"\x09cidserver\x07example\x03com\x00").unwrap(),
472 ));
473 assert_eq!(
474 r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."#,
475 record.display_zonefile(DisplayKind::Simple).to_string()
476 );
477 }
478
479 #[test]
480 fn tabbed() {
481 let record = create_record(
482 Cds::new(
483 5414,
484 SecurityAlgorithm::ED25519,
485 DigestAlgorithm::SHA256,
486 &[0xDE, 0xAD, 0xBE, 0xEF],
487 )
488 .unwrap(),
489 );
490
491 assert_eq!(
494 "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF",
495 record.display_zonefile(DisplayKind::Tabbed).to_string()
496 );
497 }
498}