Skip to content

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:

  1. Collection pass — walk all statements and collect declared ACL names, key names, and zone names.
  2. 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 parserparse_named_conf_file() reads the file and delegates to parse_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.