Architecture¶
Module layout¶
Hornet is a single crate that provides both the library and the optional CLI binary.
The CLI is gated behind the cli feature flag (enabled by default).
src/
├── ast/ # Typed AST definitions
│ ├── named_conf.rs
│ └── zone_file.rs
├── parser/ # Winnow parser combinators
│ ├── common.rs
│ ├── named_conf.rs
│ └── zone_file.rs
├── writer/ # AST → text serialisers
│ ├── named_conf.rs
│ └── zone_file.rs
├── validator/ # Semantic validation
│ └── mod.rs
├── error.rs # Error and diagnostic types
├── lib.rs # Public API surface
└── main.rs # CLI binary (requires `cli` feature)
The ast module¶
The AST types are the shared language between the parser, validator, and writer. They are pure data — no methods that perform IO or side effects.
named_conf AST¶
The top-level type is NamedConf, which contains a Vec<Statement>.
Statement is an enum with one variant per top-level BIND9 statement:
NamedConf
└── Vec<Statement>
├── Statement::Options(OptionsBlock)
├── Statement::Zone(ZoneStmt)
├── Statement::View(ViewStmt)
├── Statement::Acl(AclStmt)
├── Statement::Logging(LoggingBlock)
├── Statement::Controls(ControlsBlock)
├── Statement::Key(KeyStmt)
├── Statement::Primaries(PrimariesStmt)
├── Statement::Server(ServerStmt)
├── Statement::Include(String)
└── Statement::Unknown { keyword, body }
zone_file AST¶
The top-level type is ZoneFile, which contains a Vec<Entry>.
Entry is an enum covering directives and resource records:
ZoneFile
└── Vec<Entry>
├── Entry::Origin(String) — $ORIGIN directive
├── Entry::Ttl(u32) — $TTL directive
├── Entry::Include(String) — $INCLUDE directive
├── Entry::Generate { ... } — $GENERATE directive
└── Entry::Record(ResourceRecord)
└── rdata: RData
├── RData::A(Ipv4Addr)
├── RData::Aaaa(Ipv6Addr)
├── RData::Ns(DomainName)
├── RData::Mx { priority, exchange }
├── RData::Soa { ... }
├── RData::Cname(DomainName)
└── ... (24+ variants)
The parser module¶
Parsers are built with winnow, a fast, zero-copy parser combinator
library. The common.rs module provides shared primitives (whitespace, comments, quoted strings,
domain names, IP addresses) reused by both the named_conf and zone_file parsers.
Parsers are internal (pub(crate)) and exposed only through the top-level convenience functions
in lib.rs.
The writer module¶
Writers traverse the AST and produce a String of valid BIND9 text.
All formatting decisions (indent size, keyword style, blank lines between statements)
are controlled by WriteOptions.
Writers are deterministic: the same AST with the same WriteOptions always produces
identical output.
The validator module¶
Validation is a two-pass process:
- Collection pass — walk all statements and collect declared ACL names, key names, and zone names.
- Semantic pass — walk all statements again, cross-referencing declarations against usages.
Validators return Vec<ValidationError> — they never panic or mutate the AST.
Diagnostics carry a Severity (Info, Warning, Error) to allow callers to
decide their own tolerance threshold.
Error handling¶
Hornet uses thiserror for its Error enum
and miette for rich diagnostic rendering.
Parse errors include the source text and a byte-range span, enabling pretty-printed
output with the offending line highlighted — identical to what rustc produces.
See Error Types for the full type inventory.
Design principles¶
- No IO in the AST or parser —
parse_named_conf_file()reads the file and delegates toparse_named_conf(). The parser itself never touches the filesystem. - No mutation of the AST — validation and writing both take
&AST(shared reference). - Zero unsafe code — the entire codebase is
#![forbid(unsafe_code)]. - Feature-gated serde — AST types are lean by default; serialisation is opt-in.