// DNS response pretty-printer (dig-like output).
// Formats decoded DNS messages and RRs for human-readable display.
// Format a full DNS message for display (similar to dig output)
///|
pub fn pretty_print_message(msg : Message) -> String {
let out = Ref("")
// Header section
out.val = out.val + ";; ->>HEADER<<- opcode: QUERY"
out.val = out.val + ", status: " + rcode_to_string(msg.header.rcode())
out.val = out.val + ", id: " + msg.header.id.to_string()
out.val = out.val + "\n"
// Flags
out.val = out.val + ";; flags:"
if msg.header.is_response() {
out.val = out.val + " qr"
}
if msg.header.is_truncated() {
out.val = out.val + " tc"
}
out.val = out.val + "; QUERY: " + msg.header.qdcount.to_string()
out.val = out.val + ", ANSWER: " + msg.header.ancount.to_string()
out.val = out.val + ", AUTHORITY: " + msg.header.nscount.to_string()
out.val = out.val + ", ADDITIONAL: " + msg.header.arcount.to_string()
out.val = out.val + "\n\n"
// Question section
if msg.questions.length() > 0 {
out.val = out.val + ";; QUESTION SECTION:\n"
for q in msg.questions {
out.val = out.val + ";" + q.name + ". "
out.val = out.val + qtype_to_string(q.qtype) + "\n"
}
out.val = out.val + "\n"
}
// Answer section
if msg.answers.length() > 0 {
out.val = out.val + ";; ANSWER SECTION:\n"
for rr in msg.answers {
out.val = out.val + pretty_print_rr(rr) + "\n"
}
out.val = out.val + "\n"
}
// Authority section
if msg.authorities.length() > 0 {
out.val = out.val + ";; AUTHORITY SECTION:\n"
for rr in msg.authorities {
out.val = out.val + pretty_print_rr(rr) + "\n"
}
out.val = out.val + "\n"
}
// Additional section
if msg.additionals.length() > 0 {
out.val = out.val + ";; ADDITIONAL SECTION:\n"
for rr in msg.additionals {
out.val = out.val + pretty_print_rr(rr) + "\n"
}
out.val = out.val + "\n"
}
out.val
}
// Format a single resource record for display
///|
pub fn pretty_print_rr(rr : RR) -> String {
let result = Ref("")
result.val = rr.name + ". "
result.val = result.val + rr.ttl.to_string() + " "
result.val = result.val + qtype_to_string(rr.rtype) + " "
result.val = result.val + pretty_print_rdata(rr.rdata)
result.val
}
// Format RData for display
///|
pub fn pretty_print_rdata(rdata : RData) -> String {
match rdata {
A(ip) => pretty_ipv4(ip)
AAAA(w1, w2, w3, w4) => pretty_ipv6(w1, w2, w3, w4)
CNAME(target) => target + "."
NS(ns) => ns + "."
PTR(ptr) => ptr + "."
MX(pref, exchange) => pref.to_string() + " " + exchange + "."
TXT(strings) => {
let result = Ref("")
for s in strings {
result.val = result.val + "\"" + s + "\" "
}
result.val
}
SOA(mname, rname, serial, refresh, retry, expire, minimum) =>
mname +
". " +
rname +
". (" +
serial.to_string() +
" " +
refresh.to_string() +
" " +
retry.to_string() +
" " +
expire.to_string() +
" " +
minimum.to_string() +
")"
SRV(priority, weight, port, target) =>
priority.to_string() +
" " +
weight.to_string() +
" " +
port.to_string() +
" " +
target +
"."
OPT(options) => "OPT (" + options.length().to_string() + " options)"
Unknown(data) => "\\# " + data.length().to_string() + " bytes"
}
}
///|
/// Format RDATA for one-answer-per-line command output.
///
/// This deliberately keeps DNS names without a presentation trailing dot and
/// leaves SOA fields space separated so scripts can compare the value with
/// `dig +short` after their type-specific normalization. It is a semantic
/// formatter, not a zone-file serializer.
pub fn short_print_rdata(rdata : RData) -> String {
match rdata {
A(ip) => pretty_ipv4(ip)
AAAA(w1, w2, w3, w4) => pretty_ipv6(w1, w2, w3, w4)
CNAME(name) | NS(name) | PTR(name) => name
MX(preference, exchange) => preference.to_string() + " " + exchange
TXT(strings) => strings.join("")
SOA(mname, rname, serial, refresh, retry, expire, minimum) =>
mname +
" " +
rname +
" " +
serial.to_string() +
" " +
refresh.to_string() +
" " +
retry.to_string() +
" " +
expire.to_string() +
" " +
minimum.to_string()
SRV(priority, weight, port, target) =>
priority.to_string() +
" " +
weight.to_string() +
" " +
port.to_string() +
" " +
target
OPT(_) => "OPT"
Unknown(data) => "\\# " + data.length().to_string() + " bytes"
}
}
// Format IPv4 from integer to dotted decimal
///|
fn pretty_ipv4(ip : Int) -> String {
let a = (ip >> 24) & 0xFF
let b = (ip >> 16) & 0xFF
let c = (ip >> 8) & 0xFF
let d = ip & 0xFF
a.to_string() +
"." +
b.to_string() +
"." +
c.to_string() +
"." +
d.to_string()
}
// Format IPv6 from four 32-bit words
///|
fn pretty_ipv6(w1 : Int, w2 : Int, w3 : Int, w4 : Int) -> String {
let g = fn(v : Int) -> String {
let r = Ref("")
let val = Ref(v)
if val.val == 0 {
return "0"
}
while val.val > 0 {
let d = val.val % 16
if d < 10 {
r.val = "0123456789"[d].unsafe_to_char().to_string() + r.val
} else {
r.val = "abcdef"[d - 10].unsafe_to_char().to_string() + r.val
}
val.val = val.val / 16
}
r.val
}
g((w1 >> 16) & 0xFFFF) +
":" +
g(w1 & 0xFFFF) +
":" +
g((w2 >> 16) & 0xFFFF) +
":" +
g(w2 & 0xFFFF) +
":" +
g((w3 >> 16) & 0xFFFF) +
":" +
g(w3 & 0xFFFF) +
":" +
g((w4 >> 16) & 0xFFFF) +
":" +
g(w4 & 0xFFFF)
}
// Format a DnsResult for short display (just answers)
///|
pub fn pretty_print_result(result : DnsResult) -> String {
let out = Ref("")
if result.cname_chain.length() > 1 {
out.val = out.val + ";; CNAME chain: "
for i = 0; i < result.cname_chain.length(); i = i + 1 {
if i > 0 {
out.val = out.val + " -> "
}
out.val = out.val + result.cname_chain[i]
}
out.val = out.val + "\n"
}
for rr in result.answers {
out.val = out.val + pretty_print_rdata(rr.rdata) + "\n"
}
out.val
}
///|
fn pretty_json_escape(value : String) -> String {
let out = StringBuilder()
for char in value {
match char {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => {
let code = char.to_int()
if code == 8 {
out.write_string("\\b")
} else if code == 12 {
out.write_string("\\f")
} else if code < 0x20 {
let hex = code.to_string(radix=16)
out.write_string("\\u00")
if hex.length() == 1 {
out.write_char('0')
}
out.write_string(hex)
} else {
out.write_char(char)
}
}
}
}
out.to_string()
}
// Format a DnsResult as JSON
///|
pub fn pretty_print_result_json(
result : DnsResult,
name : String,
qtype : UInt16,
server : String,
) -> String {
let out = Ref("")
out.val = "{\n"
out.val = out.val + " \"name\": \"" + pretty_json_escape(name) + "\",\n"
out.val = out.val +
" \"type\": \"" +
pretty_json_escape(qtype_to_string(qtype)) +
"\",\n"
out.val = out.val + " \"server\": \"" + pretty_json_escape(server) + "\",\n"
out.val = out.val + " \"cname_chain\": ["
for i = 0; i < result.cname_chain.length(); i = i + 1 {
if i > 0 {
out.val = out.val + ", "
}
out.val = out.val + "\"" + pretty_json_escape(result.cname_chain[i]) + "\""
}
out.val = out.val + "],\n"
out.val = out.val + " \"answers\": [\n"
for i = 0; i < result.answers.length(); i = i + 1 {
let rr = result.answers[i]
out.val = out.val + " {\n"
out.val = out.val +
" \"name\": \"" +
pretty_json_escape(rr.name) +
"\",\n"
out.val = out.val +
" \"type\": \"" +
pretty_json_escape(qtype_to_string(rr.rtype)) +
"\",\n"
out.val = out.val + " \"ttl\": " + rr.ttl.to_string() + ",\n"
out.val = out.val +
" \"data\": \"" +
pretty_json_escape(pretty_print_rdata(rr.rdata)) +
"\"\n"
if i < result.answers.length() - 1 {
out.val = out.val + " },\n"
} else {
out.val = out.val + " }\n"
}
}
out.val = out.val + " ]\n"
out.val = out.val + "}\n"
out.val
}