Cursor-based binary reader and builder-based writer for DNS wire format. Handles name compression (RFC1035 §4.1.4) in both directions: the reader resolves compression pointers when decoding names, and the writer builds a compression table to emit pointers for previously-seen domain name suffixes.
The reader wraps a raw binary string with a 1-based cursor. Every read
advances the cursor. The writer accumulates bytes into a flat table and
converts to a string on output(). Both enforce RFC1035 security
constraints on label length, total name length, and pointer validity.
| Name | Signature |
|---|---|
reader:u8 | reader:u8() -> val, err |
reader:u16 | reader:u16() -> val, err |
reader:u32 | reader:u32() -> val, err |
reader:bytes | reader:bytes(n) -> data, err |
reader:pos | reader:pos() -> position |
reader:remaining | reader:remaining() -> count |
reader:seek | reader:seek(pos) |
reader:skip | reader:skip(n) |
reader:at_end | reader:at_end() -> ended |
reader:name | reader:name() -> fqdn, err |
reader:character_string | reader:character_string() -> data, err |
reader:sub_reader | reader:sub_reader(n) -> reader, err |
reader | reader(raw) -> reader |
writer:u8 | writer:u8(val) |
writer:u16 | writer:u16(val) |
writer:u32 | writer:u32(val) |
writer:bytes | writer:bytes(data) |
writer:pos | writer:pos() -> offset |
writer:len | writer:len() -> length |
writer:output | writer:output() -> data |
writer:patch_u16 | writer:patch_u16(offset, val) |
writer:name | writer:name(domain) |
writer:character_string | writer:character_string(data) |
writer | writer() -> writer |
reader:u8() ->
val,err
Read a single unsigned byte
reader:u16() ->
val,err
Read a 2-byte big-endian unsigned integer
reader:u32() ->
val,err
Read a 4-byte big-endian unsigned integer
reader:bytes(
n) ->data,err
Read exactly n bytes as a string
reader:pos() ->
position
Return the current 1-based cursor position
reader:remaining() ->
count
Return the number of bytes remaining from cursor to end
reader:seek(
pos)
Seek to an absolute 1-based position
reader:skip(
n)
Advance the cursor by n bytes
reader:at_end() ->
ended
Return true if the cursor is at or past the end of data
reader:name() ->
fqdn,err
Read a DNS compressed name, returning the fully qualified domain name
Reads a DNS name from the wire, handling compression pointers per RFC1035 §4.1.4. Returns the name with a trailing dot (e.g. "example.com."). The root name (single zero byte) returns ".".
Security constraints enforced:
Compression pointers must point backward (offset < pointer position)
Pointers must not target the 12-byte DNS header (offset >= 12)
Pointer chase depth limited to 128
Individual labels limited to 63 bytes
Total name limited to 255 bytes in wire format
For sub_readers: pointer chasing reads from the full parent message buffer, allowing resolution of pointers outside the sub_reader's bounds.
reader:character_string() ->
data,err
Read a character-string (length-prefixed per RFC1035 §3.3)
reader:sub_reader(
n) ->reader,err
Create a bounded sub-reader over the next n bytes
Shares the parent's full message buffer for compression pointer
resolution. Parent cursor advances past the bounded region.
Used for RDATA parsing where each record's RDATA must be read
from exactly n bytes (the RDLENGTH field).
reader(
raw) ->reader
Create a wire format reader from a raw binary string
Returns a cursor-based reader wrapping the given binary string. The reader provides methods for reading DNS wire format primitives (u8, u16, u32, bytes), compressed names, character-strings, and position management (pos, remaining, seek, skip, at_end).
Use sub_reader(n) to create bounded readers for RDATA parsing.
Sub-readers share the parent's message buffer so compression
pointers still resolve correctly.
writer:u8(
val)
Write a single unsigned byte
writer:u16(
val)
Write a 2-byte big-endian unsigned integer
writer:u32(
val)
Write a 4-byte big-endian unsigned integer
writer:bytes(
data)
Write raw bytes from a string
writer:pos() ->
offset
Return the current 0-based write offset
writer:len() ->
length
Return the current length in bytes
writer:output() ->
data
Return the accumulated bytes as a string
Converts the internal byte table to a binary string. Uses batched
string.char(unpack(...)) to avoid LuaJIT stack limits on large
messages.
writer:patch_u16(
offset,val)
Overwrite 2 bytes at a 0-based offset (for header fixups)
Patches 2 bytes at the given 0-based wire offset. Typically used to fill in section counts in the DNS header after all sections have been written.
writer:name(
domain)
Write a DNS name with compression
Writes a DNS domain name to the wire, using compression pointers for previously-seen suffixes. The domain should include a trailing dot (e.g. "example.com."). If omitted, one is appended.
The writer maintains a compression table mapping FQDN suffixes to their 0-based wire offsets. When a suffix has been seen before, a 2-byte pointer is emitted instead of repeating the labels.
Validation:
Labels longer than 63 bytes cause an error
Total wire-format name length exceeding 255 bytes causes an error
Suffixes at offsets >= 16384 are not eligible for compression (14-bit pointer field limit)
writer:character_string(
data)
Write a character-string (length-prefixed per RFC1035 §3.3)
writer() ->
writer
Create a new wire format writer
Returns a builder-based writer that accumulates bytes into an
internal table. Provides methods for writing DNS wire format
primitives (u8, u16, u32, bytes), compressed names, and
character-strings. Use output() to get the accumulated bytes
as a string. Use patch_u16(offset, val) for header fixups.
The writer maintains a compression table for name compression: when writing the same domain name suffix twice, the second occurrence is replaced with a 2-byte pointer.