use std::sync::LazyLock;
use regex::Regex;
pub static SOA: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\S+\s+\d+\s+IN\s+SOA\s+").unwrap());
pub static ZONE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\S+)\s+(\d+)\s+IN\s+(\S+)\s+(.*)$").unwrap());
pub static PROXIED: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\s*;\s*cf_tags=cf-proxied:(true|false)\s*$").unwrap());
pub static TXT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""([^"]*)""#).unwrap());
#[derive(Debug, Clone)]
pub struct DnsRecord {
pub name: String,
pub ttl: u32,
pub rtype: String,
pub data: String,
pub proxied: Option<bool>,
}
pub fn parse_zone(content: &str) -> Vec<DnsRecord> {
let mut records = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with(';') {
continue;
}
let caps = match ZONE.captures(line) {
Some(c) => c,
None => continue,
};
let name = caps[1].to_string();
let ttl: u32 = caps[2].parse().unwrap_or(1);
let rtype = caps[3].to_uppercase();
if rtype == "SOA" {
continue;
}
let raw_data = caps[4].to_string();
let (data, proxied) = split_data_and_proxied(&raw_data);
records.push(DnsRecord {
name,
ttl,
rtype,
data,
proxied,
});
}
records
}
pub fn split_data_and_proxied(raw: &str) -> (String, Option<bool>) {
let proxied = PROXIED.captures(raw).map(|c| c[1].to_lowercase() == "true");
let data = PROXIED.replace(raw, "").trim().to_string();
(data, proxied)
}
pub fn strip_zone(fqdn: &str, zone: &str) -> String {
let fqdn = fqdn.trim_end_matches('.');
let zone = zone.trim_end_matches('.');
if fqdn == zone {
return "@".to_string();
}
let suffix = format!(".{zone}");
if fqdn.to_lowercase().ends_with(&suffix.to_lowercase()) {
let end = fqdn.len() - suffix.len();
return fqdn[..end].to_string();
}
fqdn.to_string()
}
pub fn parse_srv_name(fqdn: &str, zone: &str) -> (String, String, String) {
let record_part = strip_zone(fqdn, zone);
if record_part == "@" {
return ("@".to_string(), "_unknown".to_string(), "_tcp".to_string());
}
let parts: Vec<&str> = record_part.split('.').collect();
let service = if !parts.is_empty() && parts[0].starts_with('_') {
parts[0][1..].to_string()
} else {
"_unknown".to_string()
};
let proto = if parts.len() >= 2 && parts[1].starts_with('_') {
parts[1][1..].to_string()
} else {
"_tcp".to_string()
};
let remaining = if parts.len() > 2 {
parts[2..].join(".")
} else {
"@".to_string()
};
(remaining, service, proto)
}
pub fn parse_tlsa_name(fqdn: &str, zone: &str) -> (String, u32, String) {
let record_part = strip_zone(fqdn, zone);
if record_part == "@" {
return ("@".to_string(), 0, "tcp".to_string());
}
let parts: Vec<&str> = record_part.split('.').collect();
let port: u32 = if !parts.is_empty() && parts[0].starts_with('_') {
parts[0][1..].parse().unwrap_or(0)
} else {
0
};
let proto = if parts.len() >= 2 && parts[1].starts_with('_') {
parts[1][1..].to_string()
} else {
"tcp".to_string()
};
let remaining = if parts.len() > 2 {
parts[2..].join(".")
} else {
"@".to_string()
};
(remaining, port, proto)
}
pub fn parse_txt_data(raw: &str) -> String {
let mut result = String::new();
for cap in TXT.captures_iter(raw) {
result.push_str(&cap[1]);
}
result
}
pub fn can_proxy(rtype: &str) -> bool {
matches!(rtype, "A" | "AAAA" | "CNAME")
}