Files
NLnetLabs-domain/tests/net/deckard/parse_query.rs
T
Philip-NLnetLabsandGitHub b8d2aa59bf Client Transport (#215)
This PR adds experimental support for client transport of DNS messages.

It also adds a new concept of unstable features which gate code that can introduce breaking changes also in non-breaking releases.
2024-01-15 14:54:20 +01:00

1502 lines
49 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A zonefile scanner keeping data in place.
//!
//! The zonefile scanner provided by this module reads the entire zonefile
//! into memory and tries as much as possible to modify re-use this memory
//! when scanning data. It uses the `Bytes` family of types for safely
//! storing, manipulating, and returning the data and thus requires the
//! `bytes` feature to be enabled.
//!
//! This may or may not be a good strategy. It was primarily implemented to
//! see that the [`Scan`] trait is powerful enough to build such an
//! implementation.
// #![cfg(feature = "bytes")]
// #![cfg_attr(docsrs, doc(cfg(feature = "bytes")))]
use bytes::buf::UninitSlice;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use core::str::FromStr;
use core::{fmt, str};
use domain::base::charstr::CharStr;
use domain::base::iana::{Class, Rtype};
use domain::base::name::{Chain, Dname, RelativeDname, ToDname};
use domain::base::scan::{
BadSymbol, ConvertSymbols, EntrySymbol, Scan, Scanner, ScannerError,
Symbol, SymbolOctetsError,
};
use domain::base::Question;
use domain::base::Ttl;
use domain::dep::octseq::str::Str;
//------------ Type Aliases --------------------------------------------------
/// The type used for scanned domain names.
pub type ScannedDname = Chain<RelativeDname<Bytes>, Dname<Bytes>>;
/// The type used for scanned records.
pub type ScannedQueryRecord = Question<ScannedDname>;
/// The type used for scanned strings.
pub type ScannedString = Str<Bytes>;
//------------ Zonefile ------------------------------------------------------
/// A zonefile to be scanned.
///
/// A value of this types holds data to be scanned in memory and allows
/// fetching entries by acting as an iterator.
///
/// The type implements the `bytes::BufMut` trait for appending data directly
/// into the memory buffer. The function [`load`][Self::load] can be used to
/// create a value directly from a reader.
///
/// Once data has been added, you can simply iterate over the value to
/// get entries. The [`next_entry`][Self::next_entry] method provides an
/// alternative with a more question mark friendly signature.
#[derive(Clone, Debug)]
pub struct Zonefile {
/// This is where we keep the data of the next entry.
buf: SourceBuf,
/// The current origin.
origin: Option<Dname<Bytes>>,
/// The last owner.
last_owner: Option<ScannedDname>,
/// The last TTL.
last_ttl: Option<Ttl>,
/// The last class.
last_class: Option<Class>,
}
impl Zonefile {
/// Creates a new, empty value.
pub fn new() -> Self {
Self::with_buf(SourceBuf::with_empty_buf(BytesMut::new()))
}
/// Creates a new, empty value with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::with_buf(SourceBuf::with_empty_buf(BytesMut::with_capacity(
capacity + 1,
)))
}
/// Creates a new value using the given buffer.
fn with_buf(buf: SourceBuf) -> Self {
Zonefile {
buf,
origin: Some(Dname::root_bytes()),
last_owner: None,
last_ttl: Some(Ttl::ZERO),
last_class: None,
}
}
}
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 {
/// Appends the given slice to the end of the buffer.
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 {
/// Returns the next entry in the zonefile.
///
/// Returns `Ok(None)` if the end of the file has been reached. Returns
/// an error if scanning the next entry failed.
///
/// This method is identical to the `next` method of the iterator
/// implementation but has the return type transposed for easier use
/// with the question mark operator.
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.last_ttl = Some(ttl),
ScannedEntry::Empty => {}
ScannedEntry::Eof => return Ok(None),
}
}
}
/// Returns the origin name of the zonefile.
fn get_origin(&self) -> Result<Dname<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()
}
}
//------------ Entry ---------------------------------------------------------
/// An entry of a zonefile.
#[derive(Clone, Debug)]
pub enum Entry {
/// A DNS record.
QueryRecord(ScannedQueryRecord),
/// An include directive.
///
/// When this entry is encountered, the referenced file should be scanned
/// next. If `origin` is given, this file should be scanned with it as the
/// initial origin name,
Include {
/// The path to the file to be included.
path: ScannedString,
/// The initial origin name of the included file, if provided.
origin: Option<Dname<Bytes>>,
},
}
//------------ ScannedEntry --------------------------------------------------
/// A raw scanned entry of a zonefile.
///
/// This includes all the entry types that we can handle internally and dont
/// have to bubble up to the user.
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
enum ScannedEntry {
/// An entry that should be handed to the user.
Entry(Entry),
/// An `$ORIGIN` directive changing the origin name.
Origin(Dname<Bytes>),
/// A `$TTL` directive changing the default TTL if it isnt given.
Ttl(Ttl),
/// An empty entry.
Empty,
/// The end of file was reached.
Eof,
}
//------------ EntryScanner --------------------------------------------------
/// The entry scanner for a zonefile.
///
/// A value of this type is created for each entry. It implements the
/// [`Scanner`] interface.
#[derive(Debug)]
struct EntryScanner<'a> {
/// The zonefile we are working on.
zonefile: &'a mut Zonefile,
}
impl<'a> EntryScanner<'a> {
/// Creates a new entry scanner using the given zonefile.
fn new(zonefile: &'a mut Zonefile) -> Result<Self, Error> {
Ok(EntryScanner { zonefile })
}
/// Scans a single entry from the zone file.
fn scan_entry(&mut self) -> Result<ScannedEntry, Error> {
self._scan_entry()
.map_err(|err| self.zonefile.buf.error(err))
}
/// Scans a single entry from the zone file.
///
/// This is identical to `scan_entry` but with a more convenient error
/// type.
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 {
// Indented entry: a record with the last owner as the
// owner.
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()
}
}
}
}
/// Scans a regular record.
fn scan_record(&mut self) -> Result<ScannedEntry, EntryError> {
let owner = ScannedDname::scan(self)?;
self.scan_owner_record(owner, true)
}
/// Scans a regular record with an owner name of `@`.
fn scan_at_record(&mut self) -> Result<ScannedEntry, EntryError> {
let owner = RelativeDname::empty_bytes()
.chain(match self.zonefile.origin.as_ref().cloned() {
Some(origin) => origin,
None => return Err(EntryError::missing_origin()),
})
.unwrap(); // Chaining an empty name will always work.
self.scan_owner_record(owner, true)
}
/// Scans a regular record with an explicit owner name.
fn scan_owner_record(
&mut self,
owner: ScannedDname,
new_owner: bool,
) -> Result<ScannedEntry, EntryError> {
let (class, qtype) = self.scan_qcr()?;
if new_owner {
self.zonefile.last_owner = Some(owner.clone());
}
let class = match class {
Some(class) => {
self.zonefile.last_class = Some(class);
class
}
None => match self.zonefile.last_class {
Some(class) => class,
None => return Err(EntryError::missing_last_class()),
},
};
self.zonefile.buf.require_line_feed()?;
Ok(ScannedEntry::Entry(Entry::QueryRecord(Question::new(
owner, qtype, class,
))))
}
/// Scans the class, and type portions of a query record.
fn scan_qcr(&mut self) -> Result<(Option<Class>, Rtype), EntryError> {
// Possible options are:
//
// [<TTL>] [<class>] <type>
// [<class>] [<TTL>] <type>
enum Ctr {
Class(Class),
Qtype(Rtype),
}
let first = self.scan_ascii_str(|s| {
if let Ok(qtype) = Rtype::from_str(s) {
Ok(Ctr::Qtype(qtype))
} else if let Ok(class) = Class::from_str(s) {
Ok(Ctr::Class(class))
} else {
Err(EntryError::expected_qtype())
}
})?;
match first {
Ctr::Class(class) => {
// We have a class. Now there may be a qtype.
let qtype = self.scan_ascii_str(|s| {
if let Ok(qtype) = Rtype::from_str(s) {
Ok(qtype)
} else {
Err(EntryError::expected_qtype())
}
})?;
Ok((Some(class), qtype))
}
Ctr::Qtype(qtype) => Ok((None, qtype)),
}
}
/// Scans a control directive.
fn scan_control(&mut self) -> Result<ScannedEntry, EntryError> {
let ctrl = self.scan_string()?;
if ctrl.eq_ignore_ascii_case("$ORIGIN") {
let origin = self.scan_dname()?.to_dname().unwrap();
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_dname()?.to_dname().unwrap())
} 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())
}
}
}
impl<'a> Scanner for EntryScanner<'a> {
type Octets = Bytes;
type OctetsBuilder = BytesMut;
type Dname = 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 {
self.convert_one_token(&mut convert, &mut write, &mut builder)?;
if self.zonefile.buf.is_line_feed() {
break;
}
}
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()?;
// The result will never be longer than the encoded form, so we can
// trim off everything to the left already.
self.zonefile.buf.trim_to(self.zonefile.buf.start);
// Skip over symbols that dont need converting at the beginning.
while self.zonefile.buf.next_ascii_symbol()?.is_some() {}
// If we arent done yet, we have escaped characters to replace.
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;
}
// Done. `write` marks the end.
self.zonefile.buf.next_item()?;
Ok(self.zonefile.buf.split_to(write).freeze())
}
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()?;
// The result will never be longer than the encoded form, so we can
// trim off everything to the left already.
self.zonefile.buf.trim_to(self.zonefile.buf.start);
let mut write = 0;
// Skip over symbols that dont need converting at the beginning.
while self.zonefile.buf.next_ascii_symbol()?.is_some() {
write += 1;
}
// If we not reached the end of the token, we have escaped characters
// to replace.
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;
}
}
// Done. `write` marks the end. Process via op and return.
let res = op(unsafe {
str::from_utf8_unchecked(&self.zonefile.buf.buf[..write])
})?;
self.zonefile.buf.next_item()?;
Ok(res)
}
fn scan_dname(&mut self) -> Result<Self::Dname, Self::Error> {
// Because the labels in a domain name have their content preceeded
// by the length octet, an unescaped domain name can be almost as is
// if we have one extra octet to the left. Luckily, we always do
// (SourceBuf makes sure of it).
self.zonefile.buf.require_token()?;
// Lets prepare everything. We cut off the bits we dont need with
// the result that the buffers start will be 1 and we set `write`
// to be 0, i.e., the start of the buffer. This also means that write
// will contain the length of the domain name assembled so far, so we
// can easily check if it has gotten too long.
assert!(self.zonefile.buf.start > 0, "missing token prefix space");
self.zonefile.buf.trim_to(self.zonefile.buf.start - 1);
let mut write = 0;
// Now convert label by label.
loop {
let start = write;
match self.convert_label(&mut write)? {
None => {
// End of token right after a dot, so this is an absolute
// name. Unless we have not done anything yet, then we
// have an empty domain name which is just the origin.
self.zonefile.buf.next_item()?;
if start == 0 {
return RelativeDname::empty_bytes()
.chain(self.zonefile.get_origin()?)
.map_err(|_| EntryError::bad_dname());
} else {
return unsafe {
RelativeDname::from_octets_unchecked(
self.zonefile.buf.split_to(write).freeze(),
)
.chain(Dname::root())
.map_err(|_| EntryError::bad_dname())
};
}
}
Some(true) => {
// Last symbol was a dot: check length and continue.
if write > 254 {
return Err(EntryError::bad_dname());
}
}
Some(false) => {
// Reached end of token. This means we have a relative
// dname.
self.zonefile.buf.next_item()?;
return unsafe {
RelativeDname::from_octets_unchecked(
self.zonefile.buf.split_to(write).freeze(),
)
.chain(self.zonefile.get_origin()?)
.map_err(|_| EntryError::bad_dname())
};
}
}
}
}
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()?;
// The result will never be longer than the encoded form, so we can
// trim off everything to the left already.
self.zonefile.buf.trim_to(self.zonefile.buf.start);
// Skip over symbols that dont need converting at the beginning.
while self.zonefile.buf.next_char_symbol()?.is_some() {}
// If we arent done yet, we have escaped characters to replace.
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();
}
// Done. `write` marks the end.
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> {
// Because char-strings are never longer than their representation
// format, we can definitely do this in place. Specifically, we move
// the content around in such a way that by the end we have the result
// in the space of buf before buf.start.
// Reminder: char-string are one length byte followed by that many
// content bytes. We use the byte just before self.read as the length
// byte of the first char-string. This way, if there is only one and
// it isnt escaped, we dont need to move anything at all.
// Lets prepare everything. We cut off the bits we dont need with
// the result that the buffers start will be 1 and we set `write`
// to be 0, i.e., the start of the buffer. This also means that write
// will contain the length of the domain name assembled so far, so we
// can easily check if it has gotten too long.
assert!(self.zonefile.buf.start > 0, "missing token prefix space");
self.zonefile.buf.trim_to(self.zonefile.buf.start - 1);
let mut write = 0;
// Now convert token by token.
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<'a> EntryScanner<'a> {
/// Converts a single token using a token converter.
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()
}
/// Appends output data.
///
/// If the data fits into the portion of the buffer before the current
/// read positiion, puts it there. Otherwise creates a new builder. If
/// it created a new builder or if one was passed in via `builder`,
/// appends the data to that.
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;
}
}
/// Converts a single label of a domain name.
///
/// The next symbol of the buffer should be the first symbol of the
/// labels content. The method reads symbols from the buffer and
/// constructs a single label complete with length octets starting at
/// `write`.
///
/// If it reaches the end of the token before making a label, returns
/// `None`. Otherwise returns whether it encountered a dot at the end of
/// the label. I.e., `Some(true)` means a dot was read as the last symbol
/// and `Some(false)` means the end of token was encountered right after
/// the label.
fn convert_label(
&mut self,
write: &mut usize,
) -> Result<Option<bool>, EntryError> {
let start = *write;
*write += 1;
let latest = *write + 64; // If write goes here, the label is too long
if *write == self.zonefile.buf.start {
// Reading and writing position is equal, so we dont need to
// convert char symbols. Read char symbols until the end of label
// or an escape sequence.
loop {
match self.zonefile.buf.next_ascii_symbol()? {
Some(b'.') => {
// We found an unescaped dot, ie., end of label.
// Update the length octet and return.
self.zonefile.buf.buf[start] =
(*write - start - 1) as u8;
return Ok(Some(true));
}
Some(_) => {
// A char symbol. Just increase the write index.
*write += 1;
if *write >= latest {
return Err(EntryError::bad_dname());
}
}
None => {
// Either we got an escape sequence or we reached the
// end of the token. Break out of the loop and decide
// below.
break;
}
}
}
}
// Now we need to process the label with potential escape sequences.
loop {
match self.zonefile.buf.next_symbol()? {
None => {
// We reached the end of the token.
if *write > start + 1 {
self.zonefile.buf.buf[start] =
(*write - start - 1) as u8;
return Ok(Some(false));
} else {
return Ok(None);
}
}
Some(Symbol::Char('.')) => {
// We found an unescaped dot, ie., end of label.
// Update the length octet and return.
self.zonefile.buf.buf[start] = (*write - start - 1) as u8;
return Ok(Some(true));
}
Some(sym) => {
// Any other symbol: Decode it and proceed to the next
// route.
self.zonefile.buf.buf[*write] = sym.into_octet()?;
*write += 1;
if *write >= latest {
return Err(EntryError::bad_dname());
}
}
}
}
}
/// Converts a character string.
fn convert_charstr(
&mut self,
write: &mut usize,
) -> Result<(), EntryError> {
let start = *write;
*write += 1;
let latest = *write + 255; // If write goes here, charstr is too long
if *write == self.zonefile.buf.start {
// Reading and writing position is equal, so we dont need to
// convert char symbols. Read char symbols until the end of label
// or an escape sequence.
while self.zonefile.buf.next_ascii_symbol()?.is_some() {
*write += 1;
if *write >= latest {
return Err(EntryError::bad_charstr());
}
}
}
// Now we need to process the charstr with potential escape sequences.
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());
}
}
}
}
}
}
//------------ SourceBuf -----------------------------------------------------
/// The buffer to read data from and also into if possible.
#[derive(Clone, Debug)]
struct SourceBuf {
/// The underlying real buffer.
///
/// This buffer contains the data we still need to process. This contains
/// the white space and other octets just before the start of the next
/// token as well since that can be used as extra space for in-place
/// manipulations.
buf: BytesMut,
/// Where in `buf` is the next symbol to read.
start: usize,
/// The category of the current item.
cat: ItemCat,
/// Is the token preceeded by white space?
has_space: bool,
/// How many unclosed opening parentheses did we see at `start`?
parens: usize,
/// The line number of the current line.
line_num: usize,
/// The position of the first character of the current line.
///
/// This may be negative if we cut off bits of the current line.
line_start: isize,
}
impl SourceBuf {
/// Create a new empty buffer.
///
/// Assumes that `buf` is empty. Adds a single byte to the buffer which
/// we would need for parsing if the first token is a domain name.
fn with_empty_buf(mut buf: BytesMut) -> Self {
buf.put_u8(0);
SourceBuf {
buf,
start: 1,
cat: ItemCat::None,
has_space: false,
parens: 0,
line_num: 1,
line_start: 1,
}
}
/// Enriches an entry error with position information.
fn error(&self, err: EntryError) -> Error {
Error {
err,
line: self.line_num,
col: ((self.start as isize) + 1 - self.line_start) as usize,
}
}
/// Checks whether the current item is a token.
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(()),
}
}
/// Returns whether the current item is a line feed.
fn is_line_feed(&self) -> bool {
matches!(self.cat, ItemCat::LineFeed)
}
/// Requires that we have reached a line feed.
fn require_line_feed(&self) -> Result<(), EntryError> {
if self.is_line_feed() {
Ok(())
} else {
Err(EntryError::trailing_tokens())
}
}
/// Returns the next symbol but doesnt advance the buffer.
///
/// Returns `None` if the current item is a line feed or end-of-file
/// or if we have reached the end of token or if it is not a valid symbol.
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)
}
}
}
}
/// Skips over the current token if it contains only an `@` symbol.
///
/// Returns whether it did skip the token.
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)
}
}
}
}
/// Skips over the unknown marker token.
///
/// Returns whether it didskip the token.
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)
}
/// Returns the next symbol of the current token.
///
/// Returns `None` if the current item is a line feed or end-of-file
/// or if we have reached the end of token.
///
/// If it returns `Some(_)`, advances `self.start` to the start of the
/// next symbol.
fn next_symbol(&mut self) -> Result<Option<Symbol>, EntryError> {
self._next_symbol(|sym| Ok(Some(sym)))
}
/// Returns the next symbol if it is an unescaped ASCII symbol.
///
/// Returns `None` if the symbol is escaped or not a printable ASCII
/// character or `self.next_symbol` would return `None`.
///
/// If it returns `Some(_)`, advances `self.start` to the start of the
/// next symbol.
#[allow(clippy::manual_range_contains)] // Hard disagree.
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))
}
/// Returns the next symbol if it is unescaped.
///
/// Returns `None` if the symbol is escaped or `self.next_symbol` would
/// return `None`.
///
/// If it returns `Some(_)`, advances `self.start` to the start of the
/// next symbol.
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)
}
})
}
/// Internal helper for `next_symbol` and friends.
///
/// This only exists so we dont have to copy and paste the fiddely part
/// of the logic. It behaves like `next_symbol` but provides an option
/// for the called to decide whether they want the symbol or not.
#[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))
}
}
}
}
/// Prepares the next item.
///
/// # Panics
///
/// This method must only ever by called if the current item is
/// not a token or if the current token has been read all the way to the
/// end. The latter is true if [`Self::next_symbol`] has returned
/// `Ok(None)` at least once.
///
/// If the current item is a token and has not been read all the way to
/// the end, the method will panic to maintain consistency of the data.
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(());
}
};
// Skip and mark actual white space.
if matches!(ch, b' ' | b'\t' | b'\r') {
self.has_space = true;
self.start += 1;
}
// CR: ignore for compatibility with Windows-style line endings.
else if ch == b'\r' {
self.start += 1;
}
// Opening parenthesis: increase group level.
else if ch == b'(' {
self.parens += 1;
self.start += 1;
}
// Closing parenthesis: decrease group level or error out.
else if ch == b')' {
if self.parens > 0 {
self.parens -= 1;
self.start += 1;
} else {
return Err(EntryError::unbalanced_parens());
}
}
// Semicolon: comment -- skip to line end.
else if ch == b';' {
self.start += 1;
while let Some(true) =
self.buf.get(self.start).map(|ch| *ch != b'\n')
{
self.start += 1;
}
// Next iteration deals with the LF.
}
// Line end: skip over it. Ignore if we are inside a paren group.
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;
}
}
// Double quote: quoted token
else if ch == b'"' {
self.start += 1;
self.cat = ItemCat::Quoted;
break;
}
// Else: unquoted token
else {
self.cat = ItemCat::Unquoted;
break;
}
}
Ok(())
}
/// Splits off the beginning of the buffer up to the given index.
///
/// # Panics
///
/// The method panics if `at` is greater than `self.start`.
fn split_to(&mut self, at: usize) -> BytesMut {
assert!(at <= self.start);
let res = self.buf.split_to(at);
self.start -= at;
self.line_start -= at as isize;
res
}
/// Splits off the beginning of the buffer but doesnt return it.
///
/// # Panics
///
/// The method panics if `at` is greater than `self.start`.
fn trim_to(&mut self, at: usize) {
assert!(at <= self.start);
self.buf.advance(at);
self.start -= at;
self.line_start -= at as isize;
}
}
//------------ ItemCat -------------------------------------------------------
/// The category of the current item in a source buffer.
#[allow(dead_code)] // XXX
#[derive(Clone, Copy, Debug)]
enum ItemCat {
/// We dont currently have an item.
///
/// This is used to indicate that we have reached the end of a token or
/// that we have reached the end of the buffer.
//
// XXX: We might need a separate category for EOF. But lets see if we
// can get away with mixing this up, first.
None,
/// An unquoted normal token.
///
/// This is a token that did not start with a double quote and will end
/// at the next white space.
Unquoted,
/// A quoted normal token.
///
/// This is a token that did start with a double quote and will end at
/// the next unescaped double quote.
///
/// Note that the start position of the buffer indicates the first
/// character that is part of the content, i.e., the position right after
/// the opening double quote.
Quoted,
/// A line feed.
///
/// This is an empty token. The start position is right after the actual
/// line feed.
LineFeed,
}
//------------ EntryError ----------------------------------------------------
/// An error returned by the entry scanner.
#[derive(Debug)]
struct EntryError(&'static str);
impl EntryError {
fn bad_symbol(_err: SymbolOctetsError) -> Self {
EntryError("bad symbol")
}
fn bad_charstr() -> Self {
EntryError("bad charstr")
}
fn bad_dname() -> Self {
EntryError("bad dname")
}
fn unbalanced_parens() -> Self {
EntryError("unbalanced parens")
}
fn missing_last_owner() -> Self {
EntryError("missing last owner")
}
fn missing_last_class() -> Self {
EntryError("missing last class")
}
fn missing_origin() -> Self {
EntryError("missing origin")
}
fn expected_qtype() -> Self {
EntryError("expected qtype")
}
fn unknown_control() -> Self {
EntryError("unknown control")
}
}
impl ScannerError for EntryError {
fn custom(msg: &'static str) -> Self {
EntryError(msg)
}
fn end_of_entry() -> Self {
Self("unexpected end of entry")
}
fn short_buf() -> Self {
Self("short buffer")
}
fn trailing_tokens() -> Self {
Self("trailing tokens")
}
}
impl From<SymbolOctetsError> for EntryError {
fn from(_: SymbolOctetsError) -> Self {
EntryError("symbol octets error")
}
}
impl From<BadSymbol> for EntryError {
fn from(_: BadSymbol) -> Self {
EntryError("bad symbol")
}
}
impl fmt::Display for EntryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.0.as_ref())
}
}
//#[cfg(feature = "std")]
impl std::error::Error for EntryError {}
//------------ Error ---------------------------------------------------------
#[derive(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 {}
//============ Tests =========================================================
/*
#[cfg(test)]
#[cfg(feature = "std")]
mod test {
use super::*;
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_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\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: Dname<Bytes>,
zonefile: std::string::String,
result: Vec<Record<Dname<Bytes>, ZoneRecordData<Bytes, Dname<Bytes>>>>,
}
impl TestCase {
fn test(yaml: &str) {
let case = serde_yaml::from_str::<Self>(yaml).unwrap();
let mut input = case.zonefile.as_bytes();
let mut zone = Zonefile::load(&mut input).unwrap();
zone.set_origin(case.origin);
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;
}
_ => panic!(),
}
}
}
}
#[test]
fn test_data() {
TestCase::test(include_str!("../../test-data/zonefiles/basic.yaml"));
TestCase::test(include_str!("../../test-data/zonefiles/escape.yaml"));
TestCase::test(include_str!("../../test-data/zonefiles/unknown.yaml"));
}
}
*/