use std::env;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::vec;
use anyhow::{bail, Context, Result};
use hyper::Method;
use sha2::{Digest, Sha256};
use tracing::{self, info};
use kapiti::{fetcher::Fetcher, hyper_smol, logging};
fn main() -> Result<()> {
logging::init_logging();
let csv_dir = Path::new("dns-specs");
let sections_csv_path = csv_dir.join("sections.csv");
let sections = csv::Reader::from_path(§ions_csv_path)
.with_context(|| format!("Failed to open {:?}", sections_csv_path))?
.deserialize()
.map(|v| v.expect("Failed to deserialize sections.csv entry"))
.collect();
match env::args().nth(1) {
Some(val) => {
if val == "--fetch" {
info!("Fetching csvs (--fetch):");
smol::block_on(download_csvs(&csv_dir, §ions))?;
} else {
panic!("Unrecognized arg: {}", val);
}
}
None => info!("Skipping csv download (no --fetch)"),
}
let specs_dir = Path::new("src").join("specs");
generate_enums_rs(&specs_dir.join("enums_generated.rs"), &csv_dir, §ions)?;
generate_version_rs(&specs_dir.join("version_generated.rs"), &specs_dir)?;
Ok(())
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct SectionsRecord {
Filename: String,
EnumName: String,
DataType: String,
}
async fn download_csvs(csv_dir: &Path, sections: &Vec<SectionsRecord>) -> Result<()> {
let fetcher = Fetcher::new(1024 * 1024, Some("text/csv".to_string()));
let client = hyper_smol::client_system(false, Duration::from_millis(5000));
let request_count = sections.len();
for i in 0..request_count {
let section = sections
.get(i)
.with_context(|| format!("Missing section index {}", i))?;
let url = format!(
"https://www.iana.org/assignments/dns-parameters/{}",
section.Filename.as_str()
);
let path = csv_dir.join(§ion.Filename);
info!("- Downloading {:?} to {:?}", url, path);
let req = fetcher
.build_request(&Method::GET, &url)
.expect("Failed to build HTTP request");
let mut resp = client
.request(req)
.await
.with_context(|| format!("Failed to fetch {:?}", url))?;
let path = csv_dir.join(§ion.Filename);
info!(
"[{}/{}] {:?} status: {}",
i + 1,
request_count,
section.Filename,
resp.status()
);
let mut outfile = File::create(&path.as_path())?;
fetcher
.write_response(§ion.Filename, &mut outfile, &mut resp)
.await?;
}
Ok(())
}
fn generate_enums_rs(
enums_rs: &Path,
csv_dir: &Path,
sections: &Vec<SectionsRecord>,
) -> Result<()> {
let mut enumsfile =
fs::File::create(&enums_rs).with_context(|| "Failed to create dns enums rust file")?;
info!("Generating {:?}", enums_rs);
enumsfile.write(b"// This file is autogenerated by update_specs.rs. Don't touch.\n")?;
enumsfile.write(b"use bytecheck::CheckBytes;\n")?;
enumsfile.write(b"use rkyv::{Archive, Deserialize, Serialize};\n")?;
for section in sections {
let enum_csv_path = csv_dir.join(§ion.Filename);
info!("- Parsing {:?} from {:?}", section.EnumName, enum_csv_path);
let entries = match section.EnumName.as_str() {
"ResourceClass" => generate_resourceclass_enum(enum_csv_path)?,
"ResourceType" => generate_resourcetype_enum(enum_csv_path)?,
"OpCode" => generate_opcode_enum(enum_csv_path)?,
"ResponseCode" => generate_responsecode_enum(enum_csv_path)?,
"OPTOptionCode" => generate_optoptioncode_enum(enum_csv_path)?,
&_ => panic!("Unsupported enum: {}", section.EnumName),
};
enumsfile.write(format!("#[repr({})]\n", section.DataType).as_bytes())?;
enumsfile.write(
b"#[derive(
Archive, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug,
)]\n",
)?;
enumsfile.write(b"#[archive_attr(derive(CheckBytes))]\n")?;
enumsfile.write(format!("pub enum {} {{\n", section.EnumName).as_bytes())?;
for entry in &entries {
if entry.doc {
enumsfile.write(format!("\n /// {}\n", entry.comment1).as_bytes())?;
if let Some(c2) = &entry.comment2 {
enumsfile.write(format!(" /// {}\n", c2).as_bytes())?;
}
} else {
enumsfile.write(format!("\n // {}\n", entry.comment1).as_bytes())?;
if let Some(c2) = &entry.comment2 {
enumsfile.write(format!(" // {}\n", c2).as_bytes())?;
}
}
if let Some(n) = &entry.name {
if let Some(i) = &entry.intval {
enumsfile.write(format!(" {} = {},\n", n, i).as_bytes())?;
} else {
panic!("Missing entry.intval for entry.name={}", n)
}
}
}
enumsfile.write(b"}\n")?;
enumsfile.write(
format!(
"\npub fn {}_int(i: usize) -> Option<{}> {{\n",
section.EnumName.to_lowercase(),
section.EnumName
)
.as_bytes(),
)?;
enumsfile.write(b" match i {\n")?;
for entry in &entries {
if let Some(n) = &entry.name {
if let Some(i) = &entry.intval {
enumsfile.write(
format!(" {} => Some({}::{}),\n", i, section.EnumName, n).as_bytes(),
)?;
} else {
panic!("Missing entry.intval for entry.name={}", n)
}
}
}
enumsfile.write(b" _ => None,\n")?;
enumsfile.write(b" }\n")?;
enumsfile.write(b"}\n")?;
}
Ok(())
}
struct EnumEntry {
doc: bool,
comment1: String,
comment2: Option<String>,
name: Option<String>,
intval: Option<String>,
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct ResourceClassRecord {
Decimal: String,
Name: String,
Reference: String,
}
fn generate_resourceclass_enum(enum_csv_path: PathBuf) -> Result<Vec<EnumEntry>> {
let mut entries = Vec::new();
for record in csv::Reader::from_path(&enum_csv_path)
.with_context(|| format!("Failed to open enum CSV file: {:?}", enum_csv_path))?
.deserialize()
{
let record: ResourceClassRecord = record
.with_context(|| format!("Failed to read/deserialize CSV file: {:?}", enum_csv_path))?;
let ignored = record.Decimal.contains('-') || record.Name == "Unassigned";
let mut comment1 = record.Name.clone();
if !record.Reference.is_empty() {
comment1.push_str(format!(" {}", record.Reference.replace('\n', " ")).as_str());
}
if ignored {
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: Some(record.Decimal),
name: None,
intval: None,
});
} else {
let name = match record.Name.as_str() {
"Reserved" => format!("RESERVED_{}", record.Decimal),
"QCLASS NONE" => "NONE".to_string(),
"QCLASS * (ANY)" => "ANY".to_string(),
_ => match record.Name.split_ascii_whitespace().next() {
Some(first_word) => first_word.to_uppercase().replace("CLASS_", ""),
None => panic!("Missing name '{}' in {:?}", record.Name, enum_csv_path),
},
};
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: None,
name: Some(name),
intval: Some(record.Decimal),
});
}
}
Ok(entries)
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct ResourceTypeRecord {
Value: String,
TYPE: String,
Meaning: String,
Reference: String,
}
fn generate_resourcetype_enum(enum_csv_path: PathBuf) -> Result<Vec<EnumEntry>> {
let mut entries = Vec::new();
for record in csv::Reader::from_path(&enum_csv_path)
.with_context(|| format!("Failed to open enum CSV file: {:?}", enum_csv_path))?
.deserialize()
{
let record: ResourceTypeRecord = record
.with_context(|| format!("Failed to read/deserialize CSV file: {:?}", enum_csv_path))?;
let ignored = record.Value.contains('-') || record.TYPE == "Unassigned";
let mut comment1: String;
if record.Reference.is_empty() && record.Meaning.is_empty() {
comment1 = record.TYPE.clone();
} else {
comment1 = String::new();
if !record.Meaning.is_empty() {
comment1.push_str(record.Meaning.replace('\n', " ").as_str());
}
if !record.Reference.is_empty() {
if !record.Meaning.is_empty() {
comment1.push(' ');
}
comment1.push_str(record.Reference.replace('\n', " ").as_str())
}
}
if ignored {
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: Some(record.Value),
name: None,
intval: None,
});
} else {
let name = match record.TYPE.as_str() {
"Reserved" => format!("RESERVED_{}", record.Value),
"*" => "ANY".to_string(),
_ => match record.Meaning.contains("OBSOLETE") {
true => record.TYPE.replace('-', "_") + "_OBSOLETE",
false => record.TYPE.replace('-', "_"),
},
};
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: None,
name: Some(name),
intval: Some(record.Value),
});
}
}
Ok(entries)
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct OpCodeRecord {
OpCode: String,
Name: String,
Reference: String,
}
fn generate_opcode_enum(enum_csv_path: PathBuf) -> Result<Vec<EnumEntry>> {
let mut entries = Vec::new();
for record in csv::Reader::from_path(&enum_csv_path)
.with_context(|| format!("Failed to open enum CSV file: {:?}", enum_csv_path))?
.deserialize()
{
let record: OpCodeRecord = record
.with_context(|| format!("Failed to read/deserialize CSV file: {:?}", enum_csv_path))?;
let ignored = record.OpCode.contains('-') || record.Name == "Unassigned";
let mut comment1 = record.Name.clone();
if !record.Reference.is_empty() {
comment1.push_str(format!(" {}", record.Reference.replace('\n', " ")).as_str());
}
if ignored {
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: Some(record.OpCode),
name: None,
intval: None,
});
} else {
let name = match record.Name.as_str() {
"DNS Stateful Operations (DSO)" => "DSO".to_string(),
"IQuery (Inverse Query, OBSOLETE)" => "IQUERY_OBSOLETE".to_string(),
_ => record.Name.to_uppercase().replace("OP_", ""),
};
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: None,
name: Some(name),
intval: Some(record.OpCode),
});
}
}
Ok(entries)
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct ResponseCodeRecord {
RCODE: String,
Name: String,
Description: String,
Reference: String,
}
fn generate_responsecode_enum(enum_csv_path: PathBuf) -> Result<Vec<EnumEntry>> {
let mut entries = Vec::new();
for record in csv::Reader::from_path(&enum_csv_path)
.with_context(|| format!("Failed to open enum CSV file: {:?}", enum_csv_path))?
.deserialize()
{
let record: ResponseCodeRecord = record
.with_context(|| format!("Failed to read/deserialize CSV file: {:?}", enum_csv_path))?;
let ignored = record.RCODE.contains('-') || record.Name == "Unassigned";
let mut comment1 = record.Name.clone();
if !record.Description.is_empty() {
comment1.push_str(format!(": {}", record.Description.replace('\n', " ")).as_str());
}
if !record.Reference.is_empty() {
comment1.push_str(format!(" {}", record.Reference.replace('\n', " ")).as_str());
}
if ignored {
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: Some(record.RCODE),
name: None,
intval: None,
});
} else if record.Description == "Not Authorized" {
continue;
} else if record.Name == "BADSIG" {
continue;
} else {
let name = match record.Name.as_str() {
"Reserved, can be allocated by Standards Action" => "RESERVED_65535".to_string(),
"BADVERS" => "BADVERS_BADSIG".to_string(),
_ => record.Name.to_uppercase(),
};
let comment2 = match record.Name.as_str() {
"NotAuth" => Some(String::from("NotAuth: Not Authorized [RFC2845]")),
"BADVERS" => Some(String::from("BADSIG: TSIG Signature Failure [RFC2845]")),
_ => None,
};
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2,
name: Some(name),
intval: Some(record.RCODE),
});
}
}
Ok(entries)
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct OPTOptionCodeRecord {
Value: String,
Name: String,
Status: String,
Reference: String,
}
fn generate_optoptioncode_enum(enum_csv_path: PathBuf) -> Result<Vec<EnumEntry>> {
let mut entries = Vec::new();
for record in csv::Reader::from_path(&enum_csv_path)
.with_context(|| format!("Failed to open enum CSV file: {:?}", enum_csv_path))?
.deserialize()
{
let record: OPTOptionCodeRecord = record
.with_context(|| format!("Failed to read/deserialize CSV file: {:?}", enum_csv_path))?;
let ignored = record.Value.contains('-') || record.Name == "Unassigned";
let mut comment1 = record.Name.clone();
if !record.Status.is_empty() {
comment1.push_str(format!(": {}", record.Status.replace('\n', " ")).as_str());
}
if !record.Reference.is_empty() {
comment1.push_str(format!(" {}", record.Reference.replace('\n', " ")).as_str());
}
if ignored {
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: Some(record.Value),
name: None,
intval: None,
});
} else {
let name = match record.Value.as_str() {
"0" => "NONE".to_string(),
_ => match record.Name.as_str() {
"Reserved" => format!("RESERVED_{}", record.Value),
_ => record
.Name
.to_uppercase()
.replace('-', "_")
.replace(' ', "_"),
},
};
entries.push(EnumEntry {
doc: !ignored,
comment1,
comment2: None,
name: Some(name),
intval: Some(record.Value),
});
}
}
Ok(entries)
}
fn generate_version_rs(version_rs: &Path, specs_dir: &Path) -> Result<()> {
let mut file_contents = vec![];
for entry in fs::read_dir(specs_dir)? {
let entry = entry?;
let meta = entry.metadata()?;
let path = entry.path();
if meta.is_dir() {
bail!("Unexpected directory in {:?}: {:?}", specs_dir, path);
}
if meta.len() > 1024 * 1024 {
bail!("File is larger than 1MB: {:?}", path);
}
if path.file_name() == version_rs.file_name() {
continue;
}
let mut contents = vec![];
let filename = path
.file_name()
.expect("Missing filename")
.to_str()
.expect("Failed to convert filename")
.to_string();
info!(
"Read {} bytes from {:?}",
File::open(path)?.read_to_end(&mut contents)?,
filename
);
file_contents.push((filename, contents));
}
file_contents.sort_by(|(namea, _), (nameb, _)| namea.cmp(nameb));
let mut hasher = Sha256::new();
for (filename, contents) in file_contents {
info!("Hashing {}", filename);
hasher.update(contents);
}
let hash = format!("{:x}", hasher.finalize());
let mut hash_trunc = hash.clone();
hash_trunc.truncate(16);
info!("Hash: {} => {}", hash, hash_trunc);
let mut enumsfile =
fs::File::create(&version_rs).with_context(|| "Failed to create version rust file")?;
info!("Generating {:?}", version_rs);
enumsfile.write(b"// This file is autogenerated by update_specs.rs. Don't touch.\n")?;
enumsfile.write(format!("pub const VERSION_HASH: &str = \"{}\";\n", hash_trunc).as_bytes())?;
Ok(())
}