use alloc::{
borrow::Cow,
collections::btree_map::{BTreeMap, Entry},
string::String,
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, ParseResult, parse_ttl,
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>,
mut origin: Option<Name>,
) -> Self {
if let Some(origin) = &mut origin {
origin.set_fqdn(true);
}
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(ParseError::UnexpectedToken(t)),
}
}
State::Ttl => match t {
Token::CharData(data) => {
cx.ttl.default = Some(parse_ttl(&data)?);
State::StartLine
}
_ => return Err(ParseError::UnexpectedToken(t)),
},
State::Origin => {
match t {
Token::CharData(data) => {
cx.origin = Some(Name::parse(&data, None)?);
State::StartLine
}
_ => return Err(ParseError::UnexpectedToken(t)),
}
}
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(ParseError::Message(
"Max depth level for nested $INCLUDE is reached",
));
}
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(ParseError::Message(
"Relative $INCLUDE is not supported",
));
}
};
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(ParseError::Message(
"Domain name for $INCLUDE is not supported",
));
}
(t, _) => {
return Err(ParseError::UnexpectedToken(t));
}
},
State::TtlClassType => {
match t {
Token::CharData(mut data) => {
let result: ParseResult<u32> = parse_ttl(&data);
if let Ok(ttl) = result {
cx.ttl.this = Some(ttl);
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(ParseError::UnexpectedToken(t)),
}
}
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(ParseError::UnexpectedToken(t)),
}
}
};
}
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(ParseError::Message("$ORIGIN was not specified"))?;
Ok((origin, cx.records))
}
}
#[derive(Default)]
struct Ttl {
default: Option<u32>,
last: Option<u32>,
this: Option<u32>,
}
impl Ttl {
fn take(&mut self) -> Option<u32> {
if let Some(ttl) = self.this.take() {
self.last.replace(ttl);
return Some(ttl);
}
if let Some(ttl) = self.default {
return Some(ttl);
}
if let Some(ttl) = self.last {
return Some(ttl);
}
None
}
}
struct Context {
origin: Option<Name>,
records: BTreeMap<RrKey, RecordSet>,
class: DNSClass,
current_name: Option<Name>,
rtype: Option<RecordType>,
ttl: Ttl,
}
impl Context {
fn new(origin: Option<Name>) -> Self {
Self {
origin,
records: BTreeMap::default(),
class: DNSClass::IN,
current_name: None,
rtype: None,
ttl: Ttl::default(),
}
}
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::from_tokens(
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 ttl = self
.ttl
.take()
.ok_or_else(|| ParseError::from("record ttl not specified"))?;
name.set_fqdn(true);
let mut record = Record::from_rdata(name, ttl, rdata);
record.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(())
}
}
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
);
}
}