use std::collections::{BTreeMap, HashMap};
use std::vec::Vec;
use crate::base::iana::{Class, Rtype};
use crate::base::name::{FlattenInto, ToName};
use crate::base::Name;
use crate::rdata::ZoneRecordData;
use crate::zonefile::inplace::{self, Entry};
use crate::zonetree::ZoneBuilder;
use crate::zonetree::{Rrset, SharedRr};
use super::error::{ContextError, RecordError, ZoneErrors};
use super::types::{StoredName, StoredRecord};
#[derive(Clone, Default)]
pub struct Zonefile {
origin: Option<StoredName>,
class: Option<Class>,
normal: Owners<Normal>,
zone_cuts: Owners<ZoneCut>,
cnames: Owners<SharedRr>,
out_of_zone: Owners<Normal>,
}
impl Zonefile {
pub fn new(apex: StoredName, class: Class) -> Self {
Zonefile {
origin: Some(apex),
class: Some(class),
..Default::default()
}
}
}
impl Zonefile {
pub fn set_origin(&mut self, origin: StoredName) {
self.origin = Some(origin)
}
#[allow(clippy::result_large_err)]
pub fn insert(
&mut self,
record: StoredRecord,
) -> Result<(), RecordError> {
if self.origin.is_none() {
if record.rtype() != Rtype::SOA {
return Err(RecordError::MissingSoa(record));
} else {
let apex = record.owner().to_name();
self.class = Some(record.class());
self.origin = Some(apex);
}
}
let (zone_apex, zone_class) =
(self.origin().unwrap(), self.class().unwrap());
if record.class() != zone_class {
return Err(RecordError::ClassMismatch(record, zone_class));
}
if !record.owner().ends_with(zone_apex) {
self.out_of_zone
.entry(record.owner().clone())
.insert(record);
Ok(())
} else {
match record.rtype() {
Rtype::NS | Rtype::DS if record.owner() != zone_apex => {
let incompatible_normal_record = self
.normal
.get(record.owner())
.and_then(|normal| normal.first_non_glue());
if let Some((&rtype, _)) = incompatible_normal_record {
Err(RecordError::IllegalZoneCut(record, rtype))
} else if self.cnames.contains(record.owner()) {
Err(RecordError::IllegalZoneCut(record, Rtype::CNAME))
} else {
self.zone_cuts
.entry(record.owner().clone())
.insert(record);
Ok(())
}
}
Rtype::CNAME => {
if let Some(normal_records) =
self.normal.get(record.owner())
{
let rtype = normal_records.sample_rtype().unwrap();
Err(RecordError::IllegalCname(record, rtype))
} else if let Some(zone_cut) =
self.zone_cuts.get(record.owner())
{
let rtype = zone_cut.sample_rtype().unwrap();
Err(RecordError::IllegalCname(record, rtype))
} else if self.cnames.contains(record.owner()) {
Err(RecordError::MultipleCnames(record))
} else {
self.cnames
.insert(record.owner().clone(), record.into());
Ok(())
}
}
_ => {
let incompatible_zone_cut = match record.rtype().is_glue()
{
true => None,
false => self.zone_cuts.get(record.owner()),
};
if let Some(zone_cut) = incompatible_zone_cut {
let rtype = zone_cut.sample_rtype().unwrap();
Err(RecordError::IllegalRecord(record, rtype))
} else if self.cnames.contains(record.owner()) {
Err(RecordError::IllegalRecord(record, Rtype::CNAME))
} else {
self.normal
.entry(record.owner().clone())
.insert(record);
Ok(())
}
}
}
}
}
}
impl Zonefile {
pub fn origin(&self) -> Option<&StoredName> {
self.origin.as_ref()
}
pub fn class(&self) -> Option<Class> {
self.class
}
pub fn normal(&self) -> &Owners<Normal> {
&self.normal
}
pub fn zone_cuts(&self) -> &Owners<ZoneCut> {
&self.zone_cuts
}
pub fn cnames(&self) -> &Owners<SharedRr> {
&self.cnames
}
pub fn out_of_zone(&self) -> &Owners<Normal> {
&self.out_of_zone
}
}
impl TryFrom<Zonefile> for ZoneBuilder {
type Error = ZoneErrors<ContextError>;
fn try_from(mut zonefile: Zonefile) -> Result<Self, Self::Error> {
let mut builder = ZoneBuilder::new(
zonefile.origin.unwrap(),
zonefile.class.unwrap(),
);
let mut errors = ZoneErrors::<ContextError>::default();
for (name, cut) in zonefile.zone_cuts.into_iter() {
let ns = match cut.ns {
Some(ns) => ns.into_shared(),
None => {
errors.add_error(name, ContextError::MissingNs);
continue;
}
};
let ds = cut.ds.map(Rrset::into_shared);
let mut glue = vec![];
for rdata in ns.data() {
if let ZoneRecordData::Ns(ns) = rdata {
glue.append(
&mut zonefile.normal.collect_glue(ns.nsdname()),
);
}
}
if let Err(err) = builder.insert_zone_cut(&name, ns, ds, glue) {
errors.add_error(name, ContextError::InvalidZonecut(err))
}
}
for (name, rrset) in zonefile.cnames.into_iter() {
if let Err(err) = builder.insert_cname(&name, rrset) {
errors.add_error(name, ContextError::InvalidCname(err))
}
}
for (name, rrsets) in zonefile.normal.into_iter() {
for (rtype, rrset) in rrsets.into_iter() {
if builder.insert_rrset(&name, rrset.into_shared()).is_err() {
errors.add_error(
name.clone(),
ContextError::OutOfZone(rtype),
);
}
}
}
for (name, rrsets) in zonefile.out_of_zone.into_iter() {
for (rtype, _) in rrsets.into_iter() {
errors
.add_error(name.clone(), ContextError::OutOfZone(rtype));
}
}
errors.unwrap().map(|_| builder)
}
}
impl TryFrom<inplace::Zonefile> for Zonefile {
type Error = ZoneErrors<RecordError>;
fn try_from(source: inplace::Zonefile) -> Result<Self, Self::Error> {
let mut zonefile = Zonefile::default();
let mut errors = ZoneErrors::<RecordError>::default();
for res in source {
match res.map_err(RecordError::MalformedRecord) {
Ok(Entry::Record(r)) => {
let stored_rec = r.flatten_into();
let name = stored_rec.owner().clone();
if let Err(err) = zonefile.insert(stored_rec) {
errors.add_error(name, err);
}
}
Ok(Entry::Include { .. }) => {
}
Err(err) => {
match err.owner() {
Some(name) => errors.add_error(name.clone(), err),
None => errors.add_error(Name::root_bytes(), err),
}
return Err(errors);
}
}
}
if errors.is_empty() {
Ok(zonefile)
} else {
Err(errors)
}
}
}
#[derive(Clone)]
pub struct Owners<Content> {
owners: BTreeMap<StoredName, Content>,
}
impl<Content> Owners<Content> {
fn contains(&self, name: &StoredName) -> bool {
self.owners.contains_key(name)
}
fn get(&self, name: &StoredName) -> Option<&Content> {
self.owners.get(name)
}
fn insert(&mut self, name: StoredName, content: Content) -> bool {
use std::collections::btree_map::Entry;
match self.owners.entry(name) {
Entry::Occupied(_) => false,
Entry::Vacant(vacant) => {
vacant.insert(content);
true
}
}
}
fn entry(&mut self, name: StoredName) -> &mut Content
where
Content: Default,
{
self.owners.entry(name).or_default()
}
fn into_iter(self) -> impl Iterator<Item = (StoredName, Content)> {
self.owners.into_iter()
}
}
impl Owners<Normal> {
fn collect_glue(&mut self, name: &StoredName) -> Vec<StoredRecord> {
let mut glue_records = vec![];
if let Some(normal) = self.owners.get(name) {
for (_rtype, rrset) in
normal.records.iter().filter(|(&rtype, _)| rtype.is_glue())
{
for rdata in rrset.data() {
let glue_record = StoredRecord::new(
name.clone(),
Class::IN,
rrset.ttl(),
rdata.clone(),
);
glue_records.push(glue_record);
}
}
}
glue_records
}
}
impl<Content> Default for Owners<Content> {
fn default() -> Self {
Owners {
owners: Default::default(),
}
}
}
#[derive(Clone, Default)]
pub struct Normal {
records: HashMap<Rtype, Rrset>,
}
impl Normal {
fn insert(&mut self, record: StoredRecord) {
use std::collections::hash_map::Entry;
match self.records.entry(record.rtype()) {
Entry::Occupied(mut occupied) => {
occupied.get_mut().push_record(record)
}
Entry::Vacant(vacant) => {
vacant.insert(record.into());
}
}
}
fn into_iter(self) -> impl Iterator<Item = (Rtype, Rrset)> {
self.records.into_iter()
}
fn sample_rtype(&self) -> Option<Rtype> {
self.records.iter().next().map(|(&rtype, _)| rtype)
}
fn first_non_glue(&self) -> Option<(&Rtype, &Rrset)> {
self.records.iter().find(|(rtype, _)| !rtype.is_glue())
}
}
#[derive(Clone, Default)]
pub struct ZoneCut {
ns: Option<Rrset>,
ds: Option<Rrset>,
}
impl ZoneCut {
fn insert(&mut self, record: StoredRecord) {
match record.rtype() {
Rtype::NS => {
if let Some(ns) = self.ns.as_mut() {
ns.push_record(record)
} else {
self.ns = Some(record.into())
}
}
Rtype::DS => {
if let Some(ds) = self.ds.as_mut() {
ds.push_record(record)
} else {
self.ds = Some(record.into())
}
}
_ => panic!("inserting wrong rtype to zone cut"),
}
}
fn sample_rtype(&self) -> Option<Rtype> {
self.ds.as_ref().or(self.ns.as_ref()).map(|r| r.rtype())
}
}