From 6c9dbfc3a462cbcd7bc0a1fa6463b020f4e2fc32 Mon Sep 17 00:00:00 2001 From: withjannisNLnetLabs Date: Tue, 2 Jun 2026 15:02:37 +0200 Subject: [PATCH 1/6] Extend `Rcode` & `OptCode` with `to_mnemonic_str` (#668) * Extend `Rcode` & `OptCode` with `to_mnemonic_str` The original idea was proposed by @rossmacarthur in PR #648. I moved the match statement into the `to_mnemonic_str` to avoid the unsafe block in @rossmacarthurs version and to implement it the same way as the `int_enum` macro does. * Update Changelog. * Fix Changelog links --- Changelog.md | 6 ++ src/base/iana/rcode.rs | 121 +++++++++++++++++++++++++++-------------- 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/Changelog.md b/Changelog.md index a3389a67..629e2705 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,10 +8,16 @@ New Improvements +* Implemented `to_mnemonic_str` for `Rcode` and `OptCode` ([#668] and [#648] + by [@rossmacarthur]) + Bug fixes Other changes +[#648]: https://github.com/NLnetLabs/domain/pull/648 +[#668]: https://github.com/NLnetLabs/domain/pull/668 +[@rossmacarthur]: https://github.com/rossmacarthur ## 0.12.1 diff --git a/src/base/iana/rcode.rs b/src/base/iana/rcode.rs index 6651c72d..cdd26165 100644 --- a/src/base/iana/rcode.rs +++ b/src/base/iana/rcode.rs @@ -19,6 +19,7 @@ // bits of the wrapped integer. use core::fmt; +use core::str; use core::str::FromStr; //------------ Rcode --------------------------------------------------------- @@ -195,18 +196,27 @@ impl Rcode { /// Returns the mnemonic for this value if there is one. #[must_use] pub const fn to_mnemonic(self) -> Option<&'static [u8]> { + match self.to_mnemonic_str() { + Some(m) => Some(m.as_bytes()), + None => None, + } + } + + /// Returns the mnemonic as a `&str` for this value if there is one + #[must_use] + pub const fn to_mnemonic_str(self) -> Option<&'static str> { match self { - Rcode::NOERROR => Some(b"NOERROR"), - Rcode::FORMERR => Some(b"FORMERR"), - Rcode::SERVFAIL => Some(b"SERVFAIL"), - Rcode::NXDOMAIN => Some(b"NXDOMAIN"), - Rcode::NOTIMP => Some(b"NOTIMP"), - Rcode::REFUSED => Some(b"REFUSED"), - Rcode::YXDOMAIN => Some(b"YXDOMAIN"), - Rcode::YXRRSET => Some(b"YXRRSET"), - Rcode::NXRRSET => Some(b"NXRRSET"), - Rcode::NOTAUTH => Some(b"NOTAUTH"), - Rcode::NOTZONE => Some(b"NOTZONE"), + Rcode::NOERROR => Some("NOERROR"), + Rcode::FORMERR => Some("FORMERR"), + Rcode::SERVFAIL => Some("SERVFAIL"), + Rcode::NXDOMAIN => Some("NXDOMAIN"), + Rcode::NOTIMP => Some("NOTIMP"), + Rcode::REFUSED => Some("REFUSED"), + Rcode::YXDOMAIN => Some("YXDOMAIN"), + Rcode::YXRRSET => Some("YXRRSET"), + Rcode::NXRRSET => Some("NXRRSET"), + Rcode::NOTAUTH => Some("NOTAUTH"), + Rcode::NOTZONE => Some("NOTZONE"), _ => None, } } @@ -255,10 +265,7 @@ impl From for u8 { impl fmt::Display for Rcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => f.write_str(mnemonic), None => self.0.fmt(f), } @@ -267,10 +274,7 @@ impl fmt::Display for Rcode { impl fmt::Debug for Rcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => write!(f, "Rcode::{}", mnemonic), None => f.debug_tuple("Rcode").field(&self.0).finish(), } @@ -575,20 +579,29 @@ impl OptRcode { /// Returns the mnemonic for this value if there is one. #[must_use] pub const fn to_mnemonic(self) -> Option<&'static [u8]> { + match self.to_mnemonic_str() { + Some(m) => Some(m.as_bytes()), + None => None, + } + } + + /// Returns the mnemonic as a `&str` for this value if there is one + #[must_use] + pub const fn to_mnemonic_str(self) -> Option<&'static str> { match self { - OptRcode::NOERROR => Some(b"NOERROR"), - OptRcode::FORMERR => Some(b"FORMERR"), - OptRcode::SERVFAIL => Some(b"SERVFAIL"), - OptRcode::NXDOMAIN => Some(b"NXDOMAIN"), - OptRcode::NOTIMP => Some(b"NOTIMP"), - OptRcode::REFUSED => Some(b"REFUSED"), - OptRcode::YXDOMAIN => Some(b"YXDOMAIN"), - OptRcode::YXRRSET => Some(b"YXRRSET"), - OptRcode::NXRRSET => Some(b"NXRRSET"), - OptRcode::NOTAUTH => Some(b"NOTAUTH"), - OptRcode::NOTZONE => Some(b"NOTZONE"), - OptRcode::BADVERS => Some(b"BADVERS"), - OptRcode::BADCOOKIE => Some(b"BADCOOKIE"), + OptRcode::NOERROR => Some("NOERROR"), + OptRcode::FORMERR => Some("FORMERR"), + OptRcode::SERVFAIL => Some("SERVFAIL"), + OptRcode::NXDOMAIN => Some("NXDOMAIN"), + OptRcode::NOTIMP => Some("NOTIMP"), + OptRcode::REFUSED => Some("REFUSED"), + OptRcode::YXDOMAIN => Some("YXDOMAIN"), + OptRcode::YXRRSET => Some("YXRRSET"), + OptRcode::NXRRSET => Some("NXRRSET"), + OptRcode::NOTAUTH => Some("NOTAUTH"), + OptRcode::NOTZONE => Some("NOTZONE"), + OptRcode::BADVERS => Some("BADVERS"), + OptRcode::BADCOOKIE => Some("BADCOOKIE"), _ => None, } } @@ -645,10 +658,7 @@ impl From for OptRcode { impl fmt::Display for OptRcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => f.write_str(mnemonic), None => self.0.fmt(f), } @@ -657,10 +667,7 @@ impl fmt::Display for OptRcode { impl fmt::Debug for OptRcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => write!(f, "Rcode::{}", mnemonic), None => f.debug_tuple("Rcode").field(&self.0).finish(), } @@ -973,6 +980,22 @@ mod test { assert!("#$%!@".parse::().is_err()); } + #[test] + fn rcode_tomnemonicstr() { + assert_eq!(Rcode::NOERROR.to_mnemonic_str(), Some("NOERROR")); + assert_eq!(Rcode::FORMERR.to_mnemonic_str(), Some("FORMERR")); + assert_eq!(Rcode::SERVFAIL.to_mnemonic_str(), Some("SERVFAIL")); + assert_eq!(Rcode::NXDOMAIN.to_mnemonic_str(), Some("NXDOMAIN")); + assert_eq!(Rcode::NOTIMP.to_mnemonic_str(), Some("NOTIMP")); + assert_eq!(Rcode::REFUSED.to_mnemonic_str(), Some("REFUSED")); + assert_eq!(Rcode::YXDOMAIN.to_mnemonic_str(), Some("YXDOMAIN")); + assert_eq!(Rcode::YXRRSET.to_mnemonic_str(), Some("YXRRSET")); + assert_eq!(Rcode::NXRRSET.to_mnemonic_str(), Some("NXRRSET")); + assert_eq!(Rcode::NOTAUTH.to_mnemonic_str(), Some("NOTAUTH")); + assert_eq!(Rcode::NOTZONE.to_mnemonic_str(), Some("NOTZONE")); + assert_eq!(Rcode(42).to_mnemonic_str(), None); + } + #[test] fn optrcode_fromstr() { assert_eq!(Ok(OptRcode::NOERROR), "NOERROR".parse()); @@ -991,6 +1014,24 @@ mod test { assert!("#$%!@".parse::().is_err()); } + #[test] + fn optrcode_tomnemonicstr() { + assert_eq!(OptRcode::NOERROR.to_mnemonic_str(), Some("NOERROR")); + assert_eq!(OptRcode::FORMERR.to_mnemonic_str(), Some("FORMERR")); + assert_eq!(OptRcode::SERVFAIL.to_mnemonic_str(), Some("SERVFAIL")); + assert_eq!(OptRcode::NXDOMAIN.to_mnemonic_str(), Some("NXDOMAIN")); + assert_eq!(OptRcode::NOTIMP.to_mnemonic_str(), Some("NOTIMP")); + assert_eq!(OptRcode::REFUSED.to_mnemonic_str(), Some("REFUSED")); + assert_eq!(OptRcode::YXDOMAIN.to_mnemonic_str(), Some("YXDOMAIN")); + assert_eq!(OptRcode::YXRRSET.to_mnemonic_str(), Some("YXRRSET")); + assert_eq!(OptRcode::NXRRSET.to_mnemonic_str(), Some("NXRRSET")); + assert_eq!(OptRcode::NOTAUTH.to_mnemonic_str(), Some("NOTAUTH")); + assert_eq!(OptRcode::NOTZONE.to_mnemonic_str(), Some("NOTZONE")); + assert_eq!(OptRcode::BADVERS.to_mnemonic_str(), Some("BADVERS")); + assert_eq!(OptRcode::BADCOOKIE.to_mnemonic_str(), Some("BADCOOKIE")); + assert_eq!(OptRcode(42).to_mnemonic_str(), None); + } + #[test] fn optrcode_isext() { assert!(!OptRcode::NOERROR.is_ext()); From b0e124d484943a2ffd6dcd7d1e4c0ffda80fcffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 2 Jun 2026 21:20:15 +0200 Subject: [PATCH 2/6] Prevent serde to pull std --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ef37df9..a1101532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ jiff = { version = "0.2.1", default-features = false } arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } bumpalo = { version = "3.12", optional = true } constant_time_eq = { version = "0.4.2", optional = true } -octseq = { version = "0.6.0", default-features = false } +octseq = { version = "0.6.1", default-features = false } rand = { version = "0.10.1", optional = true } arc-swap = { version = "1.7.0", optional = true } bytes = { version = "1.2", optional = true, default-features = false } @@ -60,7 +60,7 @@ proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to ring = { version = "0.17.2", optional = true } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } -serde = { version = "1.0.130", optional = true, features = ["derive"] } +serde = { version = "1.0.130", optional = true, default-features = false, features = ["derive"] } siphasher = { version = "1", optional = true } smallvec = { version = "1.3", optional = true } tokio = { version = "1.33", optional = true, features = ["io-util", "macros", "net", "time", "sync", "rt-multi-thread" ] } @@ -73,15 +73,15 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] # Support for libraries -alloc = ["jiff/alloc"] +alloc = ["jiff/alloc", "octseq/alloc", "serde?/alloc"] bumpalo = ["dep:bumpalo", "std"] bytes = ["dep:bytes", "octseq/bytes"] chrono = ["dep:chrono"] heapless = ["dep:heapless", "octseq/heapless"] rand = ["dep:rand"] -serde = ["std", "dep:serde", "octseq/serde"] +serde = ["dep:serde", "octseq/serde"] smallvec = ["dep:smallvec", "octseq/smallvec"] -std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/std", "jiff/std"] +std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/std", "jiff/std", "serde?/std"] tracing = ["dep:log", "dep:tracing"] # Cryptographic backends From e5a84d9cae62805b973ca6f619b8d39a75a258f0 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 11 Jun 2026 14:53:59 +0200 Subject: [PATCH 3/6] Document surprising ordering of Record. (#674) This PR adds a not on the doc string of Record<..> stating that the impls of the equality, ordering, and hashing traits ignore the TTL, so that this behaviour is not quite as surprising as it currently is. --- src/base/record.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/base/record.rs b/src/base/record.rs index e9449352..1c50810f 100644 --- a/src/base/record.rs +++ b/src/base/record.rs @@ -80,6 +80,12 @@ use octseq::parse::Parser; #[cfg_attr(not(feature = "zonefile"), doc = "zonefile")] /// module for that. /// +/// # Equality, Ordering, and Hashing +/// +/// The implementations for the standard equality, ordering, and hashing +/// traits ignore the TTL. That is, two records with the same owner, class, +/// type, and record data are considered identical even if their TTLs differ. +/// /// [`new`]: #method.new /// [`Message`]: ../message/struct.Message.html /// [`MessageBuilder`]: ../message_builder/struct.MessageBuilder.html @@ -300,6 +306,9 @@ where } //--- PartialEq and Eq +// +// These impls ignore the TTL. This may have been a poor choice, but we +// keep it for now and reconsider with the re-write of base. impl PartialEq> for Record where From 989d3db1a268fedaa3b4a00a64d6ee1f1a29eade Mon Sep 17 00:00:00 2001 From: withjannisNLnetLabs Date: Mon, 15 Jun 2026 10:12:51 +0200 Subject: [PATCH 4/6] Improve `new::base::Serial` with `new()`, `get()` and docs (#676) --- src/new/base/serial.rs | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/new/base/serial.rs b/src/new/base/serial.rs index 6804b103..f7fe9878 100644 --- a/src/new/base/serial.rs +++ b/src/new/base/serial.rs @@ -10,7 +10,16 @@ use super::wire::U32; //----------- Serial --------------------------------------------------------- -/// A serial number. +/// Serial number arithmetic. +/// +/// [`Serial`] implements the "Serial number arithmetic" defined in [RFC1982] +/// with a `SERIAL_BITS` value of 32. +/// +/// [`Serial`] should not be used interchangably or be confused with the SOA +/// Serial Number, which is a [`Serial`] but not the sole user of "Serial +/// number arithmetic". +/// +/// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982 #[derive( Copy, Clone, @@ -27,11 +36,23 @@ use super::wire::U32; UnsizedCopy, )] #[repr(transparent)] -pub struct Serial(U32); +pub struct Serial(pub U32); //--- Construction impl Serial { + /// Construct a new [`Serial`] + #[must_use] + pub const fn new(value: u32) -> Self { + Serial(U32::new(value)) + } + + /// Get [`u32`] value of [`Serial`] + #[must_use] + pub const fn get(&self) -> u32 { + self.0.get() + } + /// Measure the current time (in seconds) in serial number space. #[cfg(feature = "std")] pub fn unix_time() -> Self { @@ -53,9 +74,15 @@ impl Serial { /// instead of a [`u32`] because it is easier to understand and implement /// a non-negative check versus the upper range check. /// + /// Section 7 in [RFC1982] states, in particular for the SOA Serial + /// Number, but for any Serial number using a "SERIAL_BITS" value of 32: + /// "The maximum defined increment is 2147483647 (2^31 - 1)." + /// /// # Panics /// /// Panics if the number is negative. + /// + /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-7 pub fn inc(self, num: i32) -> Self { assert!(num >= 0, "Cannot subtract from a `Serial`"); self.0.get().wrapping_add_signed(num).into() @@ -65,6 +92,10 @@ impl Serial { //--- Ordering impl PartialOrd for Serial { + /// The comparison of Serial Number values is defined in Section 3.2 + /// [RFC1982]. + /// + /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-3.2 fn partial_cmp(&self, other: &Self) -> Option { let (lhs, rhs) = (self.0.get(), other.0.get()); From 8aff7ea02023697322326d520cb81c0a2d16a864 Mon Sep 17 00:00:00 2001 From: withjannisNLnetLabs Date: Mon, 15 Jun 2026 10:49:04 +0200 Subject: [PATCH 5/6] Make `set` function const (#677) --- src/new/base/wire/ints.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/new/base/wire/ints.rs b/src/new/base/wire/ints.rs index 8b05b6cb..86409312 100644 --- a/src/new/base/wire/ints.rs +++ b/src/new/base/wire/ints.rs @@ -52,8 +52,7 @@ macro_rules! define_int { } /// Overwrite this value with an integer. - // TODO: Make 'const' at MSRV 1.83.0. - pub fn set(&mut self, value: $base) { + pub const fn set(&mut self, value: $base) { *self = Self::new(value) } } From aed9492cb6f6e503fc59fd7fa3e033692f74bd94 Mon Sep 17 00:00:00 2001 From: withjannisNLnetLabs Date: Wed, 17 Jun 2026 13:05:57 +0200 Subject: [PATCH 6/6] Add `Name` conversion and mnemonic Display for `new::base` (#678) * Added impl fmt::Display for RType and RClass * Added From implementation for new::base Names --- src/new/base/name/absolute.rs | 45 +++++++++++- src/new/base/name/reversed.rs | 50 ++++++++++++++ src/new/base/record.rs | 124 ++++++++++++++++++++++++++++++++++ 3 files changed, 218 insertions(+), 1 deletion(-) diff --git a/src/new/base/name/absolute.rs b/src/new/base/name/absolute.rs index 11f1a553..ae93c3db 100644 --- a/src/new/base/name/absolute.rs +++ b/src/new/base/name/absolute.rs @@ -23,7 +23,7 @@ use crate::{ use super::{ CanonicalName, Label, LabelBuf, LabelIter, LabelParseError, - NameCompressor, + NameCompressor, RevNameBuf, }; //----------- Name ----------------------------------------------------------- @@ -100,6 +100,11 @@ impl Name { // SAFETY: A 'Name' always contains valid encoded labels. unsafe { LabelIter::new_unchecked(self.as_bytes()) } } + + /// Covert &[`Name`] into [`RevNameBuf`] + pub fn to_revname(&self) -> RevNameBuf { + NameBuf::copy_from(self).into() + } } //--- Canonical operations @@ -879,3 +884,41 @@ impl fmt::Display for NameParseError { }) } } + +// -- Convert from old Name to new::base::NameBuf ---------------------------- + +/// Upgrade a [`crate::base::Name`] into a +/// [`crate::new::base::name::NameBuf`]. +impl From<&crate::base::Name> for NameBuf +where + Octs: AsRef<[u8]> + ?Sized, +{ + fn from(value: &crate::base::Name) -> Self { + NameBuf::parse_bytes(value.as_slice()) + .expect("Tried to upgrade invalid name") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_upgrade_name_to_namebuf() { + let old_name = + crate::base::Name::from_slice(b"\x07example\x03com\x00") + .expect("Invalid name"); + + let new_name: NameBuf = old_name.into(); + assert_eq!(old_name.as_slice(), new_name.as_bytes()) + } + + #[test] + fn test_to_revname() { + let namebuf: NameBuf = "example.com".parse().unwrap(); + assert_eq!( + namebuf.to_revname().as_bytes(), + b"\x00\x03com\x07example", + ); + } +} diff --git a/src/new/base/name/reversed.rs b/src/new/base/name/reversed.rs index eb5e7965..8c396723 100644 --- a/src/new/base/name/reversed.rs +++ b/src/new/base/name/reversed.rs @@ -110,6 +110,11 @@ impl RevName { // SAFETY: A 'RevName' always contains valid encoded labels. unsafe { LabelIter::new_unchecked(self.as_bytes()) } } + + /// Covert &[`RevName`] into [`NameBuf`] + pub fn to_name(&self) -> NameBuf { + RevNameBuf::copy_from(self).into() + } } //--- Building in DNS messages @@ -666,3 +671,48 @@ impl<'a> serde::Deserialize<'a> for std::boxed::Box { .map(|this| this.unsized_copy_into()) } } + +// -- Convert from old Name to new::base::RevNameBuf ------------------------- + +/// Upgrade a [`crate::base::Name`] into a +/// [`crate::new::base::name::reversed::RevNameBuf`]. +/// +/// # Panics +/// +/// The [`crate::base::Name`] slice has to contain a valid domain. +impl From<&crate::base::Name> for RevNameBuf +where + Octs: AsRef<[u8]> + ?Sized, +{ + fn from(value: &crate::base::Name) -> Self { + RevNameBuf::parse_bytes(value.as_slice()) + .expect("Tried to upgrade invalid name") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(feature = "alloc")] + fn test_upgrade_revnamebuf() { + let old_name = + crate::base::Name::from_slice(b"\x07example\x03com\x00") + .expect("Invalid name"); + + let new_name: RevNameBuf = old_name.into(); + let mut buf = alloc::vec![0; new_name.built_bytes_size()]; + new_name.build_bytes(&mut buf).unwrap(); + assert_eq!(old_name.as_slice(), buf); + } + + #[test] + fn test_to_name() { + let revnamebuf: RevNameBuf = "example.com".parse().unwrap(); + assert_eq!( + revnamebuf.to_name().as_bytes(), + b"\x07example\x03com\x00", + ); + } +} diff --git a/src/new/base/record.rs b/src/new/base/record.rs index 66b681f1..893fcd92 100644 --- a/src/new/base/record.rs +++ b/src/new/base/record.rs @@ -447,6 +447,54 @@ impl fmt::Debug for RType { } } +/// Format an [`RType`] in a human-readable way +/// +/// Return the mnemonic of [`RType`]. If [`RType`] is unknown, then the +/// returned string contains the type in the unknown format as defined in +/// Section 5 in [RFC3597]. +/// +/// The mnemonics are consolidated by [IANA]. +/// +/// ``` +/// # use domain::new::base::RType; +/// // Known RType with mnemonic +/// assert_eq!("A", format!("{}", RType::A)); +/// // Unknown RType +/// assert_eq!("TYPE265", format!("{}", RType::from(265))); +/// ``` +/// +/// [RFC3597]: https://datatracker.ietf.org/doc/html/rfc3597#section-5 +/// [IANA]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml +impl fmt::Display for RType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::A => "A", + Self::NS => "NS", + Self::CNAME => "CNAME", + Self::SOA => "SOA", + Self::PTR => "PTR", + Self::HINFO => "HINFO", + Self::MX => "MX", + Self::TXT => "TXT", + Self::RP => "RP", + Self::AAAA => "AAAA", + Self::DNAME => "DNAME", + Self::OPT => "OPT", + Self::DS => "DS", + Self::RRSIG => "RRSIG", + Self::NSEC => "NSEC", + Self::DNSKEY => "DNSKEY", + Self::NSEC3 => "NSEC3", + Self::NSEC3PARAM => "NSEC3PARAM", + Self::CDS => "CDS", + Self::CDNSKEY => "CDNSKEY", + Self::ZONEMD => "ZONEMD", + Self::TSIG => "TSIG", + _ => return write!(f, "TYPE{}", self.code), + }) + } +} + //----------- RClass --------------------------------------------------------- /// The class of a record. @@ -489,6 +537,22 @@ impl RClass { pub const CH: Self = Self::new(3); } +//--- Conversion to and from 'u16' + +impl From for RClass { + fn from(value: u16) -> Self { + Self { + code: U16::new(value), + } + } +} + +impl From for u16 { + fn from(value: RClass) -> Self { + value.code.get() + } +} + //--- Formatting impl fmt::Debug for RClass { @@ -501,6 +565,34 @@ impl fmt::Debug for RClass { } } +/// Format an [`RClass`] in a human-readable way +/// +/// Return the mnemonic of [`RClass`]. If [`RClass`] is unknown, then the +/// returned string contains the class in the unknown format as defined in +/// Section 5 in [RFC3597]. +/// +/// The mnemonics are consolidated by [IANA]. +/// +/// ``` +/// # use domain::new::base::RClass; +/// // Known RClass with mnemonic +/// assert_eq!("IN", format!("{}", RClass::IN)); +/// // Unknown RClass +/// assert_eq!("CLASS42", format!("{}", RClass::from(42))); +/// ``` +/// +/// [RFC3597]: https://datatracker.ietf.org/doc/html/rfc3597#section-5 +/// [IANA]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml +impl fmt::Display for RClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::IN => "IN", + Self::CH => "CH", + _ => return write!(f, "CLASS{}", self.code), + }) + } +} + //----------- TTL ------------------------------------------------------------ /// How long a record can be cached. @@ -758,4 +850,36 @@ mod test { assert_eq!(record.build_bytes(&mut buffer), Ok(&mut [] as &mut [u8])); assert_eq!(buffer, &bytes[..15]); } + + #[test] + fn test_rclass_from() { + let rclass: RClass = 1.into(); + assert_eq!(rclass, RClass::IN); + + let number: u16 = rclass.into(); + assert_eq!(number, 1); + } + + #[test] + fn test_rtype_from() { + let rtype: RType = 6.into(); + assert_eq!(rtype, RType::SOA); + + let number: u16 = rtype.into(); + assert_eq!(number, 6); + } + + #[test] + fn test_rclass_display() { + assert_eq!("IN", format!("{}", RClass::IN)); + assert_eq!("CH", format!("{}", RClass::CH)); + assert_eq!("CLASS42", format!("{}", RClass::from(42))); + } + + #[test] + fn test_rtype_display() { + assert_eq!("A", format!("{}", RType::A)); + assert_eq!("MX", format!("{}", RType::MX)); + assert_eq!("TYPE265", format!("{}", RType::from(265))); + } }