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.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 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
278pub trait Formatter: FormatWriter {
280 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 fn write_token(&mut self, token: impl fmt::Display) -> Result {
291 self.fmt_token(format_args!("{token}"))
292 }
293
294 fn write_show(&mut self, item: impl ZonefileFmt) -> Result {
296 item.fmt(self)
297 }
298
299 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 assert_eq!(
465 "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF",
466 record.display_zonefile(DisplayKind::Tabbed).to_string()
467 );
468 }
469}