#![cfg(feature = "bytes")]
#![cfg_attr(docsrs, doc(cfg(feature = "bytes")))]
use core::str::FromStr;
use core::{fmt, str};
use bytes::buf::UninitSlice;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use octseq::str::Str;
use crate::base::charstr::CharStr;
use crate::base::iana::{Class, Rtype};
use crate::base::name::{Chain, Name, RelativeName, ToName};
use crate::base::record::Record;
use crate::base::scan::{
BadSymbol, ConvertSymbols, EntrySymbol, Scan, Scanner, ScannerError,
Symbol, SymbolOctetsError,
};
use crate::base::Ttl;
use crate::rdata::ZoneRecordData;
pub type ScannedDname = Chain<RelativeName<Bytes>, Name<Bytes>>;
pub type ScannedRecordData = ZoneRecordData<Bytes, ScannedDname>;
pub type ScannedRecord = Record<ScannedDname, ScannedRecordData>;
pub type ScannedString = Str<Bytes>;
#[derive(Clone, Debug)]
pub struct Zonefile {
buf: SourceBuf,
origin: Option<Name<Bytes>>,
last_owner: Option<ScannedDname>,
last_ttl: Ttl,
dollar_ttl: Option<Ttl>,
last_class: Option<Class>,
require_valid: bool,
}
impl Zonefile {
pub fn new() -> Self {
Self::with_buf(SourceBuf::with_empty_buf(BytesMut::new()))
}
pub fn with_capacity(capacity: usize) -> Self {
Self::with_buf(SourceBuf::with_empty_buf(BytesMut::with_capacity(
capacity + 1,
)))
}
pub fn allow_invalid(mut self) -> Self {
self.require_valid = false;
self
}
fn with_buf(buf: SourceBuf) -> Self {
Zonefile {
buf,
origin: None,
last_owner: None,
last_ttl: Ttl::from_secs(3600),
dollar_ttl: None,
last_class: None,
require_valid: true,
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn load(
read: &mut impl std::io::Read,
) -> Result<Self, std::io::Error> {
let mut buf = Self::new().writer();
std::io::copy(read, &mut buf)?;
Ok(buf.into_inner())
}
pub fn current_offset(&self) -> usize {
self.buf.current_offset
}
}
impl Default for Zonefile {
fn default() -> Self {
Self::new()
}
}
impl<'a> From<&'a str> for Zonefile {
fn from(src: &'a str) -> Self {
Self::from(src.as_bytes())
}
}
impl<'a> From<&'a [u8]> for Zonefile {
fn from(src: &'a [u8]) -> Self {
let mut res = Self::with_capacity(src.len() + 1);
res.extend_from_slice(src);
res
}
}
impl Zonefile {
pub fn reserve(&mut self, len: usize) {
self.buf.buf.reserve(len);
}
pub fn extend_from_slice(&mut self, slice: &[u8]) {
self.buf.buf.extend_from_slice(slice)
}
}
unsafe impl BufMut for Zonefile {
fn remaining_mut(&self) -> usize {
self.buf.buf.remaining_mut()
}
unsafe fn advance_mut(&mut self, cnt: usize) {
self.buf.buf.advance_mut(cnt);
}
fn chunk_mut(&mut self) -> &mut UninitSlice {
self.buf.buf.chunk_mut()
}
}
impl Zonefile {
pub fn set_origin(&mut self, origin: Name<Bytes>) {
self.origin = Some(origin);
}
pub fn set_default_class(&mut self, class: Class) {
self.last_class = Some(class);
}
pub fn next_entry(&mut self) -> Result<Option<Entry>, Error> {
loop {
match EntryScanner::new(self)?.scan_entry()? {
ScannedEntry::Entry(entry) => return Ok(Some(entry)),
ScannedEntry::Origin(origin) => self.origin = Some(origin),
ScannedEntry::Ttl(ttl) => self.dollar_ttl = Some(ttl),
ScannedEntry::Empty => {}
ScannedEntry::Eof => return Ok(None),
}
}
}
pub fn origin(&self) -> Result<Name<Bytes>, EntryError> {
self.origin
.as_ref()
.cloned()
.ok_or_else(EntryError::missing_origin)
}
}
impl Iterator for Zonefile {
type Item = Result<Entry, Error>;
fn next(&mut self) -> Option<Self::Item> {
self.next_entry().transpose()
}
}
#[derive(Clone, Debug)]
pub enum Entry {
Record(ScannedRecord),
Include {
path: ScannedString,
origin: Option<Name<Bytes>>,
},
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
enum ScannedEntry {
Entry(Entry),
Origin(Name<Bytes>),
Ttl(Ttl),
Empty,
Eof,
}
#[derive(Debug)]
struct EntryScanner<'a> {
zonefile: &'a mut Zonefile,
}
impl<'a> EntryScanner<'a> {
fn new(zonefile: &'a mut Zonefile) -> Result<Self, Error> {
Ok(EntryScanner { zonefile })
}
fn scan_entry(&mut self) -> Result<ScannedEntry, Error> {
self._scan_entry()
.map_err(|err| self.zonefile.buf.error(err))
}
fn _scan_entry(&mut self) -> Result<ScannedEntry, EntryError> {
self.zonefile.buf.next_item()?;
match self.zonefile.buf.cat {
ItemCat::None => Ok(ScannedEntry::Eof),
ItemCat::LineFeed => Ok(ScannedEntry::Empty),
ItemCat::Unquoted | ItemCat::Quoted => {
if self.zonefile.buf.has_space {
self.scan_owner_record(
match self.zonefile.last_owner.as_ref() {
Some(owner) => owner.clone(),
None => {
return Err(EntryError::missing_last_owner())
}
},
false,
)
} else if self.zonefile.buf.peek_symbol()
== Some(Symbol::Char('$'))
{
self.scan_control()
} else if self.zonefile.buf.skip_at_token()? {
self.scan_at_record()
} else {
self.scan_record()
}
}
}
}
fn scan_record(&mut self) -> Result<ScannedEntry, EntryError> {
let owner = ScannedDname::scan(self)?;
self.scan_owner_record(owner, true)
}
fn scan_at_record(&mut self) -> Result<ScannedEntry, EntryError> {
let owner = RelativeName::empty_bytes()
.chain(match self.zonefile.origin.as_ref().cloned() {
Some(origin) => origin,
None => return Err(EntryError::missing_origin()),
})
.unwrap(); self.scan_owner_record(owner, true)
}
fn scan_owner_record(
&mut self,
owner: ScannedDname,
new_owner: bool,
) -> Result<ScannedEntry, EntryError> {
let (class, ttl, rtype) = self.scan_ctr()?;
if new_owner {
self.zonefile.last_owner = Some(owner.clone());
}
let class = match (class, self.zonefile.last_class) {
(Some(class), Some(last_class)) => {
if self.zonefile.require_valid && class != last_class {
return Err(EntryError::different_class(
last_class, class,
));
}
class
}
(None, Some(last_class)) => last_class,
(Some(class), None) => {
self.zonefile.last_class = Some(class);
class
}
(None, None) => return Err(EntryError::missing_last_class()),
};
let ttl = match ttl {
Some(ttl) => {
self.zonefile.last_ttl = ttl;
ttl
}
None => match self.zonefile.dollar_ttl {
Some(dollar_ttl) => dollar_ttl,
None => self.zonefile.last_ttl,
},
};
let data = ZoneRecordData::scan(rtype, self)?;
self.zonefile.buf.require_line_feed()?;
Ok(ScannedEntry::Entry(Entry::Record(Record::new(
owner, class, ttl, data,
))))
}
fn scan_ctr(
&mut self,
) -> Result<(Option<Class>, Option<Ttl>, Rtype), EntryError> {
enum Ctr {
Class(Class),
Ttl(Ttl),
Rtype(Rtype),
}
let first = self.scan_ascii_str(|s| {
if let Ok(ttl) = u32::from_str(s) {
Ok(Ctr::Ttl(Ttl::from_secs(ttl)))
} else if let Ok(rtype) = Rtype::from_str(s) {
Ok(Ctr::Rtype(rtype))
} else if let Ok(class) = Class::from_str(s) {
Ok(Ctr::Class(class))
} else {
Err(EntryError::expected_rtype())
}
})?;
match first {
Ctr::Ttl(ttl) => {
let second = self.scan_ascii_str(|s| {
if let Ok(rtype) = Rtype::from_str(s) {
Ok(Ok(rtype))
} else if let Ok(class) = Class::from_str(s) {
Ok(Err(class))
} else {
Err(EntryError::expected_rtype())
}
})?;
match second {
Err(class) => {
let rtype = self.scan_ascii_str(|s| {
Rtype::from_str(s)
.map_err(|_| EntryError::expected_rtype())
})?;
Ok((Some(class), Some(ttl), rtype))
}
Ok(rtype) => Ok((None, Some(ttl), rtype)),
}
}
Ctr::Class(class) => {
let second = self.scan_ascii_str(|s| {
if let Ok(ttl) = u32::from_str(s) {
Ok(Err(Ttl::from_secs(ttl)))
} else if let Ok(rtype) = Rtype::from_str(s) {
Ok(Ok(rtype))
} else {
Err(EntryError::expected_rtype())
}
})?;
match second {
Err(ttl) => {
let rtype = self.scan_ascii_str(|s| {
Rtype::from_str(s)
.map_err(|_| EntryError::expected_rtype())
})?;
Ok((Some(class), Some(ttl), rtype))
}
Ok(rtype) => Ok((Some(class), None, rtype)),
}
}
Ctr::Rtype(rtype) => Ok((None, None, rtype)),
}
}
fn scan_control(&mut self) -> Result<ScannedEntry, EntryError> {
let ctrl = self.scan_string()?;
if ctrl.eq_ignore_ascii_case("$ORIGIN") {
let origin = self.scan_name()?.to_name();
self.zonefile.buf.require_line_feed()?;
Ok(ScannedEntry::Origin(origin))
} else if ctrl.eq_ignore_ascii_case("$INCLUDE") {
let path = self.scan_string()?;
let origin = if !self.zonefile.buf.is_line_feed() {
Some(self.scan_name()?.to_name())
} else {
None
};
self.zonefile.buf.require_line_feed()?;
Ok(ScannedEntry::Entry(Entry::Include { path, origin }))
} else if ctrl.eq_ignore_ascii_case("$TTL") {
let ttl = u32::scan(self)?;
self.zonefile.buf.require_line_feed()?;
Ok(ScannedEntry::Ttl(Ttl::from_secs(ttl)))
} else {
Err(EntryError::unknown_control(ctrl))
}
}
}
impl Scanner for EntryScanner<'_> {
type Octets = Bytes;
type OctetsBuilder = BytesMut;
type Name = ScannedDname;
type Error = EntryError;
fn has_space(&self) -> bool {
self.zonefile.buf.has_space
}
fn continues(&mut self) -> bool {
!matches!(self.zonefile.buf.cat, ItemCat::None | ItemCat::LineFeed)
}
fn scan_symbols<F>(&mut self, mut op: F) -> Result<(), Self::Error>
where
F: FnMut(Symbol) -> Result<(), Self::Error>,
{
self.zonefile.buf.require_token()?;
while let Some(sym) = self.zonefile.buf.next_symbol()? {
op(sym)?;
}
self.zonefile.buf.next_item()
}
fn scan_entry_symbols<F>(&mut self, mut op: F) -> Result<(), Self::Error>
where
F: FnMut(EntrySymbol) -> Result<(), Self::Error>,
{
loop {
self.zonefile.buf.require_token()?;
while let Some(sym) = self.zonefile.buf.next_symbol()? {
op(sym.into())?;
}
op(EntrySymbol::EndOfToken)?;
self.zonefile.buf.next_item()?;
if self.zonefile.buf.is_line_feed() {
break;
}
}
Ok(())
}
fn convert_token<C: ConvertSymbols<Symbol, Self::Error>>(
&mut self,
mut convert: C,
) -> Result<Self::Octets, Self::Error> {
let mut write = 0;
let mut builder = None;
self.convert_one_token(&mut convert, &mut write, &mut builder)?;
if let Some(data) = convert.process_tail()? {
self.append_data(data, &mut write, &mut builder);
}
match builder {
Some(builder) => Ok(builder.freeze()),
None => Ok(self.zonefile.buf.split_to(write).freeze()),
}
}
fn convert_entry<C: ConvertSymbols<EntrySymbol, Self::Error>>(
&mut self,
mut convert: C,
) -> Result<Self::Octets, Self::Error> {
let mut write = 0;
let mut builder = None;
loop {
if self.zonefile.buf.is_line_feed() {
break;
}
self.convert_one_token(&mut convert, &mut write, &mut builder)?;
}
if let Some(data) = convert.process_tail()? {
self.append_data(data, &mut write, &mut builder);
}
match builder {
Some(builder) => Ok(builder.freeze()),
None => Ok(self.zonefile.buf.split_to(write).freeze()),
}
}
fn scan_octets(&mut self) -> Result<Self::Octets, Self::Error> {
self.zonefile.buf.require_token()?;
self.zonefile.buf.trim_to(self.zonefile.buf.start);
let is_quoted = self.zonefile.buf.cat == ItemCat::Quoted;
while self.zonefile.buf.next_ascii_symbol()?.is_some() {}
if self.zonefile.buf.cat == ItemCat::None {
let write = if is_quoted {
self.zonefile.buf.start - 1
} else {
self.zonefile.buf.start
};
self.zonefile.buf.next_item()?;
return Ok(self.zonefile.buf.split_to(write).freeze());
}
let mut write = self.zonefile.buf.start;
while let Some(sym) = self.zonefile.buf.next_symbol()? {
self.zonefile.buf.buf[write] = sym.into_octet()?;
write += 1;
}
self.zonefile.buf.next_item()?;
Ok(self.zonefile.buf.split_to(write).freeze())
}
fn scan_svcb_octets(&mut self) -> Result<Self::Octets, Self::Error> {
self.zonefile.buf.require_token()?;
self.zonefile.buf.trim_to(self.zonefile.buf.start);
let mut write;
let is_quoted = self.zonefile.buf.cat == ItemCat::Quoted;
while self.zonefile.buf.next_ascii_symbol()?.is_some() {}
if self.zonefile.buf.cat == ItemCat::None {
let write = if is_quoted {
self.zonefile.buf.start - 1
} else {
self.zonefile.buf.start
};
self.zonefile.buf.next_item()?;
return Ok(self.zonefile.buf.split_to(write).freeze());
}
write = self.zonefile.buf.start;
while let Some(sym) = self.zonefile.buf.next_symbol()? {
self.zonefile.buf.buf[write] = sym.into_octet()?;
write += 1;
}
self.zonefile.buf.next_item()?;
if !self.has_space() && self.zonefile.buf.cat == ItemCat::Quoted {
while let Some(sym) = self.zonefile.buf.next_symbol()? {
self.zonefile.buf.buf[write] = sym.into_octet()?;
write += 1;
}
self.zonefile.buf.next_item()?;
}
let x = self.zonefile.buf.split_to(write).freeze();
Ok(x)
}
fn scan_ascii_str<F, T>(&mut self, op: F) -> Result<T, Self::Error>
where
F: FnOnce(&str) -> Result<T, Self::Error>,
{
self.zonefile.buf.require_token()?;
self.zonefile.buf.trim_to(self.zonefile.buf.start);
let mut write = 0;
while self.zonefile.buf.next_ascii_symbol()?.is_some() {
write += 1;
}
if !matches!(self.zonefile.buf.cat, ItemCat::None) {
while let Some(sym) = self.zonefile.buf.next_symbol()? {
self.zonefile.buf.buf[write] = sym.into_ascii()?;
write += 1;
}
}
let res = op(unsafe {
str::from_utf8_unchecked(&self.zonefile.buf.buf[..write])
})?;
self.zonefile.buf.next_item()?;
Ok(res)
}
fn scan_name(&mut self) -> Result<Self::Name, Self::Error> {
self.zonefile.buf.require_token()?;
assert!(self.zonefile.buf.start > 0, "missing token prefix space");
self.zonefile.buf.trim_to(self.zonefile.buf.start - 1);
let mut write = 0;
loop {
let start = write;
match self.convert_label(&mut write)? {
None => {
self.zonefile.buf.next_item()?;
if start == 0 {
return RelativeName::empty_bytes()
.chain(self.zonefile.origin()?)
.map_err(|_| EntryError::bad_name());
} else {
return unsafe {
RelativeName::from_octets_unchecked(
self.zonefile.buf.split_to(write).freeze(),
)
.chain(Name::root())
.map_err(|_| EntryError::bad_name())
};
}
}
Some(true) => {
if write == 1 {
if self.zonefile.buf.next_symbol()?.is_some() {
return Err(EntryError::bad_name());
} else {
self.zonefile.buf.next_item()?;
return Ok(RelativeName::empty()
.chain(Name::root())
.expect("failed to make root name"));
}
}
if write > 254 {
return Err(EntryError::bad_name());
}
}
Some(false) => {
self.zonefile.buf.next_item()?;
return unsafe {
RelativeName::from_octets_unchecked(
self.zonefile.buf.split_to(write).freeze(),
)
.chain(self.zonefile.origin()?)
.map_err(|_| EntryError::bad_name())
};
}
}
}
}
fn scan_charstr(&mut self) -> Result<CharStr<Self::Octets>, Self::Error> {
self.scan_octets().and_then(|octets| {
CharStr::from_octets(octets)
.map_err(|_| EntryError::bad_charstr())
})
}
fn scan_string(&mut self) -> Result<Str<Self::Octets>, Self::Error> {
self.zonefile.buf.require_token()?;
self.zonefile.buf.trim_to(self.zonefile.buf.start);
while self.zonefile.buf.next_char_symbol()?.is_some() {}
let mut write = self.zonefile.buf.start;
while let Some(sym) = self.zonefile.buf.next_symbol()? {
write += sym
.into_char()?
.encode_utf8(
&mut self.zonefile.buf.buf
[write..self.zonefile.buf.start],
)
.len();
}
self.zonefile.buf.next_item()?;
Ok(unsafe {
Str::from_utf8_unchecked(
self.zonefile.buf.split_to(write).freeze(),
)
})
}
fn scan_charstr_entry(&mut self) -> Result<Self::Octets, Self::Error> {
assert!(self.zonefile.buf.start > 0, "missing token prefix space");
self.zonefile.buf.trim_to(self.zonefile.buf.start - 1);
let mut write = 0;
loop {
self.convert_charstr(&mut write)?;
if self.zonefile.buf.is_line_feed() {
break;
}
}
Ok(self.zonefile.buf.split_to(write).freeze())
}
fn scan_opt_unknown_marker(&mut self) -> Result<bool, Self::Error> {
self.zonefile.buf.skip_unknown_marker()
}
fn octets_builder(&mut self) -> Result<Self::OctetsBuilder, Self::Error> {
Ok(BytesMut::new())
}
}
impl EntryScanner<'_> {
fn convert_one_token<
S: From<Symbol>,
C: ConvertSymbols<S, EntryError>,
>(
&mut self,
convert: &mut C,
write: &mut usize,
builder: &mut Option<BytesMut>,
) -> Result<(), EntryError> {
self.zonefile.buf.require_token()?;
while let Some(sym) = self.zonefile.buf.next_symbol()? {
if let Some(data) = convert.process_symbol(sym.into())? {
self.append_data(data, write, builder);
}
}
self.zonefile.buf.next_item()
}
fn append_data(
&mut self,
data: &[u8],
write: &mut usize,
builder: &mut Option<BytesMut>,
) {
if let Some(builder) = builder.as_mut() {
builder.extend_from_slice(data);
return;
}
let new_write = *write + data.len();
if new_write > self.zonefile.buf.start {
let mut new_builder = BytesMut::with_capacity(new_write);
new_builder.extend_from_slice(&self.zonefile.buf.buf[..*write]);
new_builder.extend_from_slice(data);
*builder = Some(new_builder);
} else {
self.zonefile.buf.buf[*write..new_write].copy_from_slice(data);
*write = new_write;
}
}
fn convert_label(
&mut self,
write: &mut usize,
) -> Result<Option<bool>, EntryError> {
let start = *write;
*write += 1;
let latest = *write + 64; if *write == self.zonefile.buf.start {
loop {
match self.zonefile.buf.next_ascii_symbol()? {
Some(b'.') => {
self.zonefile.buf.buf[start] =
(*write - start - 1) as u8;
return Ok(Some(true));
}
Some(_) => {
*write += 1;
if *write >= latest {
return Err(EntryError::bad_name());
}
}
None => {
break;
}
}
}
}
loop {
match self.zonefile.buf.next_symbol()? {
None => {
if *write > start + 1 {
self.zonefile.buf.buf[start] =
(*write - start - 1) as u8;
return Ok(Some(false));
} else {
*write = start;
return Ok(None);
}
}
Some(Symbol::Char('.')) => {
self.zonefile.buf.buf[start] = (*write - start - 1) as u8;
return Ok(Some(true));
}
Some(sym) => {
self.zonefile.buf.buf[*write] = sym.into_octet()?;
*write += 1;
if *write >= latest {
return Err(EntryError::bad_name());
}
}
}
}
}
fn convert_charstr(
&mut self,
write: &mut usize,
) -> Result<(), EntryError> {
let start = *write;
*write += 1;
let latest = *write + 255; if *write == self.zonefile.buf.start {
while self.zonefile.buf.next_ascii_symbol()?.is_some() {
*write += 1;
if *write > latest {
return Err(EntryError::bad_charstr());
}
}
}
loop {
match self.zonefile.buf.next_symbol()? {
None => {
self.zonefile.buf.next_item()?;
self.zonefile.buf.buf[start] = (*write - start - 1) as u8;
return Ok(());
}
Some(sym) => {
self.zonefile.buf.buf[*write] = sym.into_octet()?;
*write += 1;
if *write > latest {
return Err(EntryError::bad_charstr());
}
}
}
}
}
}
#[derive(Clone, Debug)]
struct SourceBuf {
buf: BytesMut,
current_offset: usize,
start: usize,
cat: ItemCat,
has_space: bool,
parens: usize,
line_num: usize,
line_start: isize,
}
impl SourceBuf {
fn with_empty_buf(mut buf: BytesMut) -> Self {
buf.put_u8(0);
SourceBuf {
buf,
current_offset: 0,
start: 1,
cat: ItemCat::None,
has_space: false,
parens: 0,
line_num: 1,
line_start: 1,
}
}
fn error(&self, err: EntryError) -> Error {
Error {
err,
line: self.line_num,
col: ((self.start as isize) + 1 - self.line_start) as usize,
}
}
fn require_token(&self) -> Result<(), EntryError> {
match self.cat {
ItemCat::None => Err(EntryError::short_buf()),
ItemCat::LineFeed => Err(EntryError::end_of_entry()),
ItemCat::Quoted | ItemCat::Unquoted => Ok(()),
}
}
fn is_line_feed(&self) -> bool {
matches!(self.cat, ItemCat::LineFeed)
}
fn require_line_feed(&self) -> Result<(), EntryError> {
if self.is_line_feed() {
Ok(())
} else {
Err(EntryError::trailing_tokens())
}
}
fn peek_symbol(&self) -> Option<Symbol> {
match self.cat {
ItemCat::None | ItemCat::LineFeed => None,
ItemCat::Unquoted => {
let sym =
match Symbol::from_slice_index(&self.buf, self.start) {
Ok(Some((sym, _))) => sym,
Ok(None) | Err(_) => return None,
};
if sym.is_word_char() {
Some(sym)
} else {
None
}
}
ItemCat::Quoted => {
let sym =
match Symbol::from_slice_index(&self.buf, self.start) {
Ok(Some((sym, _))) => sym,
Ok(None) | Err(_) => return None,
};
if sym == Symbol::Char('"') {
None
} else {
Some(sym)
}
}
}
}
fn skip_at_token(&mut self) -> Result<bool, EntryError> {
if self.peek_symbol() != Some(Symbol::Char('@')) {
return Ok(false);
}
let (sym, sym_end) =
match Symbol::from_slice_index(&self.buf, self.start + 1) {
Ok(Some((sym, sym_end))) => (sym, sym_end),
Ok(None) => return Err(EntryError::short_buf()),
Err(err) => return Err(EntryError::bad_symbol(err)),
};
match self.cat {
ItemCat::None | ItemCat::LineFeed => unreachable!(),
ItemCat::Unquoted => {
if !sym.is_word_char() {
self.start += 1;
self.cat = ItemCat::None;
self.next_item()?;
Ok(true)
} else {
Ok(false)
}
}
ItemCat::Quoted => {
if sym == Symbol::Char('"') {
self.start = sym_end;
self.cat = ItemCat::None;
self.next_item()?;
Ok(true)
} else {
Ok(false)
}
}
}
}
fn skip_unknown_marker(&mut self) -> Result<bool, EntryError> {
if !matches!(self.cat, ItemCat::Unquoted) {
return Ok(false);
}
let (sym, sym_end) =
match Symbol::from_slice_index(&self.buf, self.start) {
Ok(Some(some)) => some,
_ => return Ok(false),
};
if sym != Symbol::SimpleEscape(b'#') {
return Ok(false);
}
let (sym, sym_end) =
match Symbol::from_slice_index(&self.buf, sym_end) {
Ok(Some(some)) => some,
_ => return Ok(false),
};
if sym.is_word_char() {
return Ok(false);
}
self.start = sym_end;
self.cat = ItemCat::None;
self.next_item()?;
Ok(true)
}
fn next_symbol(&mut self) -> Result<Option<Symbol>, EntryError> {
self._next_symbol(|sym| Ok(Some(sym)))
}
#[allow(clippy::manual_range_contains)] fn next_ascii_symbol(&mut self) -> Result<Option<u8>, EntryError> {
if matches!(self.cat, ItemCat::None | ItemCat::LineFeed) {
return Ok(None);
}
let ch = match self.buf.get(self.start) {
Some(ch) => *ch,
None => return Ok(None),
};
match self.cat {
ItemCat::Unquoted => {
if ch < 0x21
|| ch > 0x7F
|| ch == b'"'
|| ch == b'('
|| ch == b')'
|| ch == b';'
|| ch == b'\\'
{
return Ok(None);
}
}
ItemCat::Quoted => {
if ch == b'"' {
self.start += 1;
self.cat = ItemCat::None;
return Ok(None);
} else if ch < 0x21 || ch > 0x7F || ch == b'\\' {
return Ok(None);
}
}
_ => unreachable!(),
}
self.start += 1;
Ok(Some(ch))
}
fn next_char_symbol(&mut self) -> Result<Option<char>, EntryError> {
self._next_symbol(|sym| {
if let Symbol::Char(ch) = sym {
Ok(Some(ch))
} else {
Ok(None)
}
})
}
#[inline]
fn _next_symbol<F, T>(&mut self, want: F) -> Result<Option<T>, EntryError>
where
F: Fn(Symbol) -> Result<Option<T>, EntryError>,
{
match self.cat {
ItemCat::None | ItemCat::LineFeed => Ok(None),
ItemCat::Unquoted => {
let (sym, sym_end) =
match Symbol::from_slice_index(&self.buf, self.start) {
Ok(Some((sym, sym_end))) => (sym, sym_end),
Ok(None) => return Err(EntryError::short_buf()),
Err(err) => return Err(EntryError::bad_symbol(err)),
};
if !sym.is_word_char() {
self.cat = ItemCat::None;
Ok(None)
} else {
match want(sym)? {
Some(some) => {
self.start = sym_end;
Ok(Some(some))
}
None => Ok(None),
}
}
}
ItemCat::Quoted => {
let (sym, sym_end) =
match Symbol::from_slice_index(&self.buf, self.start) {
Ok(Some((sym, sym_end))) => (sym, sym_end),
Ok(None) => return Err(EntryError::short_buf()),
Err(err) => return Err(EntryError::bad_symbol(err)),
};
let res = match want(sym)? {
Some(some) => some,
None => return Ok(None),
};
if sym == Symbol::Char('"') {
self.start = sym_end;
self.cat = ItemCat::None;
Ok(None)
} else {
self.start = sym_end;
if sym == Symbol::Char('\n') {
self.line_num += 1;
self.line_start = self.start as isize;
}
Ok(Some(res))
}
}
}
}
fn next_item(&mut self) -> Result<(), EntryError> {
assert!(
matches!(self.cat, ItemCat::None | ItemCat::LineFeed),
"token not completely read ({:?} at {}:{})",
self.cat,
self.line_num,
((self.start as isize) + 1 - self.line_start) as usize,
);
self.has_space = false;
loop {
let ch = match self.buf.get(self.start) {
Some(&ch) => ch,
None => {
self.cat = ItemCat::None;
return Ok(());
}
};
if matches!(ch, b' ' | b'\t' | b'\r') {
self.has_space = true;
self.start += 1;
}
else if ch == b'\r' {
self.start += 1;
}
else if ch == b'(' {
self.parens += 1;
self.start += 1;
}
else if ch == b')' {
if self.parens > 0 {
self.parens -= 1;
self.start += 1;
} else {
return Err(EntryError::unbalanced_parens());
}
}
else if ch == b';' {
self.start += 1;
while let Some(true) =
self.buf.get(self.start).map(|ch| *ch != b'\n')
{
self.start += 1;
}
}
else if ch == b'\n' {
self.start += 1;
self.line_num += 1;
self.line_start = self.start as isize;
if self.parens == 0 {
self.cat = ItemCat::LineFeed;
break;
}
}
else if ch == b'"' {
self.start += 1;
self.cat = ItemCat::Quoted;
break;
}
else {
self.cat = ItemCat::Unquoted;
break;
}
}
Ok(())
}
fn split_to(&mut self, at: usize) -> BytesMut {
assert!(at <= self.start);
let res = self.buf.split_to(at);
self.current_offset += at;
self.start -= at;
self.line_start -= at as isize;
res
}
fn trim_to(&mut self, at: usize) {
assert!(at <= self.start);
self.buf.advance(at);
self.current_offset += at;
self.start -= at;
self.line_start -= at as isize;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ItemCat {
None,
Unquoted,
Quoted,
LineFeed,
}
#[derive(Clone, Debug)]
pub struct EntryError {
msg: &'static str,
#[cfg(feature = "std")]
context: Option<std::string::String>,
}
impl EntryError {
fn bad_symbol(_err: SymbolOctetsError) -> Self {
EntryError {
msg: "bad symbol",
#[cfg(feature = "std")]
context: Some(format!("{}", _err)),
}
}
fn bad_charstr() -> Self {
EntryError {
msg: "bad charstr",
#[cfg(feature = "std")]
context: None,
}
}
fn bad_name() -> Self {
EntryError {
msg: "bad name",
#[cfg(feature = "std")]
context: None,
}
}
fn unbalanced_parens() -> Self {
EntryError {
msg: "unbalanced parens",
#[cfg(feature = "std")]
context: None,
}
}
fn missing_last_owner() -> Self {
EntryError {
msg: "missing last owner",
#[cfg(feature = "std")]
context: None,
}
}
fn missing_last_class() -> Self {
EntryError {
msg: "missing last class",
#[cfg(feature = "std")]
context: None,
}
}
fn missing_origin() -> Self {
EntryError {
msg: "missing origin",
#[cfg(feature = "std")]
context: None,
}
}
fn expected_rtype() -> Self {
EntryError {
msg: "expected rtype",
#[cfg(feature = "std")]
context: None,
}
}
fn unknown_control(ctrl: Str<Bytes>) -> Self {
EntryError {
msg: "unknown control",
#[cfg(feature = "std")]
context: Some(format!("{}", ctrl)),
}
}
fn different_class(expected_class: Class, found_class: Class) -> Self {
EntryError {
msg: "different class",
#[cfg(feature = "std")]
context: Some(format!("{found_class} != {expected_class}")),
}
}
}
impl ScannerError for EntryError {
fn custom(msg: &'static str) -> Self {
EntryError {
msg,
#[cfg(feature = "std")]
context: None,
}
}
fn end_of_entry() -> Self {
Self::custom("unexpected end of entry")
}
fn short_buf() -> Self {
Self::custom("short buffer")
}
fn trailing_tokens() -> Self {
Self::custom("trailing tokens")
}
}
impl From<SymbolOctetsError> for EntryError {
fn from(err: SymbolOctetsError) -> Self {
Self::bad_symbol(err)
}
}
impl From<BadSymbol> for EntryError {
fn from(_: BadSymbol) -> Self {
Self::custom("bad symbol")
}
}
impl fmt::Display for EntryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.msg)?;
#[cfg(feature = "std")]
if let Some(context) = &self.context {
write!(f, ": {}", context)?;
}
Ok(())
}
}
#[cfg(feature = "std")]
impl std::error::Error for EntryError {}
#[derive(Clone, Debug)]
pub struct Error {
err: EntryError,
line: usize,
col: usize,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}: {}", self.line, self.col, self.err)
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
#[cfg(test)]
#[cfg(feature = "std")]
mod test {
use super::*;
use crate::base::ParsedName;
use octseq::Parser;
use std::vec::Vec;
fn with_entry(s: &str, op: impl FnOnce(EntryScanner<'_>)) {
let mut zone = Zonefile::with_capacity(s.len());
zone.extend_from_slice(s.as_bytes());
let entry = EntryScanner::new(&mut zone).unwrap();
entry.zonefile.buf.next_item().unwrap();
op(entry)
}
#[test]
fn scan_octets() {
#[track_caller]
fn test(zone: &str, tok: impl AsRef<[u8]>) {
with_entry(zone, |mut entry| {
let res = entry.scan_octets().unwrap();
assert_eq!(&res[..], tok.as_ref());
});
}
test(" unquoted\r\n", b"unquoted");
test(" unquoted ", b"unquoted");
test("unquoted ", b"unquoted");
test("unqu\\oted ", b"unquoted");
test("unqu\\111ted ", b"unquoted");
test(" \"quoted\"\n", b"quoted");
test(" \"quoted\" ", b"quoted");
test("\"quoted\" ", b"quoted");
test("\"qu\\oted\"", b"quoted");
test(" \"qu\\\\ot\\\\ed\" ", b"qu\\ot\\ed");
}
#[test]
fn scan_symbols() {
fn test(zone: &str, tok: impl AsRef<[u8]>) {
with_entry(zone, |mut entry| {
let mut tok = tok.as_ref();
entry
.scan_symbols(|sym| {
let sym = sym.into_octet().unwrap();
assert_eq!(sym, tok[0]);
tok = &tok[1..];
Ok(())
})
.unwrap();
});
}
test(" unquoted\r\n", b"unquoted");
test(" unquoted ", b"unquoted");
test("unquoted ", b"unquoted");
test("unqu\\oted ", b"unquoted");
test("unqu\\111ted ", b"unquoted");
test(" \"quoted\"\n", b"quoted");
test(" \"quoted\" ", b"quoted");
test("\"quoted\" ", b"quoted");
}
#[derive(serde::Deserialize)]
#[allow(clippy::type_complexity)]
struct TestCase {
origin: Name<Bytes>,
default_class: Option<Class>,
zonefile: std::string::String,
result: Vec<Record<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>>,
#[serde(default)]
allow_invalid: bool,
}
impl From<&str> for TestCase {
fn from(yaml: &str) -> Self {
yaml_serde::from_str(yaml).unwrap()
}
}
impl TestCase {
fn test<T: Into<TestCase>>(case: T) {
let case = case.into();
let mut input = case.zonefile.as_bytes();
let mut zone = Zonefile::load(&mut input).unwrap();
if case.allow_invalid {
zone = zone.allow_invalid();
}
zone.set_origin(case.origin);
if let Some(class) = case.default_class {
zone.set_default_class(class);
}
let mut result = case.result.as_slice();
while let Some(entry) = zone.next_entry().unwrap() {
match entry {
Entry::Record(record) => {
let (first, tail) = result.split_first().unwrap();
assert_eq!(first, &record);
result = tail;
let mut buf = BytesMut::new();
record.compose(&mut buf).unwrap();
let buf = buf.freeze();
let mut parser = Parser::from_ref(&buf);
let parsed =
Record::<
ParsedName<Bytes>,
ZoneRecordData<Bytes, ParsedName<Bytes>>,
>::parse(&mut parser)
.unwrap()
.unwrap();
if !matches!(
record.data(),
ZoneRecordData::Unknown(_)
) {
assert_eq!(first, &parsed);
assert_eq!(first.ttl(), parsed.ttl());
}
}
_ => panic!(),
}
}
}
}
#[test]
fn test_basic_yaml() {
TestCase::test(include_str!("../../test-data/zonefiles/basic.yaml"));
}
#[test]
fn test_escape_yaml() {
TestCase::test(include_str!("../../test-data/zonefiles/escape.yaml"));
}
#[test]
fn test_unknown_yaml() {
TestCase::test(include_str!(
"../../test-data/zonefiles/unknown.yaml"
));
}
#[test]
fn test_unknown_zero_length_yaml() {
TestCase::test(include_str!(
"../../test-data/zonefiles/unknown-zero-length.yaml"
));
}
#[test]
fn test_default_and_last_class() {
TestCase::test(include_str!(
"../../test-data/zonefiles/defaultclass.yaml"
));
}
#[test]
#[should_panic(expected = "different class")]
fn test_rfc1035_same_class_validity_check() {
TestCase::test(include_str!(
"../../test-data/zonefiles/mixedclass.yaml"
));
}
#[test]
fn test_rfc1035_validity_checks_override() {
let mut case = TestCase::from(include_str!(
"../../test-data/zonefiles/mixedclass.yaml"
));
case.allow_invalid = true;
TestCase::test(case);
}
#[test]
fn test_chrstr_decoding() {
TestCase::test(include_str!("../../test-data/zonefiles/strlen.yaml"));
}
#[test]
#[should_panic(expected = "character string with more than 255 octets")]
fn test_chrstr_overflow_decoding() {
TestCase::test(include_str!(
"../../test-data/zonefiles/stroverflow.yaml"
));
}
#[test]
fn test_multiple_dollar_ttls_multiple_missing_ttls() {
TestCase::test(include_str!(
"../../test-data/zonefiles/multiple_dollar_ttls_multiple_missing_ttls.yaml"
))
}
#[test]
fn test_multiple_dollar_ttls_no_missing_ttls() {
TestCase::test(include_str!(
"../../test-data/zonefiles/multiple_dollar_ttls_no_missing_ttls.yaml"
))
}
#[test]
fn test_no_dollar_ttl_no_missing_ttls() {
TestCase::test(include_str!(
"../../test-data/zonefiles/no_dollar_ttl_no_missing_ttls.yaml"
))
}
#[test]
fn test_no_dollar_ttl_one_missing_ttl() {
TestCase::test(include_str!(
"../../test-data/zonefiles/no_dollar_ttl_one_missing_ttl.yaml"
))
}
#[test]
fn test_top_dollar_ttl_and_missing_ttl() {
TestCase::test(include_str!(
"../../test-data/zonefiles/top_dollar_ttl_and_missing_ttl.yaml"
))
}
#[test]
fn test_top_dollar_ttl_no_missing_ttls() {
TestCase::test(include_str!(
"../../test-data/zonefiles/top_dollar_ttl_no_missing_ttls.yaml"
))
}
#[test]
fn test_rfc_1035_class_ttl_type_rdata() {
TestCase::test(include_str!(
"../../test-data/zonefiles/rfc_1035_class_ttl_type_rdata.yaml"
))
}
#[test]
fn test_rfc_1035_ttl_class_type_rdata() {
TestCase::test(include_str!(
"../../test-data/zonefiles/rfc_1035_ttl_class_type_rdata.yaml"
))
}
}