Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proto: implement FromStr for CAA/MX/SOA record data #1762

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant to the existing parsing in the txt module of the client crate. I think we want to combine these and not duplicate the logic. Was there a reason you couldn't use that logic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware of that logic, I've never really used the client crate -- I guess I don't really understand what it's for? I've so far stuck with proto, resolver and server. Plus, it still seems weird that the server crate depends on the client crate, things like LowerQuery and LowerName feel like they belong in proto more than in client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I’m happy for us to shutdown the client crate at this point. I can review the history with you if you want, but at this point it does seem like the crate is a bit unnecessary. Moving it all into proto seems like a good idea. We could achieve the same goals of the crate by having a feature on proto.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that sounds good to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, not sure we should shut down the entirety of the client crate? I just think a lot of these low-level parts make more sense in proto.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That’s fine. Move txt to proto.

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