mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 08:01:37 +00:00
189 lines
5.9 KiB
Rust
189 lines
5.9 KiB
Rust
use std::{
|
|
collections::{BTreeMap, HashMap},
|
|
env,
|
|
fmt::Write as _,
|
|
fs,
|
|
path::PathBuf,
|
|
};
|
|
|
|
const LEAF_FLAG: u8 = 1;
|
|
const EXCEPTION_FLAG: u8 = 2;
|
|
const MAX_LABEL_LEN: usize = u8::MAX as usize;
|
|
const MAX_EDGE_COUNT: usize = u16::MAX as usize;
|
|
const MAX_LABEL_OFFSET: usize = 0x00ff_ffff;
|
|
|
|
#[derive(Default)]
|
|
struct TrieNode {
|
|
flags: u8,
|
|
children: BTreeMap<String, TrieNode>,
|
|
}
|
|
|
|
fn main() {
|
|
const SOURCE_PATH: &str = "src/data/public_domains.txt";
|
|
println!("cargo:rerun-if-changed={SOURCE_PATH}");
|
|
|
|
let source = fs::read_to_string(SOURCE_PATH).expect("read vendored public suffix list");
|
|
let (root, rule_count) = parse_rules(&source);
|
|
let generated = generate_tables(&root, rule_count);
|
|
let output = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"))
|
|
.join("public_suffix_tables.rs");
|
|
fs::write(output, generated).expect("write generated public suffix tables");
|
|
}
|
|
|
|
fn parse_rules(source: &str) -> (TrieNode, usize) {
|
|
let mut root = TrieNode::default();
|
|
let mut rule_count = 0;
|
|
|
|
for raw_line in source.lines() {
|
|
let line = raw_line.trim();
|
|
if line.is_empty() || line.starts_with("//") {
|
|
continue;
|
|
}
|
|
|
|
let (rule, is_exception) = match line.strip_prefix('!') {
|
|
Some(rule) => (rule, true),
|
|
None => (line, false),
|
|
};
|
|
assert!(!rule.is_empty(), "public suffix rule must not be empty");
|
|
assert!(
|
|
rule.is_ascii() && rule.bytes().all(|byte| !byte.is_ascii_uppercase()),
|
|
"public suffix rule must be lowercase ASCII: {line}"
|
|
);
|
|
assert!(
|
|
!rule.contains('*') || (rule.starts_with("*.") && rule[2..].find('*').is_none()),
|
|
"public suffix wildcard must be the leftmost label: {line}"
|
|
);
|
|
|
|
let mut node = &mut root;
|
|
let mut label_count = 0;
|
|
for label in rule.rsplit('.') {
|
|
assert!(
|
|
!label.is_empty(),
|
|
"public suffix label must not be empty: {line}"
|
|
);
|
|
assert!(
|
|
label.len() <= MAX_LABEL_LEN,
|
|
"public suffix label is too long: {label}"
|
|
);
|
|
node = node.children.entry(label.to_owned()).or_default();
|
|
label_count += 1;
|
|
}
|
|
assert!(
|
|
!is_exception || label_count > 1,
|
|
"public suffix exception must contain more than one label: {line}"
|
|
);
|
|
assert_eq!(node.flags, 0, "duplicate public suffix rule: {line}");
|
|
node.flags = LEAF_FLAG | if is_exception { EXCEPTION_FLAG } else { 0 };
|
|
rule_count += 1;
|
|
}
|
|
|
|
assert!(
|
|
rule_count > 0,
|
|
"vendored public suffix list must not be empty"
|
|
);
|
|
(root, rule_count)
|
|
}
|
|
|
|
fn generate_tables(root: &TrieNode, rule_count: usize) -> String {
|
|
// Nodes are visited breadth-first. Every edge appends exactly one child to
|
|
// `nodes`, so an edge at index N always points to node N + 1. That removes
|
|
// the child-index field from the generated edge table.
|
|
let mut nodes = vec![root];
|
|
let mut node_metadata = Vec::new();
|
|
let mut edges = Vec::new();
|
|
let mut label_offsets = HashMap::<&str, u32>::new();
|
|
let mut labels = Vec::new();
|
|
let mut node_index = 0;
|
|
|
|
while node_index < nodes.len() {
|
|
let node = nodes[node_index];
|
|
let edge_start = edges.len();
|
|
assert!(
|
|
node.children.len() <= MAX_EDGE_COUNT,
|
|
"a public suffix trie node has too many children"
|
|
);
|
|
|
|
for (label, child) in &node.children {
|
|
let label_start = match label_offsets.get(label.as_str()) {
|
|
Some(offset) => *offset,
|
|
None => {
|
|
assert!(
|
|
labels.len() <= MAX_LABEL_OFFSET,
|
|
"public suffix label blob exceeds the 24-bit edge encoding"
|
|
);
|
|
let offset = labels.len() as u32;
|
|
labels.extend_from_slice(label.as_bytes());
|
|
label_offsets.insert(label, offset);
|
|
offset
|
|
}
|
|
};
|
|
let encoded_edge = (label_start << 8) | label.len() as u32;
|
|
edges.push(encoded_edge);
|
|
nodes.push(child);
|
|
assert_eq!(
|
|
nodes.len() - 1,
|
|
edges.len(),
|
|
"breadth-first edge index must identify its child node"
|
|
);
|
|
}
|
|
|
|
let metadata =
|
|
edge_start as u64 | ((node.children.len() as u64) << 32) | ((node.flags as u64) << 48);
|
|
node_metadata.push(metadata);
|
|
node_index += 1;
|
|
}
|
|
|
|
assert_eq!(nodes.len(), node_metadata.len());
|
|
assert_eq!(nodes.len(), edges.len() + 1);
|
|
|
|
let mut output = String::new();
|
|
writeln!(output, "#[cfg(test)]").unwrap();
|
|
writeln!(output, "pub(super) const RULE_COUNT: usize = {rule_count};").unwrap();
|
|
writeln!(
|
|
output,
|
|
"pub(super) static NODE_METADATA: [u64; {}] = [",
|
|
node_metadata.len()
|
|
)
|
|
.unwrap();
|
|
for chunk in node_metadata.chunks(8) {
|
|
output.push_str(" ");
|
|
for value in chunk {
|
|
write!(output, "0x{value:016x}, ").unwrap();
|
|
}
|
|
output.push('\n');
|
|
}
|
|
output.push_str("];\n");
|
|
|
|
writeln!(
|
|
output,
|
|
"pub(super) static EDGES: [u32; {}] = [",
|
|
edges.len()
|
|
)
|
|
.unwrap();
|
|
for chunk in edges.chunks(12) {
|
|
output.push_str(" ");
|
|
for value in chunk {
|
|
write!(output, "0x{value:08x}, ").unwrap();
|
|
}
|
|
output.push('\n');
|
|
}
|
|
output.push_str("];\n");
|
|
|
|
writeln!(
|
|
output,
|
|
"pub(super) static LABELS: [u8; {}] = [",
|
|
labels.len()
|
|
)
|
|
.unwrap();
|
|
for chunk in labels.chunks(24) {
|
|
output.push_str(" ");
|
|
for byte in chunk {
|
|
write!(output, "0x{byte:02x}, ").unwrap();
|
|
}
|
|
output.push('\n');
|
|
}
|
|
output.push_str("];\n");
|
|
|
|
output
|
|
}
|