use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use crate::cbor_utils::{as_text, as_u64, map_get};
use crate::native::core::{EngineError, NativeTestCase};
use crate::native::intervalsets::IntervalSet;
use crate::unicodedata;
use ciborium::Value;
const DEFAULT_MAX_SIZE: usize = 100;
pub(super) fn interpret_string(
ntc: &mut NativeTestCase,
schema: &Value,
) -> Result<Value, EngineError> {
let min_size = map_get(schema, "min_size").and_then(as_u64).unwrap_or(0) as usize;
let max_size = map_get(schema, "max_size")
.and_then(as_u64)
.map(|n| n as usize)
.unwrap_or(min_size.max(DEFAULT_MAX_SIZE));
let intervals = build_intervals(schema)?;
if intervals.is_empty() && max_size > 0 {
return Err(EngineError::InvalidArgument(
"InvalidArgument: No valid characters in the specified range. \
The schema's codec/codepoint/category/include/exclude constraints \
leave no characters available."
.to_string(),
));
}
let s = ntc.draw_string(intervals, min_size, max_size)?;
Ok(Value::Tag(91, Box::new(Value::Bytes(s.into_bytes()))))
}
pub(super) fn build_intervals(schema: &Value) -> Result<IntervalSet, EngineError> {
type Cache = Mutex<HashMap<Vec<u8>, Arc<IntervalSet>>>;
static CACHE: OnceLock<Cache> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut key = Vec::new();
ciborium::into_writer(schema, &mut key).expect("CBOR encoding of schema cannot fail");
if let Some(cached) = cache.lock().unwrap().get(&key) {
return Ok((**cached).clone());
}
let computed = Arc::new(build_intervals_uncached(schema)?);
cache.lock().unwrap().insert(key, Arc::clone(&computed));
Ok((*computed).clone())
}
fn build_intervals_uncached(schema: &Value) -> Result<IntervalSet, EngineError> {
let codec = map_get(schema, "codec").and_then(as_text);
let (codec_min, codec_max): (u32, u32) = match codec {
Some("ascii") => (0, 127),
Some("latin-1") | Some("iso-8859-1") => (0, 255),
Some("utf-8") | None => (0, 0x10FFFF),
Some(other) => {
return Err(EngineError::InvalidArgument(format!(
"invalid codec: {other}"
)));
}
};
let (mut cp_min, mut cp_max) = (codec_min, codec_max);
if let Some(min_cp) = map_get(schema, "min_codepoint").and_then(as_u64) {
cp_min = cp_min.max(min_cp as u32);
}
if let Some(max_cp) = map_get(schema, "max_codepoint").and_then(as_u64) {
cp_max = cp_max.min(max_cp as u32);
}
let categories: Option<Vec<String>> = extract_string_array(schema, "categories");
let exclude_categories: Option<Vec<String>> =
extract_string_array(schema, "exclude_categories");
let include_chars: Option<Vec<char>> = map_get(schema, "include_characters")
.and_then(as_text)
.map(|s| s.chars().collect());
let exclude_chars: Option<Vec<char>> = map_get(schema, "exclude_characters")
.and_then(as_text)
.map(|s| s.chars().collect());
for cat in categories
.iter()
.flatten()
.chain(exclude_categories.iter().flatten())
{
if !is_valid_category(cat) {
return Err(EngineError::InvalidArgument(format!(
"{cat:?} is not a valid Unicode category"
)));
}
}
if codec.is_some() {
if let Some(ref incl) = include_chars {
let bad: Vec<char> = incl
.iter()
.filter(|c| {
let cp = **c as u32;
cp < codec_min || cp > codec_max
})
.copied()
.collect();
if !bad.is_empty() {
let codec_name = codec.unwrap_or_default();
return Err(EngineError::InvalidArgument(format!(
"include_characters {bad:?} cannot be encoded by codec {codec_name:?}"
)));
}
}
}
if let (Some(incl), Some(excl)) = (include_chars.as_ref(), exclude_chars.as_ref()) {
let overlap: Vec<char> = incl.iter().filter(|c| excl.contains(c)).copied().collect();
if !overlap.is_empty() {
let incl_str: String = incl.iter().collect();
let excl_str: String = excl.iter().collect();
return Err(EngineError::InvalidArgument(format!(
"characters {overlap:?} are present in both \
include_characters={incl_str:?} and exclude_characters={excl_str:?} (overlap)"
)));
}
}
let base = range_minus_surrogates(cp_min, cp_max);
let needs_category_filter = categories.is_some()
|| exclude_categories
.as_ref()
.map(|ec| !ec.iter().all(|c| c == "Cs"))
.unwrap_or(false);
let mut intervals = if let Some(ref cats) = categories {
if cats.is_empty() {
IntervalSet::new(Vec::new())
} else {
let cat_union = categories_union(cats);
base.intersection(&cat_union)
}
} else if let Some(ref excl_cats) = exclude_categories {
if needs_category_filter {
let cat_union = categories_union(
&excl_cats
.iter()
.filter(|c| c.as_str() != "Cs")
.cloned()
.collect::<Vec<_>>(),
);
base.difference(&cat_union)
} else {
base
}
} else {
base
};
if let Some(ref excl) = exclude_chars {
if !excl.is_empty() {
let excl_set = chars_to_intervals(excl);
intervals = intervals.difference(&excl_set);
}
}
if let Some(ref incl) = include_chars {
if !incl.is_empty() {
let incl_filtered: Vec<char> = incl
.iter()
.filter(|c| !is_surrogate(**c))
.copied()
.collect();
if !incl_filtered.is_empty() {
let incl_set = chars_to_intervals(&incl_filtered);
intervals = intervals.union(&incl_set);
}
}
}
Ok(intervals)
}
fn range_minus_surrogates(min: u32, max: u32) -> IntervalSet {
if min > max {
return IntervalSet::new(Vec::new());
}
let mut ranges: Vec<(u32, u32)> = Vec::new();
if max < 0xD800 || min > 0xDFFF {
ranges.push((min, max));
} else {
if min < 0xD800 {
ranges.push((min, 0xD7FF));
}
if max > 0xDFFF {
ranges.push((0xE000, max));
}
}
IntervalSet::new(ranges)
}
fn chars_to_intervals(chars: &[char]) -> IntervalSet {
let mut cps: Vec<u32> = chars.iter().map(|c| *c as u32).collect();
cps.sort_unstable();
cps.dedup();
let mut ranges: Vec<(u32, u32)> = Vec::new();
for cp in cps {
match ranges.last_mut() {
Some(last) if cp == last.1 + 1 => last.1 = cp,
_ => ranges.push((cp, cp)),
}
}
IntervalSet::new(ranges)
}
fn categories_union(cats: &[String]) -> IntervalSet {
static CACHE: OnceLock<Mutex<HashMap<String, Arc<IntervalSet>>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut union: Option<IntervalSet> = None;
for cat in cats {
let cached = {
let map = cache.lock().unwrap();
map.get(cat).map(Arc::clone)
};
let single = match cached {
Some(s) => s,
None => {
let s = Arc::new(category_intervalset(cat));
cache.lock().unwrap().insert(cat.clone(), Arc::clone(&s));
s
}
};
union = Some(match union {
Some(u) => u.union(&single),
None => (*single).clone(),
});
}
union.unwrap_or_else(|| IntervalSet::new(Vec::new()))
}
fn category_intervalset(cat: &str) -> IntervalSet {
let mut ranges: Vec<(u32, u32)> = Vec::new();
let mut run_start: Option<u32> = None;
for cp in 0u32..=0x10FFFF {
if (0xD800..=0xDFFF).contains(&cp) {
if let Some(start) = run_start.take() {
ranges.push((start, cp - 1));
}
continue;
}
if unicodedata::is_in_group(cp, cat) {
if run_start.is_none() {
run_start = Some(cp);
}
} else if let Some(start) = run_start.take() {
ranges.push((start, cp - 1));
}
}
if let Some(start) = run_start {
ranges.push((start, 0x10FFFF));
}
IntervalSet::new(ranges)
}
fn extract_string_array(schema: &Value, key: &str) -> Option<Vec<String>> {
map_get(schema, key).and_then(|v| {
if let Value::Array(arr) = v {
Some(arr.iter().filter_map(as_text).map(String::from).collect())
} else {
None
}
})
}
fn is_surrogate(c: char) -> bool {
(0xD800..=0xDFFF).contains(&(c as u32))
}
fn is_valid_category(cat: &str) -> bool {
matches!(
cat,
"L" | "M"
| "N"
| "P"
| "S"
| "Z"
| "C"
| "Lu"
| "Ll"
| "Lt"
| "Lm"
| "Lo"
| "Mn"
| "Mc"
| "Me"
| "Nd"
| "Nl"
| "No"
| "Pc"
| "Pd"
| "Ps"
| "Pe"
| "Pi"
| "Pf"
| "Po"
| "Sm"
| "Sc"
| "Sk"
| "So"
| "Zs"
| "Zl"
| "Zp"
| "Cc"
| "Cf"
| "Cs"
| "Co"
| "Cn"
)
}
#[cfg(test)]
#[path = "../../../tests/embedded/native/schema/text_tests.rs"]
mod tests;