domain/base/
zonefile_fmt.rs

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
44/// Show a value as zonefile format
45pub trait ZonefileFmt {
46    /// Format the item as zonefile fmt into a [`fmt::Formatter`]
47    ///
48    /// This method is meant for use in a `fmt::Display` implementation.
49    fn fmt(&self, p: &mut impl Formatter) -> Result;
50
51    /// Display the item as a zonefile
52    ///
53    /// The returned object will be displayed as zonefile when printed or
54    /// written using `fmt::Display`.
55    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
72/// Determines how a zonefile is formatted
73pub trait FormatWriter: Sized {
74    /// Push a token to the zonefile
75    fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result;
76
77    /// Start a block of grouped tokens
78    ///
79    /// This might push `'('` to the zonefile, but may be ignored by the
80    /// `PresentationWriter`.
81    fn begin_block(&mut self) -> Result;
82
83    /// End a block of grouped tokens
84    ///
85    /// This might push `'('` to the zonefile, but may be ignored by the
86    /// `PresentationWriter`.
87    fn end_block(&mut self) -> Result;
88
89    /// Write a comment
90    ///
91    /// This may be ignored.
92    fn fmt_comment(&mut self, args: fmt::Arguments<'_>) -> Result;
93
94    /// End the current record and start a new line
95    fn newline(&mut self) -> Result;
96}
97
98/// The simplest possible zonefile writer
99///
100/// This writer does not do any alignment, comments and squeezes each record
101/// onto a single line.
102struct 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
145/// A single line writer that puts tabs between ungrouped tokens
146struct 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 we enter the first level of blocks, we do 1 more tab
187        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            // a comment should not have been allowed
260            // so ignore it
261            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
286/// A more structured wrapper around a [`PresentationWriter`]
287pub trait Formatter: FormatWriter {
288    /// Start a sequence of grouped tokens
289    ///
290    /// The block might be surrounded by `(` and `)` in a multiline format.
291    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    /// Push a token
298    fn write_token(&mut self, token: impl fmt::Display) -> Result {
299        self.fmt_token(format_args!("{token}"))
300    }
301
302    /// Call the `show` method on `item` with this `Presenter`
303    fn write_show(&mut self, item: impl ZonefileFmt) -> Result {
304        item.fmt(self)
305    }
306
307    /// Write a comment
308    ///
309    /// This may be ignored.
310    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        // No tabs because it is a single block
394        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        // The name, ttl, class and rtype should be separated by \t, but the
492        // rdata shouldn't.
493        assert_eq!(
494            "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF",
495            record.display_zonefile(DisplayKind::Tabbed).to_string()
496        );
497    }
498}