use std::sync::LazyLock;
use crate::cbor_utils::{as_u64, map_get};
use crate::native::core::{EngineError, ManyState, NativeTestCase, Status};
use crate::native::intervalsets::IntervalSet;
use ciborium::Value;
use super::many_more;
const IANA_TLDS_TXT: &str = include_str!("tlds-alpha-by-domain.txt");
static TOP_LEVEL_DOMAINS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
let mut sorted: Vec<&'static str> = IANA_TLDS_TXT
.lines()
.filter(|line| !line.starts_with('#') && *line != "ARPA")
.collect();
sorted.sort_by_key(|s| s.len());
let mut result = Vec::with_capacity(sorted.len() + 1);
result.push("COM");
result.extend(sorted);
result
});
fn email_local_part_intervals() -> IntervalSet {
IntervalSet::new(vec![
(b'!' as u32, b'!' as u32), (b'#' as u32, b'\'' as u32), (b'*' as u32, b'+' as u32), (b'-' as u32, b'-' as u32), (b'/' as u32, b'9' as u32), (b'=' as u32, b'=' as u32), (b'A' as u32, b'Z' as u32), (b'^' as u32, b'`' as u32), (b'a' as u32, b'z' as u32), (b'{' as u32, b'~' as u32), ])
}
fn printable_ascii_intervals() -> IntervalSet {
IntervalSet::new(vec![(9, 13), (32, 126)])
}
fn fragment_byte_intervals() -> IntervalSet {
IntervalSet::new(vec![(0, 0xFF)])
}
fn encode_string(s: String) -> Value {
Value::Tag(91, Box::new(Value::Bytes(s.into_bytes())))
}
fn mark_invalid(ntc: &mut NativeTestCase) -> Result<Value, EngineError> {
ntc.status = Some(Status::Invalid);
Err(EngineError::StopTest)
}
pub(super) fn interpret_domain(
ntc: &mut NativeTestCase,
schema: &Value,
) -> Result<Value, EngineError> {
let max_length = map_get(schema, "max_length")
.and_then(as_u64)
.unwrap_or(255) as usize;
Ok(encode_string(draw_domain(ntc, max_length)?))
}
fn draw_domain(ntc: &mut NativeTestCase, max_length: usize) -> Result<String, EngineError> {
const MAX_LABEL_LEN: usize = 63;
let eligible: Vec<&'static str> = TOP_LEVEL_DOMAINS
.iter()
.copied()
.filter(|tld| tld.len() + 2 <= max_length)
.collect();
assert!(
!eligible.is_empty(),
"domain max_length={max_length} leaves no eligible TLDs"
);
let idx = ntc.draw_integer(0, (eligible.len() - 1) as i128)? as usize;
let tld = eligible[idx];
let mut domain = String::with_capacity(tld.len());
for c in tld.chars() {
let flip = ntc.weighted(0.5, None)?;
domain.push(if flip { c.to_ascii_lowercase() } else { c });
}
let mut state = ManyState::new(1, Some(126));
loop {
if !many_more(ntc, &mut state)? {
break;
}
let label = draw_dns_label(ntc, MAX_LABEL_LEN)?;
if domain.len() + 1 + label.len() > max_length {
break;
}
let mut next = String::with_capacity(label.len() + 1 + domain.len());
next.push_str(&label);
next.push('.');
next.push_str(&domain);
domain = next;
}
Ok(domain)
}
fn draw_dns_label(ntc: &mut NativeTestCase, max_len: usize) -> Result<String, EngineError> {
let len = ntc.draw_integer(1, max_len as i128)? as usize;
let mut s = String::with_capacity(len);
s.push(draw_ascii_letter(ntc)?);
if len > 1 {
for i in 1..(len - 1) {
let avoid_dash = i == 3 && s.as_bytes()[2] == b'-';
s.push(if avoid_dash {
draw_ascii_alnum(ntc)?
} else {
draw_ascii_alnum_or_hyphen(ntc)?
});
}
s.push(draw_ascii_alnum(ntc)?);
}
Ok(s)
}
fn draw_ascii_letter(ntc: &mut NativeTestCase) -> Result<char, EngineError> {
let i = ntc.draw_integer(0, 51)? as u8;
let b = if i < 26 { b'a' + i } else { b'A' + (i - 26) };
Ok(b as char)
}
fn draw_ascii_alnum(ntc: &mut NativeTestCase) -> Result<char, EngineError> {
let i = ntc.draw_integer(0, 61)? as u8;
let b = if i < 26 {
b'a' + i
} else if i < 52 {
b'A' + (i - 26)
} else {
b'0' + (i - 52)
};
Ok(b as char)
}
fn draw_ascii_alnum_or_hyphen(ntc: &mut NativeTestCase) -> Result<char, EngineError> {
let i = ntc.draw_integer(0, 62)? as u8;
let b = if i < 26 {
b'a' + i
} else if i < 52 {
b'A' + (i - 26)
} else if i < 62 {
b'0' + (i - 52)
} else {
b'-'
};
Ok(b as char)
}
pub(super) fn interpret_email(ntc: &mut NativeTestCase) -> Result<Value, EngineError> {
let local = ntc.draw_string(email_local_part_intervals(), 1, 64)?;
let domain = draw_domain(ntc, 255)?;
let address = format!("{local}@{domain}");
if address.len() > 254 {
return mark_invalid(ntc);
}
Ok(encode_string(address))
}
pub(super) fn interpret_url(ntc: &mut NativeTestCase) -> Result<Value, EngineError> {
let scheme = if ntc.weighted(0.5, None)? {
"https"
} else {
"http"
};
let domain = draw_domain(ntc, 255)?;
let port = if ntc.weighted(0.5, None)? {
format!(":{}", ntc.draw_integer(1, 65535)?)
} else {
String::new()
};
let printable = printable_ascii_intervals();
let mut state = ManyState::new(0, None);
let mut components: Vec<String> = Vec::new();
loop {
if !many_more(ntc, &mut state)? {
break;
}
let raw = ntc.draw_string(printable.clone(), 0, 100)?;
components.push(url_encode_path(&raw));
}
let path = components.join("/");
let fragment = if ntc.weighted(0.5, None)? {
let raw = ntc.draw_string(fragment_byte_intervals(), 1, 100)?;
format!("#{}", url_encode_fragment(ntc, &raw)?)
} else {
String::new()
};
let url = format!("{scheme}://{domain}{port}/{path}{fragment}");
Ok(encode_string(url))
}
fn is_url_safe(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'$' | '-' | '_' | '.' | '+' | '!' | '*' | '\'' | '(' | ')' | ',' | '~'
)
}
fn is_fragment_safe(c: char) -> bool {
is_url_safe(c) || c == '?' || c == '/'
}
fn url_encode_path(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if is_url_safe(c) {
out.push(c);
} else {
push_percent_encoded(&mut out, c);
}
}
out
}
fn url_encode_fragment(ntc: &mut NativeTestCase, s: &str) -> Result<String, EngineError> {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
let force_encode = ntc.weighted(0.5, None)?;
if force_encode || !is_fragment_safe(c) {
push_percent_encoded(&mut out, c);
} else {
out.push(c);
}
}
Ok(out)
}
fn push_percent_encoded(out: &mut String, c: char) {
let cp = c as u32;
debug_assert!(
cp <= 0xFF,
"push_percent_encoded called with codepoint > 0xFF: {cp:#x}"
);
out.push_str(&format!("%{:02X}", cp & 0xFF));
}
#[cfg(test)]
#[path = "../../../tests/embedded/native/schema/internet_tests.rs"]
mod tests;