Add bussard as-is
This commit is contained in:
parent
9b6a3fe164
commit
c451d4cb00
11 changed files with 872 additions and 0 deletions
17
projects/bussard/Makefile
Normal file
17
projects/bussard/Makefile
Normal file
|
@ -0,0 +1,17 @@
|
|||
.PHONY: deploy test
|
||||
|
||||
src/python/bussard/gen/parser.py: Makefile src/canopy/zonefile.peg
|
||||
mkdir -p tempdir
|
||||
cp src/canopy/zonefile.peg tempdir/
|
||||
canopy --lang=python tempdir/zonefile.peg
|
||||
which gsed && gsed -i 's/ / /g' tempdir/zonefile.py || sed -i 's/ / /g' tempdir/zonefile.py
|
||||
which gsed && gsed -i '1s/^/# checkstyle: noqa\n\n"""Generated code.\n\nDo not modify or lint\n"""\n\n/' tempdir/zonefile.py || sed -i '1s/^/# checkstyle: noqa\n\n"""Generated code.\n\nDo not modify or lint\n"""\n\n/' tempdir/zonefile.py
|
||||
mv tempdir/zonefile.py src/python/bussard/gen/parser.py
|
||||
|
||||
src/python/bussard/gen/types.py: Makefile src/canopy/zonefile.peg src/awk/gen_types.awk
|
||||
awk -f src/awk/gen_types.awk src/canopy/zonefile.peg > src/python/bussard/gen/types.py
|
||||
|
||||
all: src/python/bussard/gen/types.py src/python/bussard/gen/parser.py
|
||||
|
||||
test: all
|
||||
pytest
|
13
projects/bussard/README.md
Normal file
13
projects/bussard/README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Bussard
|
||||
|
||||
> Sometimes you need an engine that works at the Bottom, near the Slow
|
||||
Zone when you're crawling along just above the Unthinking Depths. Hard
|
||||
to beat a ramscoop when it's time to go on ice.
|
||||
|
||||
Bussard is a small tooklit for parsing BIND zonefiles hence the
|
||||
reference to Verner Vinge's ["Zones of Thought"](https://en.wikipedia.org/wiki/A_Fire_Upon_the_Deep)
|
||||
series.
|
||||
|
||||
## Usage
|
||||
|
||||
FIXME
|
31
projects/bussard/setup.py
Normal file
31
projects/bussard/setup.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from setuptools import setup
|
||||
|
||||
|
||||
setup(
|
||||
name="arrdem.bussard",
|
||||
# Package metadata
|
||||
version="0.0.0",
|
||||
license="MIT",
|
||||
description="A DNS zonefile engine",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Reid 'arrdem' McKenzie",
|
||||
author_email="me@arrdem.com",
|
||||
url="https://git.arrdem.com/arrdem/bussard",
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
],
|
||||
# Package setup
|
||||
package_dir={"": "src/python"},
|
||||
packages=[
|
||||
"bussard",
|
||||
],
|
||||
scripts=[
|
||||
"src/python/bussard/bfmt",
|
||||
"src/python/bussard/bparse",
|
||||
],
|
||||
)
|
36
projects/bussard/src/awk/gen_types.awk
Normal file
36
projects/bussard/src/awk/gen_types.awk
Normal file
|
@ -0,0 +1,36 @@
|
|||
BEGIN {
|
||||
print "#!/usr/bin/env python3\n"
|
||||
print "\"\"\"GENERATED.\n\nRecord types derived from the grammar.\n\"\"\"\n";
|
||||
print "from typing import NamedTuple, Optional\n\n";
|
||||
print "class Record(object):\n \"\"\"Base class for DNS records.\"\"\"\n\n"
|
||||
spacing=""
|
||||
}
|
||||
|
||||
/<-/ {
|
||||
if (or(("\""$1"\"" == tolower($3)), ("\"$"$1"\"" == tolower($3)))) {
|
||||
if (spacing)
|
||||
print spacing;
|
||||
|
||||
print "class " toupper($1) "(NamedTuple, Record): # noqa: T000";
|
||||
|
||||
# If this isn't $TTL or $ORIGIN, it has a name.
|
||||
if ($3 !~ /\$/)
|
||||
print " name: str";
|
||||
|
||||
for(i=3;i<=NF;i++) {
|
||||
if ($i ~ /:/) {
|
||||
split($i,arr,":")
|
||||
|
||||
if ($i ~ /:(word|v[46]address|string)/) {
|
||||
print " " arr[1] ": str";
|
||||
} else if ($i ~ /:(num|seconds)/) {
|
||||
print " " arr[1] ": int"
|
||||
}
|
||||
}
|
||||
}
|
||||
print " type: str = \"IN\"";
|
||||
print " ttl: Optional[int] = None";
|
||||
print " comment: Optional[str] = None";
|
||||
spacing="\n";
|
||||
}
|
||||
}
|
74
projects/bussard/src/canopy/zonefile.peg
Normal file
74
projects/bussard/src/canopy/zonefile.peg
Normal file
|
@ -0,0 +1,74 @@
|
|||
# A zonefile parser.
|
||||
#
|
||||
# Based on RFC 883, RFC 1035
|
||||
# - Drops WKS per RFC 1123
|
||||
# - Drops NULL per RFC 1035
|
||||
# - Drops MG per RFC 2505
|
||||
# - Drops MR per RFC 2505
|
||||
# - Drops MINFO per RFC 2505
|
||||
# + Adds SRV from RFC 2782
|
||||
# + Adds AAAA from RFC 3596
|
||||
|
||||
grammar Zonefile
|
||||
# Baze zone rule
|
||||
zone <- _one* %make_zone
|
||||
_one <- origin / ttl / records / eol # helper for testing
|
||||
|
||||
# The origin and TTL special records
|
||||
origin <- "$ORIGIN" ws name:word comment:eol %make_origin
|
||||
ttl <- "$TTL" ws ttl:seconds comment:eol %make_ttl
|
||||
|
||||
# Base record rule
|
||||
records <- name:word (_r_repeat / comment / eol)+ %make_records
|
||||
_r_repeat <- ws (_r_with_ttl / _r_with_type / _r ) ws comment:eol %make_repeat
|
||||
_r_with_ttl <- ttl:seconds ws (_r_with_type / _r) %make_record_ttl
|
||||
_r_with_type <- type:"IN" ws (_r_with_ttl / _r) %make_record_type
|
||||
|
||||
####################################################################################################
|
||||
# Record types
|
||||
|
||||
# A big alternation of the supported records
|
||||
_r <- aaaa / a / cname / txt / mx / ns / ptr / soa / srv / rp
|
||||
|
||||
# Oh gawd SOAs
|
||||
soa <- "SOA" ws mname:word ws rname:word ws "(" _ws_ serial:num _ws_ refresh:seconds _ws_ retry:seconds _ws_ expire:seconds _ws_ minimum:seconds _ws_ ")" %make_soa
|
||||
|
||||
a <- "A" ws address:v4address %make_a
|
||||
aaaa <- "AAAA" ws address:v6address %make_aaaa
|
||||
cname <- "CNAME" ws cname:word %make_cname
|
||||
mx <- "MX" ws preference:num ws exchange:word %make_mx
|
||||
ns <- "NS" ws nsdname:word %make_ns
|
||||
ptr <- "PTR" ws ptrdname:word %make_ptr
|
||||
txt <- "TXT" ws txt_data:string %make_txt
|
||||
srv <- "SRV" ws priority:num ws weight:num ws port:num ws target:word %make_srv
|
||||
rp <- "RP" ws mbox_dname:word ws txt_dname:word %make_rp
|
||||
|
||||
####################################################################################################
|
||||
# Record fragments
|
||||
|
||||
# Massively overbroad word regex
|
||||
word <- [@.*_A-Za-z0-9-]+ %make_word
|
||||
|
||||
# num
|
||||
num <- [\d]+ %make_num
|
||||
|
||||
# seconds
|
||||
seconds <- num sec_unit? %make_seconds
|
||||
sec_unit <- [WwDdHhMmSs]
|
||||
|
||||
# v4address (AKA address in RFC-1035) is a 32bi address
|
||||
v4address <- num '.' num '.' num '.' num %make_v4
|
||||
|
||||
# v6address is a 64bi aka IPV6 address
|
||||
# This is a garbage, overbroad regex >.>
|
||||
v6address <- [A-Za-z0-9:]+ %make_v6
|
||||
|
||||
string <- '"' [^\"]* '"' %make_string
|
||||
|
||||
# Whitespace in various forms
|
||||
eol <- ws (comment / newline) %make_blank
|
||||
_ws_ <- eol? ws? %make_blank
|
||||
blank <- ws? newline %make_blank
|
||||
comment <- ";" [^\n]* "\n" %make_blank
|
||||
ws <- [ \t]* %make_blank
|
||||
newline <- [\n] %make_blank
|
0
projects/bussard/src/python/bussard/__init__.py
Normal file
0
projects/bussard/src/python/bussard/__init__.py
Normal file
182
projects/bussard/src/python/bussard/bfmt
Normal file
182
projects/bussard/src/python/bussard/bfmt
Normal file
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
|
||||
import bussard.gen.types as t # for types
|
||||
from bussard.reader import read
|
||||
|
||||
|
||||
def format_time(num):
|
||||
week = (7 * 24 * 60 * 60)
|
||||
day = (24 * 60 * 60)
|
||||
hour = (60 * 60)
|
||||
minute = 60
|
||||
|
||||
if num % week == 0:
|
||||
return f"{num//week}w"
|
||||
elif num % day == 0:
|
||||
return f"{num//day}d"
|
||||
elif num % hour == 0:
|
||||
return f"{num//hour}h"
|
||||
elif num % minute == 0:
|
||||
return f"{num//minute}m"
|
||||
else:
|
||||
return f"{num}s"
|
||||
|
||||
|
||||
def format_comment(record):
|
||||
return record.comment or "\n"
|
||||
|
||||
|
||||
def format_record_name(record, cont=None, soa=None, name_width=None):
|
||||
name = record.name
|
||||
if name == soa.name:
|
||||
name = "@"
|
||||
if cont and name == cont.name:
|
||||
name = " " * len(cont.name)
|
||||
if name_width:
|
||||
name = name.ljust(name_width)
|
||||
return name
|
||||
|
||||
|
||||
def format_record_ttl(record, ttl=None):
|
||||
if ttl and record.ttl == ttl.ttl:
|
||||
return ""
|
||||
elif record.ttl:
|
||||
return f"{record.ttl} "
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def format_record(record, cont=None, soa=None, ttl=None, name_width=None):
|
||||
"""Given a single record, render it nicely."""
|
||||
|
||||
if isinstance(record, t.TTL):
|
||||
return f"$TTL {format_time(record.ttl)}{format_comment(record)}"
|
||||
|
||||
elif isinstance(record, t.ORIGIN):
|
||||
return f"$ORIGIN {record.name}{format_comment(record)}"
|
||||
|
||||
rname = format_record_name(record, cont=cont, soa=soa, name_width=name_width)
|
||||
prefix = f"{rname} {format_record_ttl(record, ttl=ttl)}{record.type}"
|
||||
|
||||
if isinstance(record, t.SOA):
|
||||
return f"""{prefix} SOA {record.mname} {record.rname} (
|
||||
{record.serial: <10} ; serial
|
||||
{format_time(record.refresh): <10} ; refresh after
|
||||
{format_time(record.retry): <10} ; retry after
|
||||
{format_time(record.expire): <10} ; expire after
|
||||
{format_time(record.minimum): <10} ; negative cache
|
||||
)"""
|
||||
|
||||
elif isinstance(record, t.A):
|
||||
return f"""{prefix} A {record.address: <15}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.AAAA):
|
||||
return f"""{prefix} AAAA {record.address: <39}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.CNAME):
|
||||
return f"""{prefix} CNAME {record.cname}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.MX):
|
||||
return f"""{prefix} MX {record.preference} {record.exchange}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.NS):
|
||||
return f"""{prefix} NS {record.nsdname}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.PTR):
|
||||
return f"""{prefix} PTR {record.ptrdname}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.TXT):
|
||||
return f'''{prefix} TXT "{record.txt_data}"{format_comment(record)}'''
|
||||
|
||||
elif isinstance(record, t.SRV):
|
||||
return f"""{prefix} SRV {record.priority} {record.weight} {record.port} {record.target}{format_comment(record)}"""
|
||||
|
||||
elif isinstance(record, t.RP):
|
||||
return f"""{prefix} RP {record.mbox_dname} {record.txt_data}{format_comment(record)}"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open(sys.argv[1], "r") as f:
|
||||
records = list(read(f.read()))
|
||||
|
||||
# Order records preferentially.
|
||||
# $ORIGIN
|
||||
# $TTL
|
||||
# SOA
|
||||
# $ORIGIN NS
|
||||
# $ORIGIN MX
|
||||
# then alphabetically by name.
|
||||
# one space between groups.
|
||||
|
||||
origin = [r for r in records if isinstance(r, t.ORIGIN)]
|
||||
if origin:
|
||||
origin = origin[0]
|
||||
else:
|
||||
origin = None
|
||||
|
||||
ttl = [r for r in records if isinstance(r, t.TTL)]
|
||||
if ttl:
|
||||
ttl = ttl[0]
|
||||
else:
|
||||
ttl = None
|
||||
|
||||
soa = [r for r in records if isinstance(r, t.SOA)]
|
||||
if soa:
|
||||
soa = soa[0]
|
||||
else:
|
||||
soa = None
|
||||
|
||||
if soa and soa.name and not origin:
|
||||
origin = t.ORIGIN(soa.name)
|
||||
|
||||
# Find the global nss and mxs
|
||||
nss = [r for r in records if isinstance(r, t.NS) and r.name == origin.name]
|
||||
mxs = [r for r in records if isinstance(r, t.MX) and r.name == origin.name]
|
||||
|
||||
# Sort the remaining records and comments
|
||||
tail = [r for r in records
|
||||
if (r != origin and r != ttl and r != soa and r not in nss and r not in mxs)]
|
||||
|
||||
def name_key(o):
|
||||
if isinstance(o, str):
|
||||
# It's a comment, sort by first word
|
||||
return o.split()[0].replace(";", "").lower()
|
||||
else:
|
||||
# It's a record, return the name
|
||||
return o.name
|
||||
|
||||
# Group chunks, preserving the original order.
|
||||
chunk = []
|
||||
chunks = [chunk]
|
||||
for record in tail:
|
||||
if record != "\n":
|
||||
chunk.append(record)
|
||||
elif chunk:
|
||||
chunk = []
|
||||
chunks.append(chunk)
|
||||
|
||||
# FIXME (arrdem 2020-02-01):
|
||||
# Split chunks somehow???
|
||||
# This is where the formater and linter diverge some.
|
||||
|
||||
# Now render
|
||||
if origin.name != "@":
|
||||
print(format_record(origin).strip())
|
||||
if ttl:
|
||||
print(format_record(ttl).strip())
|
||||
print(format_record(soa, ttl=ttl, soa=soa).rstrip())
|
||||
for ns in nss:
|
||||
print(format_record(ns, cont=soa, ttl=ttl, soa=soa).rstrip())
|
||||
for mx in mxs:
|
||||
print(format_record(mx, cont=soa, ttl=ttl, soa=soa).rstrip())
|
||||
for chunk in chunks:
|
||||
if chunk:
|
||||
width = max([len(r.name) if hasattr(r, "name") else 0 for r in chunk])
|
||||
for record in chunk:
|
||||
if isinstance(record, str):
|
||||
print(record.rstrip())
|
||||
else:
|
||||
print(format_record(record, ttl=ttl, soa=soa, name_width=width).rstrip())
|
||||
print()
|
14
projects/bussard/src/python/bussard/bparse
Normal file
14
projects/bussard/src/python/bussard/bparse
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
|
||||
import bussard.gen.types as t # for types
|
||||
from bussard.reader import read
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open(sys.argv[1], "r") as f:
|
||||
records = list(read(f.read()))
|
||||
|
||||
for r in records:
|
||||
print(r)
|
0
projects/bussard/src/python/bussard/gen/__init__.py
Normal file
0
projects/bussard/src/python/bussard/gen/__init__.py
Normal file
236
projects/bussard/src/python/bussard/reader.py
Normal file
236
projects/bussard/src/python/bussard/reader.py
Normal file
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""A reader, integrating the generated parser and types.
|
||||
|
||||
Integrates the generated parser with the types, providing a reasonable way to interface with both
|
||||
zonefiles through the parser.
|
||||
|
||||
"""
|
||||
|
||||
from types import LambdaType
|
||||
|
||||
from bussard.gen.parser import parse as _parse, Parser # noqa
|
||||
from bussard.gen.types import * # noqa
|
||||
|
||||
|
||||
def _merge(d1, d2):
|
||||
res = {}
|
||||
for k, v in d1.items():
|
||||
res[k] = v
|
||||
for k, v in d2.items():
|
||||
if v is not None:
|
||||
res[k] = v
|
||||
return res
|
||||
|
||||
|
||||
class PrintableLambda(object):
|
||||
def __init__(self, fn, **kwargs):
|
||||
self._target = fn
|
||||
self._kwargs = kwargs
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self._target(*args, **_merge(kwargs, self._kwargs))
|
||||
|
||||
def __repr__(self):
|
||||
return f"lambda ({self._target!r}, **{self._kwargs!r})"
|
||||
|
||||
|
||||
class Actions:
|
||||
@staticmethod
|
||||
def make_zone(_input, _index, _offset, elements):
|
||||
"""Zones are just a sequence of entries. For now."""
|
||||
origin = "@" # Preserve the default unless we get an explicit origin
|
||||
ttl = None
|
||||
|
||||
for e in elements:
|
||||
# $ORIGIN and $TTL set global defaults
|
||||
if isinstance(e, ORIGIN):
|
||||
if origin != "@":
|
||||
raise RuntimeError("$ORIGIN occurs twice!")
|
||||
origin = e.name
|
||||
yield e
|
||||
|
||||
elif isinstance(e, TTL):
|
||||
if ttl:
|
||||
raise RuntimeError("$TTL occurs twice!")
|
||||
ttl = e.ttl
|
||||
yield e
|
||||
|
||||
# apply bindings to emit records
|
||||
elif isinstance(e, list):
|
||||
for fn in e:
|
||||
if isinstance(fn, (LambdaType, PrintableLambda)):
|
||||
yield fn(name=origin, ttl=ttl)
|
||||
else:
|
||||
yield fn
|
||||
|
||||
@staticmethod
|
||||
def make_origin(_input, _index, _offset, elements):
|
||||
return ORIGIN(elements[2])
|
||||
|
||||
@staticmethod
|
||||
def make_ttl(_input, _index, _offset, elements):
|
||||
return TTL(elements[2])
|
||||
|
||||
@staticmethod
|
||||
def make_records(_input, _index, _offset, elements):
|
||||
name, repetitions = elements
|
||||
if name == "@":
|
||||
# We allow make_zone to bind @ to $ORIGIN if present
|
||||
name = None
|
||||
return [
|
||||
PrintableLambda(e, name=name) if isinstance(e, PrintableLambda) else e
|
||||
for e in repetitions
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def make_repeat(input, _index, _offset, elements):
|
||||
_, record, _, comment = elements
|
||||
return PrintableLambda(record, comment=comment)
|
||||
|
||||
@staticmethod
|
||||
def make_record_ttl(_input, _index, _offset, elements):
|
||||
ttl, _, record = elements
|
||||
return PrintableLambda(record, ttl=ttl)
|
||||
|
||||
@staticmethod
|
||||
def make_record_type(_input, _index, _offset, elements):
|
||||
type, _, record = elements
|
||||
return PrintableLambda(record, type=type.text)
|
||||
|
||||
##################################################
|
||||
@staticmethod
|
||||
def make_a(_input, _index, _offset, elements):
|
||||
_, _, address = elements
|
||||
return PrintableLambda(A, address=address)
|
||||
|
||||
@staticmethod
|
||||
def make_aaaa(_input, _index, _offset, elements):
|
||||
_, _, address = elements
|
||||
return PrintableLambda(AAAA, address=address)
|
||||
|
||||
@staticmethod
|
||||
def make_cname(_input, _index, _offset, elements):
|
||||
_, _, cname = elements
|
||||
return PrintableLambda(CNAME, cname=cname)
|
||||
|
||||
@staticmethod
|
||||
def make_mx(_input, _index, _offset, elements):
|
||||
_, _, preference, _, mx = elements
|
||||
return PrintableLambda(MX, preference=preference, exchange=mx)
|
||||
|
||||
@staticmethod
|
||||
def make_ns(_input, _index, _offset, elements):
|
||||
_, _, ns = elements
|
||||
return PrintableLambda(NS, nsdname=ns)
|
||||
|
||||
@staticmethod
|
||||
def make_soa(_input, _index, _offset, elements):
|
||||
(
|
||||
_,
|
||||
_,
|
||||
mname,
|
||||
_,
|
||||
rname,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
serial,
|
||||
_,
|
||||
refresh,
|
||||
_,
|
||||
retry,
|
||||
_,
|
||||
expire,
|
||||
_,
|
||||
minimum,
|
||||
_,
|
||||
_,
|
||||
) = elements
|
||||
return PrintableLambda(
|
||||
SOA,
|
||||
mname=mname,
|
||||
rname=rname,
|
||||
serial=serial,
|
||||
refresh=refresh,
|
||||
retry=retry,
|
||||
expire=expire,
|
||||
minimum=minimum,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def make_srv(_input, _index, _offset, elements):
|
||||
_, _, priority, _, weight, _, port, _, target = elements
|
||||
return PrintableLambda(
|
||||
SRV, priority=priority, weight=weight, port=port, target=target
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def make_txt(_input, _index, _offset, elements):
|
||||
_, _, txt_data = elements
|
||||
return PrintableLambda(TXT, txt_data=txt_data)
|
||||
|
||||
@staticmethod
|
||||
def make_ptr(_input, _index, _offset, elements):
|
||||
_, _, ptrdname = elements
|
||||
return PrintableLambda(PTR, ptrdname=ptrdname)
|
||||
|
||||
@staticmethod
|
||||
def make_rp(_input, _index, _offset, elements):
|
||||
_, _, mbox_dname, _, txt_dname = elements
|
||||
return PrintableLambda(RP, mbox_dname=mbox_dname, txt_dname=txt_dname)
|
||||
|
||||
@staticmethod
|
||||
def make_string(input, start, end, _elements):
|
||||
return input[start + 1 : end - 1]
|
||||
|
||||
##################################################
|
||||
@staticmethod
|
||||
def make_word(_input, _index, _offset, elements):
|
||||
"""Words have many elements, but we want their whole text."""
|
||||
return "".join(e.text for e in elements).lower() # Uppercase is a lie in DNS
|
||||
|
||||
@staticmethod
|
||||
def make_num(input, start, end, _elements):
|
||||
return int(input[start:end], 10)
|
||||
|
||||
@staticmethod
|
||||
def make_seconds(_input, _, _end, elements):
|
||||
base = elements[0]
|
||||
factor = 1
|
||||
unit = elements[1].text.lower()
|
||||
if len(elements) == 2 and unit:
|
||||
factor = {
|
||||
"s": 1,
|
||||
"m": 60,
|
||||
"h": 60 * 60,
|
||||
"d": 24 * 60 * 60,
|
||||
"w": 7 * 24 * 60 * 60,
|
||||
}[unit]
|
||||
|
||||
return base * factor
|
||||
|
||||
@staticmethod
|
||||
def make_v4(input, start, end, _elements):
|
||||
return input[start:end]
|
||||
|
||||
@staticmethod
|
||||
def make_v6(input, start, end, _elements):
|
||||
return input[start:end]
|
||||
|
||||
@staticmethod
|
||||
def make_blank(input, start, end, *_):
|
||||
return input[start:end]
|
||||
|
||||
|
||||
def read(input):
|
||||
"""Read an entire zonefile, returning an AST for it which contains formatting information."""
|
||||
return _parse(input, actions=Actions())
|
||||
|
||||
|
||||
def read1(input):
|
||||
"""Read a single record as if it were part or a zonefile.
|
||||
|
||||
Really just for testing.
|
||||
"""
|
||||
return next(read(input))
|
269
projects/bussard/test/python/bussard/test_reader.py
Normal file
269
projects/bussard/test/python/bussard/test_reader.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
"""
|
||||
Tests of the Bussard reader.
|
||||
"""
|
||||
|
||||
import bussard.reader as t
|
||||
from bussard.reader import Actions, Parser, read, read1
|
||||
|
||||
|
||||
def parse_word(input):
|
||||
return Parser(input, Actions(), None)._read_word()
|
||||
|
||||
|
||||
def test_parse_name():
|
||||
assert "foo" == parse_word("foo")
|
||||
assert "foo" == parse_word("foo bar")
|
||||
assert "foo" == parse_word("foo bar")
|
||||
assert "foo-bar" == parse_word("foo-bar")
|
||||
assert "*" == parse_word("*")
|
||||
assert "*.foo" == parse_word("*.foo")
|
||||
|
||||
|
||||
def test_read_ttl():
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""$TTL 300
|
||||
"""
|
||||
),
|
||||
t.TTL,
|
||||
)
|
||||
|
||||
|
||||
def test_read_origin():
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""$ORIGIN foobar.org
|
||||
"""
|
||||
),
|
||||
t.ORIGIN,
|
||||
)
|
||||
|
||||
|
||||
def test_read_soa():
|
||||
"""Test a couple of SOA cases, exercising both parsing and formatting."""
|
||||
|
||||
# Basically no formatting.
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ IN SOA ns1. root. (2020012301 60 90 1w 60)
|
||||
"""
|
||||
),
|
||||
t.SOA,
|
||||
)
|
||||
|
||||
# With a TTL
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN SOA ns1. root. (2020012301 60 90 1w 60)
|
||||
"""
|
||||
),
|
||||
t.SOA,
|
||||
)
|
||||
|
||||
# Some meaningful formatting.
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ IN SOA ns1. root. (
|
||||
2020012301 ; comment
|
||||
60 ; comment
|
||||
90 ; comment
|
||||
1w ; comment
|
||||
60 ; comment
|
||||
)
|
||||
"""
|
||||
),
|
||||
t.SOA,
|
||||
)
|
||||
|
||||
|
||||
def test_read_a():
|
||||
"""Test that some A records parse."""
|
||||
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ IN A 127.0.0.1
|
||||
"""
|
||||
),
|
||||
t.A,
|
||||
)
|
||||
|
||||
# With a TTL
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN A 127.0.0.1
|
||||
"""
|
||||
),
|
||||
t.A,
|
||||
)
|
||||
|
||||
# With a TTL & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN A 127.0.0.1; comment
|
||||
"""
|
||||
),
|
||||
t.A,
|
||||
)
|
||||
|
||||
# With a TTL & comment & whitespace
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN A 127.0.0.1 ; comment
|
||||
"""
|
||||
),
|
||||
t.A,
|
||||
)
|
||||
|
||||
|
||||
def test_read_aaaa():
|
||||
"""Test that some quad-a records parse"""
|
||||
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""foo IN AAAA ::1
|
||||
"""
|
||||
),
|
||||
t.AAAA,
|
||||
)
|
||||
|
||||
# With a TTL
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""foo 300 IN AAAA ::1
|
||||
"""
|
||||
),
|
||||
t.AAAA,
|
||||
)
|
||||
|
||||
# With a TTL & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""foo 300 IN AAAA ::1; comment
|
||||
"""
|
||||
),
|
||||
t.AAAA,
|
||||
)
|
||||
|
||||
# With a TTL & whitespace & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""foo 300 IN AAAA ::1 ; comment
|
||||
"""
|
||||
),
|
||||
t.AAAA,
|
||||
)
|
||||
|
||||
|
||||
def test_read_cname():
|
||||
"""Test some CNAME cases."""
|
||||
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""bar IN CNAME qux.
|
||||
"""
|
||||
),
|
||||
t.CNAME,
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""bar IN CNAME bar-other
|
||||
"""
|
||||
),
|
||||
t.CNAME,
|
||||
)
|
||||
|
||||
# With TTL
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""bar 300 IN CNAME bar-other.
|
||||
"""
|
||||
),
|
||||
t.CNAME,
|
||||
)
|
||||
|
||||
# With TTL & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""bar 300 IN CNAME bar-other.; comment
|
||||
"""
|
||||
),
|
||||
t.CNAME,
|
||||
)
|
||||
|
||||
# With TTL & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""bar 300 IN CNAME bar-other. ; comment
|
||||
"""
|
||||
),
|
||||
t.CNAME,
|
||||
)
|
||||
|
||||
|
||||
def test_read_mx():
|
||||
"""Some MX record examples."""
|
||||
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ IN MX 10 mx1.
|
||||
"""
|
||||
),
|
||||
t.MX,
|
||||
)
|
||||
|
||||
# With TTL
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN MX 10 mx1.
|
||||
"""
|
||||
),
|
||||
t.MX,
|
||||
)
|
||||
|
||||
# With TTL & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN MX 10 mx1.;bar
|
||||
"""
|
||||
),
|
||||
t.MX,
|
||||
)
|
||||
|
||||
# With TTL & comment
|
||||
assert isinstance(
|
||||
read1(
|
||||
"""@ 300 IN MX 10 mx1. ; bar
|
||||
"""
|
||||
),
|
||||
t.MX,
|
||||
)
|
||||
|
||||
|
||||
def test_read_repeated():
|
||||
"""Test t=support for repetition."""
|
||||
|
||||
assert all(
|
||||
isinstance(e, t.A)
|
||||
for e in read(
|
||||
"""foo IN A 10.0.0.1
|
||||
IN A 10.0.0.2; comment
|
||||
IN A 10.0.0.3 ; with whitespace
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# Note that comments and newlines become raw strings
|
||||
assert all(
|
||||
list(
|
||||
isinstance(e, (t.A, str))
|
||||
for e in read(
|
||||
"""foo IN A 10.0.0.1
|
||||
; comment
|
||||
IN A 10.0.0.2; comment
|
||||
|
||||
IN A 10.0.0.3 ; with whitespace
|
||||
"""
|
||||
)
|
||||
)
|
||||
)
|
Loading…
Reference in a new issue