1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! Implements the FromStr trait for the various types, to be able to parse in `dig` style.
// Refer to https://github.com/tigeli/bind-utils/blob/master/bin/dig/dig.c for reference.
use crate::Resource;
use crate::Type;
use crate::MX;
use crate::SOA;
use crate::SRV;
use crate::TXT;
use core::num::ParseIntError;
use core::str::FromStr;
use regex::Regex;
use std::net::AddrParseError;
use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FromStrError {
#[error("that resource type doesn't have a text representation")]
UnsupportedType,
#[error("string doesn't match expected format")]
InvalidFormat,
#[error(transparent)]
ParseIntError(#[from] ParseIntError),
#[error(transparent)]
AddrParseError(#[from] AddrParseError),
}
impl Resource {
// Similar to the FromStr but needs the record Type since they are ambiguous.
///
/// # Errors
///
/// Returns an error when the text is not valid for the requested record type,
/// contains an invalid number or address, or the type has no text representation.
pub fn from_str(r#type: Type, s: &str) -> Result<Self, FromStrError> {
Ok(match r#type {
// IP Addresses
Type::A => Resource::A(s.parse()?),
Type::AAAA => Resource::AAAA(s.parse()?),
// Simple strings (domains)
Type::NS => Resource::NS(s.to_string()),
Type::CNAME => Resource::CNAME(s.to_string()),
Type::PTR => Resource::PTR(s.to_string()),
// Complex types
Type::MX => Resource::MX(s.parse()?),
Type::SRV => Resource::SRV(s.parse()?),
Type::SOA => Resource::SOA(s.parse()?),
Type::SPF => Resource::SPF(s.parse()?),
Type::TXT => Resource::TXT(s.parse()?),
// This should never appear in a answer record unless we have invalid data.
Type::Reserved | Type::OPT | Type::ANY => return Err(FromStrError::UnsupportedType),
})
}
}
impl FromStr for SOA {
type Err = FromStrError;
/// Parses an SOA resource from its text representation.
///
/// # Errors
///
/// Returns an error when the text does not contain the seven required SOA fields
/// or a numeric field is invalid.
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
// "ns1.google.com. dns-admin.google.com. 376337657 900 900 1800 60"
// "{mname} {rname} {serial} {refresh} {retry} {expire} {minimum}",
static ref RE: Regex = Regex::new(r"^(\S+) (\S+) (\d+) (\d+) (\d+) (\d+) (\d+)$").unwrap();
}
if let Some(caps) = RE.captures(s) {
let rname = caps[2].to_string();
let rname = match Self::rname_to_email(&rname) {
Ok(name) => name,
Err(_) => rname, // Ignore the error
};
Ok(SOA {
mname: caps[1].to_string(),
rname,
serial: caps[3].parse()?,
refresh: Duration::from_secs(caps[4].parse()?),
retry: Duration::from_secs(caps[5].parse()?),
expire: Duration::from_secs(caps[6].parse()?),
minimum: Duration::from_secs(caps[7].parse()?),
})
} else {
Err(FromStrError::InvalidFormat)
}
}
}
impl FromStr for MX {
type Err = FromStrError;
/// Parses an MX resource from its text representation.
///
/// # Errors
///
/// Returns an error when the preference is not a valid integer or the format is invalid.
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
// "10 aspmx.l.google.com."
// "{preference} {exchange}",
static ref RE: Regex = Regex::new(r"^(\d+) (.+)$").unwrap();
}
if let Some(caps) = RE.captures(s) {
Ok(MX {
preference: caps[1].parse()?,
exchange: caps[2].to_string(),
})
} else {
Err(FromStrError::InvalidFormat)
}
}
}
impl FromStr for SRV {
type Err = FromStrError;
/// Parses an SRV resource from its text representation.
///
/// # Errors
///
/// Returns an error when a priority, weight, or port is not a valid integer or the format is invalid.
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
// "5 0 389 ldap.google.com."
// "{priority} {weight} {port} {name}",
static ref RE: Regex = Regex::new(r"^(\d+) (\d+) (\d+) (.+)$").unwrap();
}
if let Some(caps) = RE.captures(s) {
Ok(SRV {
priority: caps[1].parse()?,
weight: caps[2].parse()?,
port: caps[3].parse()?,
name: caps[4].to_string(),
})
} else {
Err(FromStrError::InvalidFormat)
}
}
}
impl FromStr for TXT {
type Err = FromStrError;
/// Parses a TXT resource from its text representation.
///
/// # Errors
///
/// Returns an error when quoted text is malformed or no quoted segments are found.
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
// TODO Handle escaped quotes
static ref RE: Regex = Regex::new(r#""(.*?)""#).unwrap();
}
if !s.starts_with('"') && !s.ends_with('"') {
// Assume a single unquoted string
return Ok(TXT::from(s));
}
// Otherparse parse multiple "..." strings
let mut txts = Vec::new();
for caps in RE.captures_iter(s) {
txts.push(caps[1].as_bytes().to_vec());
}
if txts.is_empty() {
return Err(FromStrError::InvalidFormat);
}
// TODO Also check we parsed the full record
Ok(TXT(txts))
}
}