///|
fn decimal_number(text : String) -> Int? {
let chars = text.to_array()
if chars.length() == 0 {
return None
}
let mut result = 0
for c in chars {
if !is_digit(c) {
return None
}
let digit = c.to_int() - '0'.to_int()
if result > (2147483647 - digit) / 10 {
return None
}
result = result * 10 + digit
}
Some(result)
}
///|
pub fn valid_ipv4(value : String) -> Bool {
let parts = value.split(".").collect()
if parts.length() != 4 {
return false
}
for part in parts {
let text = part.to_owned()
if text.length() == 0 || text.length() > 3 {
return false
}
match decimal_number(text) {
Some(n) => if n > 255 { return false }
None => return false
}
}
true
}
///|
fn is_hex(c : Char) -> Bool {
is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
///|
/// Check the text form of an IPv6 address without accepting zone identifiers.
pub fn valid_ipv6(value : String) -> Bool {
let chars = value.to_array()
let mut dotted = false
let mut last_colon = -1
for index in 0.. 39 {
return false
}
let mut compressed = false
let mut groups = 0
let mut group_len = 0
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c == ':' {
if i + 1 < chars.length() && chars[i + 1] == ':' {
if compressed {
return false
}
compressed = true
if group_len > 0 {
groups += 1
group_len = 0
}
i += 2
continue
}
if group_len == 0 {
return false
}
groups += 1
group_len = 0
} else if is_hex(c) {
group_len += 1
if group_len > 4 {
return false
}
} else {
return false
}
i += 1
}
if group_len > 0 {
groups += 1
} else if !compressed {
return false
}
if compressed {
groups < 8
} else {
groups == 8
}
}
///|
fn number_in_range(text : String, low : Int, high : Int) -> Bool {
match decimal_number(text) {
Some(n) => n >= low && n <= high
None => false
}
}
///|
fn require_rdata_length(
record : ResourceRecord,
expected : Int,
diagnostics : Array[Diagnostic],
) -> Bool {
if record.rdata.length() == expected {
true
} else {
diagnostics.push(
diagnostic(
"Z100",
"error",
"\{record.record_type} requires \{expected} RDATA fields; found \{record.rdata.length()}",
record.line,
record.column,
owner=record.owner,
),
)
false
}
}
///|
fn check_domain_field(
record : ResourceRecord,
index : Int,
origin : String,
diagnostics : Array[Diagnostic],
) -> Unit {
let name = absolute_rdata_name(record, index, origin)
if !valid_domain_name(name) {
diagnostics.push(
diagnostic(
"Z101",
"error",
"invalid domain name in \{record.record_type} data: \{record.rdata[index]}",
record.line,
record.column,
owner=record.owner,
),
)
}
}
///|
fn check_address_record(
record : ResourceRecord,
diagnostics : Array[Diagnostic],
) -> Unit {
if !require_rdata_length(record, 1, diagnostics) {
return
}
let valid = if record.record_type == "A" {
valid_ipv4(record.rdata[0])
} else {
valid_ipv6(record.rdata[0])
}
if !valid {
diagnostics.push(
diagnostic(
"Z102",
"error",
"invalid \{record.record_type} address: \{record.rdata[0]}",
record.line,
record.column,
owner=record.owner,
),
)
}
}
///|
fn check_soa(
record : ResourceRecord,
origin : String,
diagnostics : Array[Diagnostic],
) -> Unit {
if !require_rdata_length(record, 7, diagnostics) {
return
}
check_domain_field(record, 0, origin, diagnostics)
check_domain_field(record, 1, origin, diagnostics)
if parse_serial(record.rdata[2]) is None {
diagnostics.push(
diagnostic(
"Z103",
"error",
"SOA serial must be a 32-bit unsigned decimal integer",
record.line,
record.column,
owner=record.owner,
),
)
}
for i in 3..<7 {
if parse_ttl(record.rdata[i]) is None {
diagnostics.push(
diagnostic(
"Z103",
"error",
"SOA numeric or duration field is invalid: \{record.rdata[i]}",
record.line,
record.column,
owner=record.owner,
),
)
}
}
}
///|
fn check_mx(
record : ResourceRecord,
origin : String,
diagnostics : Array[Diagnostic],
) -> Unit {
if !require_rdata_length(record, 2, diagnostics) {
return
}
if !number_in_range(record.rdata[0], 0, 65535) {
diagnostics.push(
diagnostic(
"Z104",
"error",
"MX preference must be 0..65535",
record.line,
record.column,
owner=record.owner,
),
)
}
check_domain_field(record, 1, origin, diagnostics)
}
///|
fn check_srv(
record : ResourceRecord,
origin : String,
diagnostics : Array[Diagnostic],
) -> Unit {
if !require_rdata_length(record, 4, diagnostics) {
return
}
for i in 0..<3 {
if !number_in_range(record.rdata[i], 0, 65535) {
diagnostics.push(
diagnostic(
"Z105",
"error",
"SRV priority, weight, and port must be 0..65535",
record.line,
record.column,
owner=record.owner,
),
)
break
}
}
check_domain_field(record, 3, origin, diagnostics)
}
///|
fn check_caa(record : ResourceRecord, diagnostics : Array[Diagnostic]) -> Unit {
if !require_rdata_length(record, 3, diagnostics) {
return
}
if !number_in_range(record.rdata[0], 0, 255) {
diagnostics.push(
diagnostic(
"Z106",
"error",
"CAA flags must be 0..255",
record.line,
record.column,
owner=record.owner,
),
)
}
if record.rdata[1].length() == 0 || record.rdata[1].length() > 15 {
diagnostics.push(
diagnostic(
"Z107",
"error",
"CAA tag length must be 1..15",
record.line,
record.column,
owner=record.owner,
),
)
}
}
///|
fn check_record_data(
record : ResourceRecord,
origin : String,
diagnostics : Array[Diagnostic],
) -> Unit {
match record.record_type {
"A" | "AAAA" => check_address_record(record, diagnostics)
"NS" | "CNAME" | "PTR" =>
if require_rdata_length(record, 1, diagnostics) {
check_domain_field(record, 0, origin, diagnostics)
}
"SOA" => check_soa(record, origin, diagnostics)
"MX" => check_mx(record, origin, diagnostics)
"SRV" => check_srv(record, origin, diagnostics)
"CAA" => check_caa(record, diagnostics)
"TXT" | "SPF" =>
if record.rdata.length() == 0 {
diagnostics.push(
diagnostic(
"Z108",
"error",
"text record requires at least one string",
record.line,
record.column,
owner=record.owner,
),
)
}
_ => ()
}
}