diff --git a/src/base/show.rs b/src/base/show.rs index 68a4e825..ad23dc80 100644 --- a/src/base/show.rs +++ b/src/base/show.rs @@ -1,18 +1,17 @@ -//! Modelled roughly after `std::fmt::Display` - use core::fmt; #[derive(Clone, Copy, Debug)] pub struct Error; -impl From for Error { - fn from(value: std::fmt::Error) -> Self { +impl From for Error { + fn from(_: fmt::Error) -> Self { Self } } pub type Result = std::result::Result<(), Error>; +/// Show a value as zonefile format pub trait Show { fn show(&self, p: &mut Presenter<'_>) -> Result; @@ -23,15 +22,15 @@ pub trait Show { impl fmt::Display for ZoneFileDisplay<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.show(&mut Presenter { - writer: &mut SimplePresentationWriter::new(f) - }).map_err(|_| fmt::Error) + self.inner + .show(&mut Presenter { + writer: &mut SimplePresentationWriter::new(f), + }) + .map_err(|_| fmt::Error) } } - ZoneFileDisplay { - inner: self - } + ZoneFileDisplay { inner: self } } } @@ -39,19 +38,41 @@ impl Show for &T { fn show(&self, p: &mut Presenter<'_>) -> Result { T::show(self, p) } -} +} +/// Determines how a zonefile is formatted pub trait PresentationWriter { + /// Push a token to the zonefile fn fmt_token(&mut self, args: std::fmt::Arguments<'_>) -> Result; + + /// Start a block of grouped tokens + /// + /// This might push `'('` to the zonefile, but may be ignored by the + /// `PresentationWriter`. fn begin_block(&mut self) -> Result; + + /// End a block of grouped tokens + /// + /// This might push `'('` to the zonefile, but may be ignored by the + /// `PresentationWriter`. fn end_block(&mut self) -> Result; + + /// Write a comment + /// + /// This may be ignored. fn fmt_comment(&mut self, args: std::fmt::Arguments<'_>) -> Result; + + /// End the current record and start a new line fn newline(&mut self) -> Result; } +/// The simplest possible zonefile writer +/// +/// This writer does not do any alignment, comments and squeezes each record +/// onto a single line. struct SimplePresentationWriter<'a> { first: bool, - writer: &'a mut (dyn std::fmt::Write + 'a), + writer: &'a mut (dyn fmt::Write + 'a), } impl<'a> SimplePresentationWriter<'a> { @@ -64,7 +85,7 @@ impl<'a> SimplePresentationWriter<'a> { } impl PresentationWriter for SimplePresentationWriter<'_> { - fn fmt_token(&mut self, args: std::fmt::Arguments<'_>) -> Result { + fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { self.writer.write_char(' ')?; } @@ -81,22 +102,29 @@ impl PresentationWriter for SimplePresentationWriter<'_> { Ok(()) } - fn fmt_comment(&mut self, _args: std::fmt::Arguments<'_>) -> Result { + fn fmt_comment(&mut self, _args: fmt::Arguments<'_>) -> Result { Ok(()) } fn newline(&mut self) -> Result { - self.writer.write_char('\n'); + self.writer.write_char('\n')?; self.first = true; Ok(()) } } +/// A more structured wrapper around a [`PresentationWriter`] +/// +/// Writing comments is not allowed with this type because comments can only +/// appear when a token is surrounded by parentheses. pub struct Presenter<'a> { writer: &'a mut (dyn PresentationWriter + 'a), } impl<'a> Presenter<'a> { + /// Start a sequence of grouped tokens + /// + /// The block might be surrounded by `(` and `)` in a multiline format. pub fn block<'b>(&'b mut self) -> Block<'a, 'b> { let result = self.writer.begin_block(); Block { @@ -105,7 +133,8 @@ impl<'a> Presenter<'a> { } } - pub fn write_token(&mut self, token: impl std::fmt::Display) -> Result { + /// Push a token + pub fn write_token(&mut self, token: impl fmt::Display) -> Result { self.writer.fmt_token(format_args!("{token}")) } } @@ -117,49 +146,139 @@ pub struct Block<'a, 'b> { } impl<'a, 'b> Block<'a, 'b> { - pub fn write_token(&mut self, token: impl std::fmt::Display) -> &mut Self { - self.result = self - .result - .and_then(|_| self.presenter.write_token(token)); + /// Push a token + pub fn write_token( + &mut self, + token: impl std::fmt::Display, + ) -> &mut Self { + self.result = + self.result.and_then(|_| self.presenter.write_token(token)); self } + /// Push the sequence of tokens generated by the [`Show`] implementation + /// of the `token` pub fn write_show(&mut self, token: impl Show) -> &mut Self { - self.result = self - .result - .and_then(|_| token.show(self.presenter)); + self.result = self.result.and_then(|_| token.show(self.presenter)); self } - pub fn write_comment(&mut self, s: impl std::fmt::Display) -> &mut Self { - self.result = self - .result - .and_then(|_| self.presenter.writer.fmt_comment(format_args!("{s}"))); + /// Write a comment + /// + /// This may be ignored. + pub fn write_comment(&mut self, s: impl fmt::Display) -> &mut Self { + self.result = self.result.and_then(|_| { + self.presenter.writer.fmt_comment(format_args!("{s}")) + }); self } + /// Finish the block. + /// + /// This _must_ be called before the `Block` is dropped. pub fn finish(&mut self) -> Result { self.result.and_then(|_| self.presenter.writer.end_block()) } } -#[cfg(test)] +#[cfg(all(test, feature = "std"))] mod test { + use std::string::ToString as _; + use std::vec::Vec; + + use crate::base::iana::{Class, DigestAlg, SecAlg}; use crate::base::show::Show; - use crate::base::{Record, Name, Ttl}; - use crate::rdata::A; - use crate::base::iana::Class; + use crate::base::{Name, Record, Ttl}; + use crate::rdata::{Cds, Cname, Ds, Mx, Txt, A}; + + fn create_record(data: Data) -> Record<&'static Name<[u8]>, Data> { + let name = Name::from_slice(b"\x07example\x03com\x00").unwrap(); + Record::new(name, Class::IN, Ttl::from_secs(3600), data) + } #[test] fn a_record() { - let name = Name::from_slice(b"\x07example\x03com\x00").unwrap(); - let record = Record::new( - name, - Class::IN, - Ttl::from_secs(3600), - A::new("128.140.76.106".parse().unwrap()), + let record = create_record(A::new("128.140.76.106".parse().unwrap())); + assert_eq!( + "example.com. 3600 IN A 128.140.76.106", + record.display_zonefile().to_string() ); - eprintln!("{}", record.display_zonefile()); - panic!() } -} \ No newline at end of file + + #[test] + fn cname_record() { + let record = create_record(Cname::new( + Name::from_slice(b"\x07example\x03com\x00").unwrap(), + )); + assert_eq!( + "example.com. 3600 IN CNAME example.com.", + record.display_zonefile().to_string() + ); + } + + #[test] + fn ds_key_record() { + let record = create_record( + Ds::new( + 5414, + SecAlg::ED25519, + DigestAlg::SHA256, + &[0xDE, 0xAD, 0xBE, 0xEF], + ) + .unwrap(), + ); + assert_eq!( + "example.com. 3600 IN DS 5414 15 2 DEADBEEF", + record.display_zonefile().to_string() + ); + } + + #[test] + fn cds_record() { + let record = create_record( + Cds::new( + 5414, + SecAlg::ED25519, + DigestAlg::SHA256, + &[0xDE, 0xAD, 0xBE, 0xEF], + ) + .unwrap(), + ); + assert_eq!( + "example.com. 3600 IN CDS 5414 15 2 DEADBEEF", + record.display_zonefile().to_string() + ); + } + + #[test] + fn mx_record() { + let record = create_record(Mx::new( + 20, + Name::from_slice(b"\x07example\x03com\x00").unwrap(), + )); + assert_eq!( + "example.com. 3600 IN MX 20 example.com.", + record.display_zonefile().to_string() + ); + } + + #[test] + fn txt_record() { + let record = create_record(Txt::>::build_from_slice( + b"this is a string that is longer than 255 characters if I just \ + type a little bit more to pad this test out and then write some \ + more like a silly monkey with a typewriter accidentally writing \ + some shakespeare along the way but it feels like I have to type \ + even longer to hit that limit!\ + ").unwrap()); + assert_eq!( + "example.com. 3600 IN TXT \ + \"this is a string that is longer than 255 characters if I just \ + type a little bit more to pad this test out and then write some \ + more like a silly monkey with a typewriter accidentally writing \ + some shakespeare along the way but it feels like I have to type \ + e\" \"ven longer to hit that limit!\"", + record.display_zonefile().to_string() + ); + } +} diff --git a/src/rdata/cds.rs b/src/rdata/cds.rs index d4d8b8ef..4fa0e58c 100644 --- a/src/rdata/cds.rs +++ b/src/rdata/cds.rs @@ -680,22 +680,11 @@ impl> fmt::Debug for Cds { impl> Show for Cds { fn show(&self, p: &mut Presenter) -> show::Result { - struct Data<'a>(&'a [u8]); - - impl std::fmt::Display for Data<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for ch in self.0 { - write!(f, " {:02x}", *ch)? - } - Ok(()) - } - } - p.block() .write_token(self.key_tag) .write_show(self.algorithm) .write_show(self.digest_type) - .write_token(Data(self.digest.as_ref())) + .write_token(base16::encode_display(&self.digest)) .finish() } } diff --git a/src/rdata/rfc1035/mx.rs b/src/rdata/rfc1035/mx.rs index b5af3b01..424377f5 100644 --- a/src/rdata/rfc1035/mx.rs +++ b/src/rdata/rfc1035/mx.rs @@ -234,7 +234,7 @@ impl Show for Mx { fn show(&self, p: &mut Presenter) -> show::Result { p.block() .write_token(self.preference) - .write_token(&self.exchange) + .write_token(format_args!("{}.", self.exchange)) .finish() } } diff --git a/src/rdata/rfc1035/txt.rs b/src/rdata/rfc1035/txt.rs index 5f53648d..bc4417c9 100644 --- a/src/rdata/rfc1035/txt.rs +++ b/src/rdata/rfc1035/txt.rs @@ -448,7 +448,7 @@ impl> fmt::Debug for Txt { //--- Show -impl> Show for Txt { +impl Show for Txt where Octs: AsRef<[u8]> { fn show(&self, p: &mut Presenter) -> show::Result { let mut block = p.block(); for slice in self.iter_charstrs() {