Skip to content

Commit

Permalink
proto: implement FromStr for CAA/MX/SOA record data
Browse files Browse the repository at this point in the history
  • Loading branch information
djc committed Aug 9, 2022
1 parent be6d6a6 commit 49ef75f
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 0 deletions.
109 changes: 109 additions & 0 deletions crates/proto/src/rr/rdata/caa.rs
Expand Up @@ -23,6 +23,7 @@

use std::fmt;
use std::str;
use std::str::FromStr;

#[cfg(feature = "serde-config")]
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -837,6 +838,19 @@ impl fmt::Display for Property {
}
}

impl FromStr for Property {
type Err = ProtoError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"issue" => Property::Issue,
"issuewild" => Property::IssueWild,
"iodef" => Property::Iodef,
_ => return Err(ProtoErrorKind::Message("invalid tag in CAA record").into()),
})
}
}

impl fmt::Display for Value {
// https://datatracker.ietf.org/doc/html/rfc6844#section-5.1.1
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
Expand Down Expand Up @@ -890,6 +904,101 @@ impl fmt::Display for CAA {
}
}

impl FromStr for CAA {
type Err = ProtoError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(' ');
let critical = match parts.next() {
Some("1") => true,
Some("0") => false,
Some(_) => {
return Err(ProtoErrorKind::Message("invalid critical value in CAA record").into())
}
None => {
return Err(ProtoErrorKind::Message("missing critical value in CAA record").into())
}
};

let tag = match parts.next() {
Some(s) => Property::from_str(s)?,
None => return Err(ProtoErrorKind::Message("missing tag in CAA record").into()),
};

let value = match parts.next() {
Some(part) => part
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.ok_or(ProtoErrorKind::Message("invalid value in CAA record"))?,
None => return Err(ProtoErrorKind::Message("missing value in CAA record").into()),
};

if parts.next().is_some() {
return Err(ProtoErrorKind::Message(
"unexpected data after value in CAA record",
).into());
}

if let Property::Iodef = tag {
return Ok(Self::new_iodef(
critical,
Url::from_str(value)
.map_err(|_| ProtoErrorKind::Message("invalid URL in CAA record"))?,
));
}

let value = value.trim();
let (domain, params) = match value.split_once(';') {
Some((domain, params)) => (domain, params),
None => (value, ""),
};

let name = match domain.trim().is_empty() {
true => None,
false => Some(
Name::from_str(domain)
.map_err(|_| ProtoErrorKind::Message("invalid domain in CAA record"))?,
),
};

let options = params
.split(';')
.filter_map(|param| {
if param.is_empty() {
return None;
}

let mut parts = param.split('=');
let key = match parts.next() {
Some(part) => part,
None => {
return Some(Err(ProtoErrorKind::Message(
"missing key in CAA record options",
)))
}
};

let value = match parts.next() {
Some(part) => part,
None => {
return Some(Err(ProtoErrorKind::Message(
"missing value in CAA record options",
)))
}
};

Some(Ok(KeyValue::new(key, value)))
})
.collect::<Result<Vec<_>, ProtoErrorKind>>()?;

Ok(match tag {
Property::Issue => Self::new_issue(critical, name, options),
Property::IssueWild => Self::new_issuewild(critical, name, options),
_ => unreachable!(),
})
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::dbg_macro, clippy::print_stdout)]
Expand Down
29 changes: 29 additions & 0 deletions crates/proto/src/rr/rdata/mx.rs
Expand Up @@ -17,6 +17,7 @@
//! mail exchange, email, record

use std::fmt;
use std::str::FromStr;

#[cfg(feature = "serde-config")]
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -162,6 +163,34 @@ impl fmt::Display for MX {
}
}

// Keep this in sync with the `Display` impl above!
impl FromStr for MX {
type Err = ProtoError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(' ');
let preference = match parts.next() {
Some(part) => u16::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid preference in MX record"))?,
None => return Err(ProtoErrorKind::Message("missing preference in MX record").into()),
};

let exchange = match parts.next() {
Some(part) => Name::from_str(part.trim_matches('"'))
.map_err(|_| ProtoErrorKind::Message("invalid exchange in MX record"))?,
None => return Err(ProtoErrorKind::Message("missing exchange in MX record").into()),
};

if parts.next().is_some() {
return Err(
ProtoErrorKind::Message("unexpected data after exchange in MX record").into(),
);
}

Ok(Self::new(preference, exchange))
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::dbg_macro, clippy::print_stdout)]
Expand Down
61 changes: 61 additions & 0 deletions crates/proto/src/rr/rdata/soa.rs
Expand Up @@ -17,6 +17,7 @@
//! start of authority record defining ownership and defaults for the zone

use std::fmt;
use std::str::FromStr;

#[cfg(feature = "serde-config")]
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -328,6 +329,66 @@ impl fmt::Display for SOA {
}
}

// Keep this in sync with the `Display` impl above!
impl FromStr for SOA {
type Err = ProtoError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(' ');
let mname = match parts.next() {
Some(part) => Name::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing mname in SOA record").into()),
};

let rname = match parts.next() {
Some(part) => Name::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing rname in SOA record").into()),
};

let serial = match parts.next() {
Some(part) => u32::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing serial in SOA record").into()),
};

let refresh = match parts.next() {
Some(part) => i32::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing refresh in SOA record").into()),
};

let retry = match parts.next() {
Some(part) => i32::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing retry in SOA record").into()),
};

let expire = match parts.next() {
Some(part) => i32::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing expire in SOA record").into()),
};

let minimum = match parts.next() {
Some(part) => u32::from_str(part)
.map_err(|_| ProtoErrorKind::Message("invalid mname in SOA record"))?,
None => return Err(ProtoErrorKind::Message("missing minimum in SOA record").into()),
};

if parts.next().is_some() {
return Err(
ProtoErrorKind::Message("unexpected data after minimum in SOA record").into(),
);
}

Ok(Self::new(
mname, rname, serial, refresh, retry, expire, minimum,
))
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::dbg_macro, clippy::print_stdout)]
Expand Down

0 comments on commit 49ef75f

Please sign in to comment.