// MoonDNS Domain Name Codec with Robust Pointer Defense (RFC 1035 Section 4.1.4)
///|
/// Convert ASCII uppercase letters to lowercase
pub fn to_ascii_lower(s : String) -> String {
let sb = StringBuilder()
for c in s {
if c >= 'A' && c <= 'Z' {
let code = c.to_int() + 32
sb.write_char(Int::unsafe_to_char(code))
} else {
sb.write_char(c)
}
}
sb.to_string()
}
///|
/// Normalize domain name: lowercase ASCII and strip trailing dot
pub fn normalize_domain(name : String) -> String {
let lower = to_ascii_lower(name)
let trimmed = lower.trim().to_owned()
if trimmed == "" || trimmed == "." {
""
} else if trimmed.has_suffix(".") {
match trimmed.chop_suffix(".") {
Some(v) => v.to_owned()
None => trimmed
}
} else {
trimmed
}
}
///|
/// RFC 1035 Case-insensitive domain equality
pub fn domain_equal(a : String, b : String) -> Bool {
normalize_domain(a) == normalize_domain(b)
}
///|
/// Split a normalized domain into labels
pub fn split_labels(domain : String) -> Array[String] {
let norm = normalize_domain(domain)
if norm == "" {
return []
}
let result : Array[String] = []
let mut curr = StringBuilder()
for c in norm {
if c == '.' {
result.push(curr.to_string())
curr = StringBuilder()
} else {
curr.write_char(c)
}
}
result.push(curr.to_string())
result
}
///|
/// Compression dictionary for domain name serialization
pub(all) struct NameCompressor {
entries : Array[(String, Int)]
}
///|
pub fn NameCompressor::new() -> NameCompressor {
{ entries: [] }
}
///|
pub fn NameCompressor::find(self : NameCompressor, suffix : String) -> Option[Int] {
let norm = normalize_domain(suffix)
for entry in self.entries {
if entry.0 == norm {
return Some(entry.1)
}
}
None
}
///|
pub fn NameCompressor::add(
self : NameCompressor,
suffix : String,
offset : Int,
) -> Unit {
let norm = normalize_domain(suffix)
// Store only if within 14-bit pointer reach
if offset < 0x4000 {
self.entries.push((norm, offset))
}
}
///|
/// Encode domain name into DnsBuffer, optionally using compression
pub fn encode_name(
name : String,
buf : DnsBuffer,
comp : NameCompressor?,
compress : Bool,
) -> Unit {
let norm = normalize_domain(name)
if norm == "" {
buf.write_byte(b'\x00')
return
}
let labels = split_labels(norm)
let n = labels.length()
for i in 0.. i {
suffix_sb.write_char('.')
}
suffix_sb.write_string(labels[j])
}
let suffix = suffix_sb.to_string()
if compress {
match comp {
Some(c) =>
match c.find(suffix) {
Some(target_offset) => {
// Write 2-byte compression pointer: 11xxxxxx xxxxxxxx
let p0 = (0xC0 | ((target_offset >> 8) & 0x3F)).to_byte()
let p1 = (target_offset & 0xFF).to_byte()
buf.write_byte(p0)
buf.write_byte(p1)
return
}
None =>
if buf.length() < 0x4000 {
c.add(suffix, buf.length())
}
}
None => ()
}
}
let label = labels[i]
let len = label.length()
buf.write_byte(len.to_byte())
for c in label {
buf.write_byte(c.to_int().to_byte())
}
}
// Terminating root label
buf.write_byte(b'\x00')
}
///|
/// Parse domain name from bytes with full security guards:
/// - Guard 1: Loop detection (A -> A, A -> B -> A)
/// - Guard 2: Forward pointer / self pointer defense (target >= ptr_pos)
/// - Guard 3: Out of bounds defense (target >= bytes.length)
/// - Guard 4: Jump limit defense (<= 128 jumps)
/// - Guard 5: Label length <= 63 bytes
/// - Guard 6: Total name length <= 255 bytes
pub fn parse_name(
bytes : Bytes,
start_offset : Int,
) -> Result[(String, Int), DnsError] {
let mut curr = start_offset
let mut next_pos = -1
let mut jumps = 0
let visited_pointers : Array[Int] = []
let labels : Array[String] = []
let mut total_len = 0
for ;; {
if curr >= bytes.length() {
return Err(DnsError::BufferUnderflow)
}
let b0 = bytes[curr].to_int()
if b0 == 0 {
// Root label (end of name)
if next_pos < 0 {
next_pos = curr + 1
}
break
}
let flag = b0 & 0xC0
if flag == 0xC0 {
// Compression pointer (2 bytes)
if curr + 1 >= bytes.length() {
return Err(DnsError::BufferUnderflow)
}
let ptr_pos = curr
let target_offset = ((b0 & 0x3F) << 8) | bytes[curr + 1].to_int()
if next_pos < 0 {
next_pos = curr + 2
}
// Guard 2: Target must be strictly prior to current pointer position
if target_offset >= ptr_pos {
return Err(
DnsError::PointerOutOfBounds(
"Target offset \{target_offset} >= pointer position \{ptr_pos}",
),
)
}
// Guard 3: Target offset must not exceed total message length
if target_offset >= bytes.length() {
return Err(
DnsError::PointerOutOfBounds(
"Target offset \{target_offset} >= packet length \{bytes.length()}",
),
)
}
// Guard 1: Loop detection across visited pointer targets
for v in visited_pointers {
if v == target_offset {
return Err(
DnsError::PointerLoop("Loop detected at offset \{target_offset}"),
)
}
}
visited_pointers.push(target_offset)
// Guard 4: Jump count limit
jumps += 1
if jumps > 128 {
return Err(DnsError::PointerJumpLimitExceeded)
}
curr = target_offset
} else if flag == 0 {
// Standard uncompressed label
let label_len = b0
if label_len > 63 {
return Err(DnsError::InvalidLabelLength(label_len))
}
if curr + 1 + label_len > bytes.length() {
return Err(DnsError::BufferUnderflow)
}
total_len += label_len + 1
if total_len > 255 {
return Err(DnsError::InvalidDomainName("Domain exceeds 255 bytes"))
}
let sb = StringBuilder()
for i in 0.. 0 {
sb.write_char('.')
}
sb.write_string(labels[i])
}
sb.to_string()
}
let consumed_pos = if next_pos < 0 { curr + 1 } else { next_pos }
Ok((final_name, consumed_pos))
}