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.writer.write_fmt(args)?;
179        Ok(())
180    }
181
182    fn begin_block(&mut self) -> Result {
183        self.blocks += 1;
184        Ok(())
185    }
186
187    fn end_block(&mut self) -> Result {
188        self.blocks -= 1;
189        Ok(())
190    }
191
192    fn fmt_comment(&mut self, _args: fmt::Arguments<'_>) -> Result {
193        Ok(())
194    }
195
196    fn newline(&mut self) -> Result {
197        self.writer.write_char('\n')?;
198        self.first = true;
199
200        debug_assert_eq!(self.blocks, 0);
201
202        Ok(())
203    }
204}
205
206struct MultiLineWriter<W> {
207    current_column: usize,
208    block_indent: Option<usize>,
209    first: bool,
210    writer: W,
211}
212
213impl<W> MultiLineWriter<W> {
214    fn new(writer: W) -> Self {
215        Self {
216            first: true,
217            current_column: 0,
218            block_indent: None,
219            writer,
220        }
221    }
222}
223
224impl<W: fmt::Write> FormatWriter for MultiLineWriter<W> {
225    fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result {
226        use fmt::Write;
227        if !self.first {
228            self.write_str(" ")?;
229        }
230        self.first = false;
231        self.write_fmt(args)?;
232        Ok(())
233    }
234
235    fn begin_block(&mut self) -> Result {
236        self.fmt_token(format_args!("("))?;
237        self.block_indent = Some(self.current_column + 1);
238        Ok(())
239    }
240
241    fn end_block(&mut self) -> Result {
242        self.block_indent = None;
243        self.fmt_token(format_args!(")"))
244    }
245
246    fn fmt_comment(&mut self, args: fmt::Arguments<'_>) -> Result {
247        if self.block_indent.is_some() {
248            write!(self.writer, "\t; {}", args)?;
249            self.newline()
250        } else {
251            // a comment should not have been allowed
252            // so ignore it
253            Ok(())
254        }
255    }
256
257    fn newline(&mut self) -> Result {
258        use fmt::Write;
259        self.writer.write_char('\n')?;
260        self.current_column = 0;
261        if let Some(x) = self.block_indent {
262            for _ in 0..x {
263                self.write_str(" ")?;
264            }
265        }
266        self.first = true;
267        Ok(())
268    }
269}
270
271impl<W: fmt::Write> fmt::Write for MultiLineWriter<W> {
272    fn write_str(&mut self, x: &str) -> fmt::Result {
273        self.current_column += x.len();
274        self.writer.write_str(x)
275    }
276}
277
278/// A more structured wrapper around a [`PresentationWriter`]
279pub trait Formatter: FormatWriter {
280    /// Start a sequence of grouped tokens
281    ///
282    /// The block might be surrounded by `(` and `)` in a multiline format.
283    fn block(&mut self, f: impl Fn(&mut Self) -> Result) -> Result {
284        self.begin_block()?;
285        f(self)?;
286        self.end_block()
287    }
288
289    /// Push a token
290    fn write_token(&mut self, token: impl fmt::Display) -> Result {
291        self.fmt_token(format_args!("{token}"))
292    }
293
294    /// Call the `show` method on `item` with this `Presenter`
295    fn write_show(&mut self, item: impl ZonefileFmt) -> Result {
296        item.fmt(self)
297    }
298
299    /// Write a comment
300    ///
301    /// This may be ignored.
302    fn write_comment(&mut self, s: impl fmt::Display) -> Result {
303        self.fmt_comment(format_args!("{s}"))
304    }
305}
306
307impl<T: FormatWriter> Formatter for T {}
308
309#[cfg(all(test, feature = "std"))]
310mod test {
311    use std::string::ToString as _;
312    use std::vec::Vec;
313
314    use crate::base::iana::{Class, DigestAlgorithm, SecurityAlgorithm};
315    use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt};
316    use crate::base::{Name, Record, Ttl};
317    use crate::rdata::{Cds, Cname, Ds, Mx, Txt, A};
318
319    fn create_record<Data>(data: Data) -> Record<&'static Name<[u8]>, Data> {
320        let name = Name::from_slice(b"\x07example\x03com\x00").unwrap();
321        Record::new(name, Class::IN, Ttl::from_secs(3600), data)
322    }
323
324    #[test]
325    fn a_record() {
326        let record = create_record(A::new("128.140.76.106".parse().unwrap()));
327        assert_eq!(
328            "example.com. 3600 IN A 128.140.76.106",
329            record.display_zonefile(DisplayKind::Simple).to_string()
330        );
331    }
332
333    #[test]
334    fn cname_record() {
335        let record = create_record(Cname::new(
336            Name::from_slice(b"\x07example\x03com\x00").unwrap(),
337        ));
338        assert_eq!(
339            "example.com. 3600 IN CNAME example.com.",
340            record.display_zonefile(DisplayKind::Simple).to_string()
341        );
342    }
343
344    #[test]
345    fn ds_key_record() {
346        let record = create_record(
347            Ds::new(
348                5414,
349                SecurityAlgorithm::ED25519,
350                DigestAlgorithm::SHA256,
351                &[0xDE, 0xAD, 0xBE, 0xEF],
352            )
353            .unwrap(),
354        );
355        assert_eq!(
356            "example.com. 3600 IN DS 5414 15 2 DEADBEEF",
357            record.display_zonefile(DisplayKind::Simple).to_string()
358        );
359        assert_eq!(
360            [
361                "example.com. 3600 IN DS ( 5414\t; key tag",
362                "                          15\t; algorithm: ED25519",
363                "                          2\t; digest type: SHA-256",
364                "                          DEADBEEF )",
365            ]
366            .join("\n"),
367            record.display_zonefile(DisplayKind::Multiline).to_string()
368        );
369    }
370
371    #[test]
372    fn cds_record() {
373        let record = create_record(
374            Cds::new(
375                5414,
376                SecurityAlgorithm::ED25519,
377                DigestAlgorithm::SHA256,
378                &[0xDE, 0xAD, 0xBE, 0xEF],
379            )
380            .unwrap(),
381        );
382        assert_eq!(
383            "example.com. 3600 IN CDS 5414 15 2 DEADBEEF",
384            record.display_zonefile(DisplayKind::Simple).to_string()
385        );
386    }
387
388    #[test]
389    fn mx_record() {
390        let record = create_record(Mx::new(
391            20,
392            Name::from_slice(b"\x07example\x03com\x00").unwrap(),
393        ));
394        assert_eq!(
395            "example.com. 3600 IN MX 20 example.com.",
396            record.display_zonefile(DisplayKind::Simple).to_string()
397        );
398    }
399
400    #[test]
401    fn txt_record() {
402        let record = create_record(Txt::<Vec<u8>>::build_from_slice(
403            b"this is a string that is longer than 255 characters if I just \
404            type a little bit more to pad this test out and then write some \
405            more like a silly monkey with a typewriter accidentally writing \
406            some shakespeare along the way but it feels like I have to type \
407            even longer to hit that limit!\
408        ").unwrap());
409        assert_eq!(
410            "example.com. 3600 IN TXT \
411            \"this is a string that is longer than 255 characters if I just \
412            type a little bit more to pad this test out and then write some \
413            more like a silly monkey with a typewriter accidentally writing \
414            some shakespeare along the way but it feels like I have to type \
415            e\" \"ven longer to hit that limit!\"",
416            record.display_zonefile(DisplayKind::Simple).to_string()
417        );
418    }
419
420    #[test]
421    fn hinfo_record() {
422        use crate::rdata::Hinfo;
423        let record = create_record(Hinfo::<Vec<u8>>::new(
424            "Windows".parse().unwrap(),
425            "Windows Server".parse().unwrap(),
426        ));
427        assert_eq!(
428            "example.com. 3600 IN HINFO \"Windows\" \"Windows Server\"",
429            record.display_zonefile(DisplayKind::Simple).to_string()
430        );
431    }
432
433    #[test]
434    fn naptr_record() {
435        use crate::rdata::Naptr;
436        let record = create_record(Naptr::<Vec<u8>, &Name<[u8]>>::new(
437            100,
438            50,
439            "a".parse().unwrap(),
440            "z3950+N2L+N2C".parse().unwrap(),
441            r#"!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i"#.parse().unwrap(),
442            Name::from_slice(b"\x09cidserver\x07example\x03com\x00").unwrap(),
443        ));
444        assert_eq!(
445            r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."#,
446            record.display_zonefile(DisplayKind::Simple).to_string()
447        );
448    }
449
450    #[test]
451    fn tabbed() {
452        let record = create_record(
453            Cds::new(
454                5414,
455                SecurityAlgorithm::ED25519,
456                DigestAlgorithm::SHA256,
457                &[0xDE, 0xAD, 0xBE, 0xEF],
458            )
459            .unwrap(),
460        );
461
462        // The name, ttl, class and rtype should be separated by \t, but the
463        // rdata shouldn't.
464        assert_eq!(
465            "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF",
466            record.display_zonefile(DisplayKind::Tabbed).to_string()
467        );
468    }
469}