Add support for CAA record type (#434)

This commit is contained in:
Weilence
2025-11-21 12:51:55 +01:00
committed by GitHub
parent 43493a13c9
commit 4b25c746be
4 changed files with 734 additions and 6 deletions
+12 -6
View File
@@ -398,7 +398,7 @@ impl<Octs: AsRef<[u8]> + ?Sized> CharStr<Octs> {
/// quotes. It will escape double quotes, backslashes, and non-printable
/// octets only.
pub fn display_quoted(&self) -> DisplayQuoted<'_> {
DisplayQuoted(self.for_slice())
DisplayQuoted(self.0.as_ref())
}
/// Returns an object that formats in unquoted presentation format.
@@ -407,7 +407,7 @@ impl<Octs: AsRef<[u8]> + ?Sized> CharStr<Octs> {
/// delimiters and escapes space, double quotes, semicolons, backslashes,
/// and non-printable octets.
pub fn display_unquoted(&self) -> DisplayUnquoted<'_> {
DisplayUnquoted(self.for_slice())
DisplayUnquoted(self.0.as_ref())
}
}
@@ -927,12 +927,18 @@ impl Iterator for Iter<'_> {
///
/// A value of this type can be obtained via `CharStr::display_quoted`.
#[derive(Clone, Copy, Debug)]
pub struct DisplayQuoted<'a>(&'a CharStr<[u8]>);
pub struct DisplayQuoted<'a>(&'a [u8]);
impl<'a> DisplayQuoted<'a> {
pub fn from_slice(octets: &'a [u8]) -> Self {
DisplayQuoted(octets)
}
}
impl fmt::Display for DisplayQuoted<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\"")?;
for &ch in self.0.as_ref() {
for &ch in self.0 {
fmt::Display::fmt(&Symbol::quoted_from_octet(ch), f)?;
}
f.write_str("\"")
@@ -945,11 +951,11 @@ impl fmt::Display for DisplayQuoted<'_> {
///
/// A value of this type can be obtained via `CharStr::display_serialized`.
#[derive(Clone, Copy, Debug)]
pub struct DisplayUnquoted<'a>(&'a CharStr<[u8]>);
pub struct DisplayUnquoted<'a>(&'a [u8]);
impl fmt::Display for DisplayUnquoted<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for &ch in self.0.as_ref() {
for &ch in self.0 {
fmt::Display::fmt(&Symbol::from_octet(ch), f)?;
}
Ok(())
+15
View File
@@ -322,6 +322,7 @@ mod test {
use crate::base::iana::{Class, DigestAlgorithm, SecurityAlgorithm};
use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt};
use crate::base::{Name, Record, Ttl};
use crate::rdata::caa::{CaaFlags, CaaTag};
use crate::rdata::{Cds, Cname, Ds, Mx, Txt, A};
fn create_record<Data>(data: Data) -> Record<&'static Name<[u8]>, Data> {
@@ -495,4 +496,18 @@ mod test {
record.display_zonefile(DisplayKind::Tabbed).to_string()
);
}
#[test]
fn caa_record() {
use crate::rdata::Caa;
let record = create_record(Caa::new(
CaaFlags::default(),
CaaTag::from_octets("issue".as_bytes()).unwrap(),
"ca.example.net".as_bytes(),
));
assert_eq!(
"example.com. 3600 IN CAA 0 issue \"ca.example.net\"",
record.display_zonefile(DisplayKind::Simple).to_string()
);
}
}
+701
View File
@@ -0,0 +1,701 @@
//! Record data from [RFC 8659]: CAA records.
//!
//! This RFC defines the CAA record type.
//!
//! [RFC 8659]: https://www.rfc-editor.org/info/rfc8659
use crate::base::{
charstr::DisplayQuoted,
name::FlattenInto,
rdata::ComposeRecordData,
scan::{Scan, Scanner, ScannerError},
wire::{Compose, Parse, ParseError},
zonefile_fmt::{self, Formatter, ZonefileFmt},
CanonicalOrd, CharStr, ParseRecordData, RecordData, Rtype,
};
use core::{cmp::Ordering, fmt, hash};
#[cfg(feature = "serde")]
use octseq::{
builder::{EmptyBuilder, FromBuilder},
serde::DeserializeOctets,
serde::SerializeOctets,
};
use octseq::{Octets, OctetsBuilder, OctetsFrom, OctetsInto, Parser};
//------------ Caa ---------------------------------------------------------
/// Caa record data.
///
/// The Certification Authority Authorization (CAA) DNS Resource Record allows
/// a DNS domain name holder to specify one or more Certification Authorities
/// (CAs) authorized to issue certificates for that domain name.
///
/// CAA Resource Records allow a public CA to implement additional controls to reduce the
/// risk of unintended certificate mis-issue.
///
/// The Caa record type is defined in [RFC 8659, section 4.1][1].
///
/// [1]: https://www.rfc-editor.org/rfc/rfc8659#section-4.1
#[derive(Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(bound(
serialize = "
Octs: SerializeOctets + AsRef<[u8]>
",
deserialize = "
Octs: FromBuilder + DeserializeOctets<'de>,
<Octs as FromBuilder>::Builder:
OctetsBuilder + EmptyBuilder
+ AsRef<[u8]>,
",
))
)]
pub struct Caa<Octs> {
flags: CaaFlags,
tag: CaaTag<Octs>,
#[cfg_attr(
feature = "serde",
serde(
serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
bound(
serialize = "Octs: octseq::serde::SerializeOctets",
deserialize = "Octs: octseq::serde::DeserializeOctets<'de>",
)
)
)]
value: Octs,
}
impl Caa<()> {
/// The rtype of this record data type.
pub const RTYPE: Rtype = Rtype::CAA;
}
impl<Octs> Caa<Octs> {
/// Creates a new CAA record data from the flags, tag, and value.
pub fn new(flags: CaaFlags, tag: CaaTag<Octs>, value: Octs) -> Self {
Caa { flags, tag, value }
}
/// Returns the flags. If the value is set to "1", the Property is critical.
/// A CA MUST NOT issue certificates for any FQDN if the
/// Relevant RRset for that FQDN contains a CAA critical
/// Property for an unknown or unsupported Property Tag.
pub fn flags(&self) -> CaaFlags {
self.flags
}
/// Returns the Property identifier
pub fn tag(&self) -> &CaaTag<Octs> {
&self.tag
}
/// Returns the Property Value
pub fn value(&self) -> &Octs {
&self.value
}
pub(in crate::rdata) fn convert_octets<TOcts: OctetsFrom<Octs>>(
self,
) -> Result<Caa<TOcts>, TOcts::Error> {
Ok(Caa::new(
self.flags,
self.tag.try_octets_into()?,
self.value.try_octets_into()?,
))
}
pub(in crate::rdata) fn flatten<TOcts: OctetsFrom<Octs>>(
self,
) -> Result<Caa<TOcts>, TOcts::Error> {
self.convert_octets()
}
pub fn scan<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error>
where
Octs: AsRef<[u8]>,
{
Ok(Self::new(
CaaFlags::scan(scanner)?,
CaaTag::scan(scanner)?,
scanner.scan_octets()?,
))
}
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError>
where
Octs: AsRef<[u8]>,
{
Ok(Self::new(
CaaFlags::parse(parser)?,
CaaTag::parse(parser)?,
parser.parse_octets(parser.remaining())?,
))
}
}
//--- OctetsFrom
impl<Octs, SrcOcts> OctetsFrom<Caa<SrcOcts>> for Caa<Octs>
where
Octs: OctetsFrom<SrcOcts>,
{
type Error = Octs::Error;
fn try_octets_from(source: Caa<SrcOcts>) -> Result<Self, Self::Error> {
Ok(Caa {
flags: source.flags,
tag: CaaTag::try_octets_from(source.tag)?,
value: Octs::try_octets_from(source.value)?,
})
}
}
//--- FlattenInto
impl<Octs, TOcts> FlattenInto<Caa<TOcts>> for Caa<Octs>
where
TOcts: OctetsFrom<Octs>,
{
type AppendError = TOcts::Error;
fn try_flatten_into(self) -> Result<Caa<TOcts>, Self::AppendError> {
self.flatten()
}
}
//--- PartialEq and Eq
impl<Octs, OtherOcts> PartialEq<Caa<OtherOcts>> for Caa<Octs>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
{
fn eq(&self, other: &Caa<OtherOcts>) -> bool {
self.flags == other.flags
&& self.tag.eq(&other.tag)
&& self.value.as_ref().eq(other.value.as_ref())
}
}
impl<O: AsRef<[u8]>> Eq for Caa<O> {}
//--- PartialOrd, Ord, and CanonicalOrd
impl<Octs, OtherOcts> PartialOrd<Caa<OtherOcts>> for Caa<Octs>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
{
fn partial_cmp(&self, other: &Caa<OtherOcts>) -> Option<Ordering> {
match self.flags.partial_cmp(&other.flags) {
Some(Ordering::Equal) => (),
other => return other,
}
match self.tag.partial_cmp(&other.tag) {
Some(Ordering::Equal) => (),
other => return other,
}
self.value.as_ref().partial_cmp(other.value.as_ref())
}
}
impl<Octs, OtherOcts> CanonicalOrd<Caa<OtherOcts>> for Caa<Octs>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
{
fn canonical_cmp(&self, other: &Caa<OtherOcts>) -> Ordering {
match self.flags.cmp(&other.flags) {
Ordering::Equal => (),
ord => return ord,
}
match self.tag.canonical_cmp(&other.tag) {
Ordering::Equal => (),
ord => return ord,
}
self.value.as_ref().cmp(other.value.as_ref())
}
}
impl<O: AsRef<[u8]>> Ord for Caa<O> {
fn cmp(&self, other: &Self) -> Ordering {
match self.flags.cmp(&other.flags) {
Ordering::Equal => (),
ord => return ord,
}
match self.tag.cmp(&other.tag) {
Ordering::Equal => (),
ord => return ord,
}
self.value.as_ref().cmp(other.value.as_ref())
}
}
//--- Hash
impl<O: AsRef<[u8]>> hash::Hash for Caa<O> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.flags.hash(state);
self.tag.hash(state);
self.value.as_ref().hash(state);
}
}
//--- RecordData, ParseRecordData, ComposeRecordData
impl<Octs> RecordData for Caa<Octs> {
fn rtype(&self) -> Rtype {
Caa::RTYPE
}
}
impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs>
for Caa<Octs::Range<'a>>
{
fn parse_rdata(
rtype: Rtype,
parser: &mut octseq::Parser<'a, Octs>,
) -> Result<Option<Self>, crate::base::wire::ParseError> {
if rtype == Caa::RTYPE {
Self::parse(parser).map(Some)
} else {
Ok(None)
}
}
}
impl<Octs: AsRef<[u8]>> ComposeRecordData for Caa<Octs> {
fn rdlen(&self, _compress: bool) -> Option<u16> {
Some(
u8::COMPOSE_LEN
.checked_add(self.tag.compose_len())
.expect("long tag")
.checked_add(
u16::try_from(self.value.as_ref().len())
.expect("long value"),
)
.expect("long value"),
)
}
fn compose_rdata<Target: crate::base::wire::Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.flags.compose(target)?;
self.tag.compose(target)?;
target.append_slice(self.value.as_ref())
}
fn compose_canonical_rdata<
Target: crate::base::wire::Composer + ?Sized,
>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.flags.compose(target)?;
self.tag.compose(target)?;
target.append_slice(self.value.as_ref())
}
}
//--- Display
impl<O: AsRef<[u8]>> fmt::Display for Caa<O> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {}",
self.flags,
self.tag,
DisplayQuoted::from_slice(self.value.as_ref()),
)
}
}
//--- Debug
impl<O: AsRef<[u8]>> fmt::Debug for Caa<O> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Caa")
.field("flags", &self.flags)
.field("tag", &self.tag)
.field("value", &DisplayQuoted::from_slice(self.value.as_ref()))
.finish()
}
}
//--- ZonefileFmt
impl<O: AsRef<[u8]>> ZonefileFmt for Caa<O> {
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
p.block(|p| {
p.write_token(self.flags)?;
p.write_comment("flags")?;
p.write_token(&self.tag)?;
p.write_comment("tag")?;
p.write_token(DisplayQuoted::from_slice(self.value.as_ref()))?;
p.write_comment("value")
})
}
}
/// A CAA property tag as defined in [RFC 8659 section 4.1].
///
/// A CAA tag identifies the property name that an issuer must honor when
/// evaluating a certificate issuance request. RFC 8659 restricts the tag to
/// printable ASCII alphabetic characters and digits with a maximal length of
/// 255 octets, and the wire format is a length-prefixed string of those
/// characters.
///
/// [RFC 8659 section 4.1]: https://www.rfc-editor.org/rfc/rfc8659#section-4.1
#[derive(Clone)]
#[repr(transparent)]
pub struct CaaTag<Octs: ?Sized>(CharStr<Octs>);
impl<Octs> CaaTag<Octs> {
/// Constructs a CAA tag from a `CharStr`, validating that it only contains
/// ASCII letters/digits and is at most 255 octets long.
pub fn new(charstr: CharStr<Octs>) -> Result<Self, ParseError>
where
Octs: AsRef<[u8]>,
{
CaaTag::check_slice(charstr.as_slice())?;
Ok(CaaTag(charstr))
}
/// Parses a CAA tag from an octets sequence while enforcing the same
/// validation as [`CaaTag::new`].
pub fn from_octets(octets: Octs) -> Result<Self, ParseError>
where
Octs: AsRef<[u8]>,
{
CaaTag::check_slice(octets.as_ref())?;
Ok(unsafe { Self::from_octets_unchecked(octets) })
}
/// Creates a CAA tag from octets without validation.
///
/// # Safety
///
/// The caller must ensure `octets` consists only of ASCII alphanumeric
/// characters and is no longer than 255 octets, as required by RFC 8659.
pub unsafe fn from_octets_unchecked(octets: Octs) -> Self {
CaaTag(CharStr::from_octets_unchecked(octets))
}
}
impl CaaTag<[u8]> {
/// Parses a CAA tag from a slice, validating it against the same rules as
/// [`CaaTag::new`].
pub fn from_slice(slice: &[u8]) -> Result<&Self, ParseError> {
Self::check_slice(slice)?;
Ok(unsafe { Self::from_slice_unchecked(slice) })
}
/// Creates a new value from a slice without checking.
///
/// # Safety
///
/// The caller needs to make sure that the slice only contains ascii
/// alphanumeric characters and is not longer than 255 bytes.
pub unsafe fn from_slice_unchecked(slice: &[u8]) -> &Self {
// SAFETY: CaaTag has repr(transparent)
&*(CharStr::from_slice_unchecked(slice) as *const CharStr<[u8]>
as *const Self)
}
fn check_slice(octets: &[u8]) -> Result<(), ParseError> {
if octets.iter().any(|e| !e.is_ascii_alphanumeric()) {
return Err(ParseError::form_error(
"CAA tag contains invalid character",
));
}
Ok(())
}
}
impl<Octs: AsRef<[u8]>> CaaTag<Octs> {
/// Returns the length of the wire-format tag, which mirrors the length of
/// the underlying `CharStr`.
pub fn compose_len(&self) -> u16 {
self.0.compose_len()
}
/// Writes the tag into `target` using the standard length-prefixed wire
/// format built from the ASCII characters of the tag.
pub fn compose<Target: OctetsBuilder + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.0.compose(target)
}
/// Scans a CAA tag from the scanner, enforcing the ASCII rules used by
/// the CAA property tag.
pub fn scan<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
let octets = CharStr::scan(scanner)?;
CaaTag::check_slice(octets.as_slice()).map_err(|_| {
S::Error::custom("CAA tag contains invalid character")
})?;
Ok(CaaTag(octets))
}
/// Parses a CAA tag from the parser while validating it for ASCII letters
/// and digits with a valid length.
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError> {
Self::new(CharStr::parse(parser)?)
}
}
impl<Octs, SrcOcts> OctetsFrom<CaaTag<SrcOcts>> for CaaTag<Octs>
where
Octs: OctetsFrom<SrcOcts>,
{
type Error = Octs::Error;
fn try_octets_from(source: CaaTag<SrcOcts>) -> Result<Self, Self::Error> {
Ok(CaaTag(CharStr::try_octets_from(source.0)?))
}
}
//--- PartialEq and Eq
impl<Octs, OtherOcts> PartialEq<CaaTag<OtherOcts>> for CaaTag<Octs>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
{
fn eq(&self, other: &CaaTag<OtherOcts>) -> bool {
self.0.eq(&other.0)
}
}
impl<O: AsRef<[u8]>> Eq for CaaTag<O> {}
//--- PartialOrd, Ord, and CanonicalOrd
impl<Octs, OtherOcts> PartialOrd<CaaTag<OtherOcts>> for CaaTag<Octs>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
{
fn partial_cmp(&self, other: &CaaTag<OtherOcts>) -> Option<Ordering> {
self.0.partial_cmp(&other.0)
}
}
impl<Octs, OtherOcts> CanonicalOrd<CaaTag<OtherOcts>> for CaaTag<Octs>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
{
fn canonical_cmp(&self, other: &CaaTag<OtherOcts>) -> Ordering {
self.0.canonical_cmp(&other.0)
}
}
impl<O: AsRef<[u8]>> Ord for CaaTag<O> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
//--- Hash
impl<O: AsRef<[u8]>> hash::Hash for CaaTag<O> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
//--- Display and Debug
impl<O: AsRef<[u8]>> fmt::Display for CaaTag<O> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<O: AsRef<[u8]>> fmt::Debug for CaaTag<O> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("CaaTag").field(&self.0).finish()
}
}
//--- Serialize and Deserialize
#[cfg(feature = "serde")]
impl<Octs> serde::Serialize for CaaTag<Octs>
where
Octs: AsRef<[u8]> + octseq::serde::SerializeOctets,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, Octs> serde::Deserialize<'de> for CaaTag<Octs>
where
Octs: FromBuilder + octseq::serde::DeserializeOctets<'de>,
<Octs as FromBuilder>::Builder: AsRef<[u8]> + EmptyBuilder,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Self::new(CharStr::deserialize(deserializer)?)
.map_err(serde::de::Error::custom)
}
}
/// CAA flags as defined in [RFC 8659 section 4.1].
///
/// The only defined flag is the critical flag (bit 7).
/// You can create a plain CAA flags instance with [CaaFlags::default()]
/// or critical CAA flags with [CaaFlags::critical()].
///
/// The [CaaFlags::new()] method allows creating a CAA flags instance
/// from any underlying byte, but be aware that only bit 7 is defined.
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CaaFlags(u8);
impl CaaFlags {
/// Creates a new CAA flags instance from the underlying byte.
pub fn new(bits: u8) -> Self {
CaaFlags(bits)
}
/// Creates a CAA flags instance with the critical flag set.
pub fn critical() -> Self {
CaaFlags(0x80)
}
/// Returns the underlying flags byte.
pub fn bits(&self) -> u8 {
self.0
}
}
impl fmt::Display for CaaFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Compose for CaaFlags {
fn compose<Target: octseq::OctetsBuilder + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.0.compose(target)
}
}
impl<S: Scanner> Scan<S> for CaaFlags {
fn scan(scanner: &mut S) -> Result<Self, S::Error> {
Ok(CaaFlags(u8::scan(scanner)?))
}
}
impl<'a, Octs: AsRef<[u8]> + ?Sized> Parse<'a, Octs> for CaaFlags {
fn parse(parser: &mut Parser<'a, Octs>) -> Result<Self, ParseError> {
Ok(CaaFlags(u8::parse(parser)?))
}
}
#[cfg(test)]
#[cfg(all(feature = "std", feature = "bytes"))]
mod test {
use super::*;
use crate::std::string::ToString;
use octseq::array::Array;
#[test]
fn caa_eq() {
let caa1 = Caa::new(
CaaFlags::default(),
CaaTag::from_octets("ISSUE".as_bytes()).unwrap(),
"ca.example.net".as_bytes(),
);
let caa2 = Caa::new(
CaaFlags::default(),
CaaTag::from_octets("issue".as_bytes()).unwrap(),
"ca.example.net".as_bytes(),
);
assert_eq!(caa1, caa2);
}
#[test]
fn caa_octets_info() {
let caa = Caa::new(
CaaFlags::default(),
CaaTag::from_octets("issue".as_bytes()).unwrap(),
"ca.example.net".as_bytes(),
);
let caa_bytes: Caa<bytes::Bytes> = caa.clone().octets_into();
assert_eq!(caa.flags, caa_bytes.flags);
assert_eq!(caa.tag, caa_bytes.tag);
assert_eq!(caa.value, caa_bytes.value);
}
#[test]
fn caa_display() {
let caa = Caa::new(
CaaFlags::default(),
CaaTag::from_octets("issue".as_bytes()).unwrap(),
"ca.example.net".as_bytes(),
);
assert_eq!(caa.to_string(), r#"0 issue "ca.example.net""#);
}
#[test]
fn caa_tag_creation_and_validation() {
assert!(CaaTag::from_octets("issue".as_bytes()).is_ok());
assert!(CaaTag::from_octets("bad tag".as_bytes()).is_err());
}
#[test]
fn caa_tag_display_and_debug() {
let tag = CaaTag::from_octets("ISSUE".as_bytes()).unwrap();
assert_eq!(tag.to_string(), "ISSUE");
assert_eq!(format!("{:?}", tag), "CaaTag(CharStr(ISSUE))");
}
#[test]
fn caa_tag_compose_canonical_lowercases() {
let tag = CaaTag::from_octets("Issue".as_bytes()).unwrap();
let mut buf = Array::<8>::new();
tag.compose(&mut buf).unwrap();
assert_eq!(buf.as_ref(), &[5, b'I', b's', b's', b'u', b'e']);
}
#[test]
fn caa_flags_display() {
let flags = CaaFlags::default();
assert_eq!(flags.bits(), 0);
}
#[test]
fn caa_flags_critical() {
let flags = CaaFlags::critical();
assert_eq!(flags.bits(), 0x80);
}
}
+6
View File
@@ -46,6 +46,7 @@
mod macros;
pub mod aaaa;
pub mod caa;
pub mod cds;
pub mod dname;
pub mod dnssec;
@@ -102,6 +103,11 @@ rdata_types! {
Aaaa,
}
}
caa::{
zone {
Caa<O>,
}
}
cds::{
zone {
Cdnskey<O>,