use alloc::{
borrow::Cow,
collections::btree_map::{BTreeMap, Entry},
string::{String, ToString},
vec::Vec,
};
use core::{mem, str::FromStr};
use std::{
fs,
path::{Path, PathBuf},
};
use crate::{
rr::{DNSClass, LowerName, Name, RData, Record, RecordSet, RecordType, RrKey},
serialize::txt::{
ParseError, ParseErrorKind, ParseResult,
parse_rdata::RDataParser,
zone_lex::{Lexer, Token},
},
};
pub struct Parser<'a> {
lexers: Vec<(Lexer<'a>, Option<PathBuf>)>,
origin: Option<Name>,
}
impl<'a> Parser<'a> {
pub fn new(
input: impl Into<Cow<'a, str>>,
path: Option<PathBuf>,
origin: Option<Name>,
) -> Self {
Self {
lexers: vec![(Lexer::new(input), path)],
origin,
}
}
pub fn parse(mut self) -> ParseResult<(Name, BTreeMap<RrKey, RecordSet>)> {
let mut cx = Context::new(self.origin);
let mut state = State::StartLine;
let mut stack = self.lexers.len();
'outer: while let Some((lexer, path)) = self.lexers.last_mut() {
while let Some(t) = lexer.next_token()? {
state = match state {
State::StartLine => {
cx.rtype = None;
match t {
Token::Include => State::Include(None),
Token::Origin => State::Origin,
Token::Ttl => State::Ttl,
Token::CharData(data) => {
cx.current_name = Some(Name::parse(&data, cx.origin.as_ref())?);
State::TtlClassType
}
Token::At => {
cx.current_name.clone_from(&cx.origin); State::TtlClassType
}
Token::Blank => State::TtlClassType,
Token::EOL => State::StartLine, _ => return Err(ParseErrorKind::UnexpectedToken(t).into()),
}
}
State::Ttl => match t {
Token::CharData(data) => {
cx.ttl = Some(Self::parse_time(&data)?);
State::StartLine
}
_ => return Err(ParseErrorKind::UnexpectedToken(t).into()),
},
State::Origin => {
match t {
Token::CharData(data) => {
cx.origin = Some(Name::parse(&data, None)?);
State::StartLine
}
_ => return Err(ParseErrorKind::UnexpectedToken(t).into()),
}
}
State::Include(include_path) => match (t, include_path) {
(Token::CharData(data), None) => State::Include(Some(data)),
(Token::EOL, Some(include_path)) => {
if stack > MAX_INCLUDE_LEVEL {
return Err(ParseErrorKind::Message(
"Max depth level for nested $INCLUDE is reached",
)
.into());
}
let include = Path::new(&include_path);
let include = match (include.is_absolute(), path) {
(true, _) => include.to_path_buf(),
(false, Some(path)) => path
.parent()
.expect("file has to have parent folder")
.join(include),
(false, None) => {
return Err(ParseErrorKind::Message(
"Relative $INCLUDE is not supported",
)
.into());
}
};
let input = fs::read_to_string(&include)?;
let lexer = Lexer::new(input);
self.lexers.push((lexer, Some(include)));
stack += 1;
state = State::StartLine;
continue 'outer;
}
(Token::CharData(_), Some(_)) => {
return Err(ParseErrorKind::Message(
"Domain name for $INCLUDE is not supported",
)
.into());
}
(t, _) => {
return Err(ParseErrorKind::UnexpectedToken(t).into());
}
},
State::TtlClassType => {
match t {
Token::CharData(mut data) => {
let result: ParseResult<u32> = Self::parse_time(&data);
if result.is_ok() {
cx.ttl = result.ok();
State::TtlClassType } else {
data.make_ascii_uppercase();
let result = DNSClass::from_str(&data);
if let Ok(parsed) = result {
cx.class = parsed;
State::TtlClassType
} else {
cx.rtype = Some(RecordType::from_str(&data)?);
State::Record(vec![])
}
}
}
Token::EOL => {
State::StartLine }
_ => return Err(ParseErrorKind::UnexpectedToken(t).into()),
}
}
State::Record(record_parts) => {
match t {
Token::EOL => {
cx.insert(record_parts)?;
State::StartLine
}
Token::CharData(part) => {
let mut record_parts = record_parts;
record_parts.push(part);
State::Record(record_parts)
}
Token::List(list) => {
let mut record_parts = record_parts;
record_parts.extend(list);
State::Record(record_parts)
}
_ => return Err(ParseErrorKind::UnexpectedToken(t).into()),
}
}
};
}
if let State::Record(record_parts) = mem::replace(&mut state, State::StartLine) {
cx.insert(record_parts)?;
}
stack -= 1;
self.lexers.pop();
}
let origin = cx.origin.ok_or_else(|| {
ParseError::from(ParseErrorKind::Message("$ORIGIN was not specified"))
})?;
Ok((origin, cx.records))
}
pub fn parse_time(ttl_str: &str) -> ParseResult<u32> {
if ttl_str.is_empty() {
return Err(ParseErrorKind::ParseTime(ttl_str.to_string()).into());
}
let (mut state, mut value) = (None, 0_u32);
for (i, c) in ttl_str.chars().enumerate() {
let start = match (state, c) {
(None, '0'..='9') => {
state = Some(i);
continue;
}
(Some(_), '0'..='9') => continue,
(Some(start), 'S' | 's' | 'M' | 'm' | 'H' | 'h' | 'D' | 'd' | 'W' | 'w') => start,
_ => return Err(ParseErrorKind::ParseTime(ttl_str.to_string()).into()),
};
#[allow(clippy::char_indices_as_byte_indices)]
let number = u32::from_str(&ttl_str[start..i])
.map_err(|_| ParseErrorKind::ParseTime(ttl_str.to_string()))?;
let multiplier = match c {
'S' | 's' => 1,
'M' | 'm' => 60,
'H' | 'h' => 3_600,
'D' | 'd' => 86_400,
'W' | 'w' => 604_800,
_ => unreachable!(),
};
value = number
.checked_mul(multiplier)
.and_then(|add| value.checked_add(add))
.ok_or_else(|| ParseErrorKind::ParseTime(ttl_str.to_string()))?;
state = None;
}
if let Some(start) = state {
let number = u32::from_str(&ttl_str[start..])
.map_err(|_| ParseErrorKind::ParseTime(ttl_str.to_string()))?;
value = value
.checked_add(number)
.ok_or_else(|| ParseErrorKind::ParseTime(ttl_str.to_string()))?;
}
Ok(value)
}
}
struct Context {
origin: Option<Name>,
records: BTreeMap<RrKey, RecordSet>,
class: DNSClass,
current_name: Option<Name>,
rtype: Option<RecordType>,
ttl: Option<u32>,
}
impl Context {
fn new(origin: Option<Name>) -> Self {
Self {
origin,
records: BTreeMap::default(),
class: DNSClass::IN,
current_name: None,
rtype: None,
ttl: None,
}
}
fn insert(&mut self, record_parts: Vec<String>) -> ParseResult<()> {
let rtype = self
.rtype
.ok_or_else(|| ParseError::from("record type not specified"))?;
let rdata = RData::parse(
rtype,
record_parts.iter().map(AsRef::as_ref),
self.origin.as_ref(),
)?;
let mut name = self
.current_name
.clone()
.ok_or_else(|| ParseError::from("record name not specified"))?;
let set_ttl = match (rtype, self.ttl, &rdata) {
(RecordType::SOA, _, RData::SOA(soa)) => {
let set_ttl = soa.expire() as u32; if self.ttl.is_none() {
self.ttl = Some(soa.minimum());
} set_ttl
}
(RecordType::SOA, _, _) => {
return ParseResult::Err(ParseError::from(format!(
"invalid RData here, expected SOA: {rdata:?}"
)));
}
(_, Some(ttl), _) => ttl,
(_, None, _) => return Err(ParseError::from("record ttl not specified")),
};
name.set_fqdn(true);
let mut record = Record::from_rdata(name, set_ttl, rdata);
record.set_dns_class(self.class);
let entry = self.records.entry(RrKey::new(
LowerName::new(record.name()),
record.record_type(),
));
match (rtype, entry) {
(RecordType::SOA, Entry::Occupied(_)) => {
return Err(ParseError::from("SOA is already specified"));
}
(_, Entry::Vacant(entry)) => {
entry.insert(RecordSet::from(record));
}
(_, Entry::Occupied(mut entry)) => {
entry.get_mut().insert(record, 0);
}
};
Ok(())
}
}
#[allow(unused)]
enum State {
StartLine, TtlClassType, Ttl, Record(Vec<String>),
Include(Option<String>), Origin,
}
const MAX_INCLUDE_LEVEL: usize = 256;
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
#[allow(clippy::uninlined_format_args)]
fn test_zone_parse() {
let domain = Name::from_str("parameter.origin.org.").unwrap();
let zone_data = r#"$ORIGIN parsed.zone.origin.org.
faulty-record-type 60 IN A 1.2.3.4
"#;
let result = Parser::new(zone_data, None, Some(domain)).parse();
assert!(
result.is_err()
& result
.as_ref()
.unwrap_err()
.to_string()
.contains("FAULTY-RECORD-TYPE"),
"unexpected success: {:#?}",
result
);
}
}