use super::{CodeUnitBuf, CodeUnits, MAX_STRING_LEN, StringError, is_in_table};
#[cfg(any(test, feature = "test-hooks"))]
use std::cell::Cell;
mod tables {
include!("generated/unicode/unicode_case_tables.rs");
}
const RUN_TYPE_U: u32 = 0;
const RUN_TYPE_L: u32 = 1;
const RUN_TYPE_UF: u32 = 2;
const RUN_TYPE_LF: u32 = 3;
const RUN_TYPE_UL: u32 = 4;
const RUN_TYPE_LSU: u32 = 5;
const RUN_TYPE_U2L_399_EXT2: u32 = 6;
const RUN_TYPE_UF_D20: u32 = 7;
const RUN_TYPE_UF_D1_EXT: u32 = 8;
const RUN_TYPE_U_EXT: u32 = 9;
const RUN_TYPE_LF_EXT: u32 = 10;
const RUN_TYPE_UF_EXT2: u32 = 11;
const RUN_TYPE_LF_EXT2: u32 = 12;
const RUN_TYPE_UF_EXT3: u32 = 13;
#[cfg(any(test, feature = "test-hooks"))]
thread_local! {
static FAIL_CASE_RESERVATION_AFTER: Cell<Option<usize>> = const { Cell::new(None) };
}
#[cfg(any(test, feature = "test-hooks"))]
pub fn fail_next_case_reservation_for_test() {
fail_case_reservation_after_for_test(0);
}
#[cfg(any(test, feature = "test-hooks"))]
fn fail_case_reservation_after_for_test(successful_reservations: usize) {
FAIL_CASE_RESERVATION_AFTER.with(|armed| {
assert!(
armed.replace(Some(successful_reservations)).is_none(),
"case reservation failure was already armed"
);
});
}
fn try_reserve_exact<T>(values: &mut Vec<T>, additional: usize) -> Result<(), StringError> {
#[cfg(any(test, feature = "test-hooks"))]
if FAIL_CASE_RESERVATION_AFTER.with(|armed| match armed.get() {
None => false,
Some(0) => {
armed.set(None);
true
}
Some(remaining) => {
armed.set(Some(remaining - 1));
false
}
}) {
return Err(StringError::OutOfMemory);
}
values
.try_reserve_exact(additional)
.map_err(|_| StringError::OutOfMemory)
}
enum CaseStorage {
Latin1(Vec<u8>),
Utf16(Vec<u16>),
}
struct CaseStringBuffer {
storage: CaseStorage,
size: usize,
limit: usize,
}
impl CaseStringBuffer {
fn new(initial_size: usize, limit: usize) -> Result<Self, StringError> {
let limit = limit.min(MAX_STRING_LEN);
let size = initial_size.min(limit);
let mut values = Vec::new();
try_reserve_exact(&mut values, size)?;
Ok(Self {
storage: CaseStorage::Latin1(values),
size,
limit,
})
}
fn len(&self) -> usize {
match &self.storage {
CaseStorage::Latin1(values) => values.len(),
CaseStorage::Utf16(values) => values.len(),
}
}
fn grow_size(&self, new_len: usize) -> Result<usize, StringError> {
if new_len > self.limit {
return Err(StringError::TooLong);
}
Ok(new_len.max(self.size.saturating_mul(3) / 2).min(self.limit))
}
fn reserve_to<T>(values: &mut Vec<T>, size: usize) -> Result<(), StringError> {
try_reserve_exact(values, size.saturating_sub(values.len()))
}
fn widen(&mut self, size: usize) -> Result<(), StringError> {
let CaseStorage::Latin1(narrow) =
std::mem::replace(&mut self.storage, CaseStorage::Latin1(Vec::new()))
else {
unreachable!("case buffer was widened twice")
};
let mut wide = Vec::new();
if let Err(error) = try_reserve_exact(&mut wide, size) {
self.storage = CaseStorage::Latin1(narrow);
return Err(error);
}
wide.extend(narrow.iter().copied().map(u16::from));
self.storage = CaseStorage::Utf16(wide);
self.size = size;
Ok(())
}
fn push_code_unit(&mut self, unit: u16) -> Result<(), StringError> {
let len = self.len();
let new_len = len.checked_add(1).ok_or(StringError::TooLong)?;
if new_len > self.limit {
return Err(StringError::TooLong);
}
if len >= self.size {
let new_size = self.grow_size(new_len)?;
match &mut self.storage {
CaseStorage::Latin1(_) if unit > u16::from(u8::MAX) => {
self.widen(new_size)?;
}
CaseStorage::Latin1(values) => {
Self::reserve_to(values, new_size)?;
self.size = new_size;
}
CaseStorage::Utf16(values) => {
Self::reserve_to(values, new_size)?;
self.size = new_size;
}
}
} else if matches!(self.storage, CaseStorage::Latin1(_)) && unit > u16::from(u8::MAX) {
self.widen(self.size)?;
}
match &mut self.storage {
CaseStorage::Latin1(values) => values.push(unit as u8),
CaseStorage::Utf16(values) => values.push(unit),
}
Ok(())
}
fn push_code_point(&mut self, code_point: u32) -> Result<(), StringError> {
debug_assert!(code_point <= 0x10_ffff);
if code_point < 0x1_0000 {
self.push_code_unit(code_point as u16)
} else {
let adjusted = code_point - 0x1_0000;
self.push_code_unit(0xd800 | (adjusted >> 10) as u16)?;
self.push_code_unit(0xdc00 | (adjusted & 0x3ff) as u16)
}
}
fn finish(self) -> CodeUnitBuf {
match self.storage {
CaseStorage::Latin1(values) => CodeUnitBuf::Latin1(values),
CaseStorage::Utf16(values) => CodeUnitBuf::Utf16(values),
}
}
}
#[derive(Clone, Copy)]
struct CaseMapping {
code_points: [u32; 3],
len: usize,
}
impl CaseMapping {
const fn one(code_point: u32) -> Self {
Self {
code_points: [code_point, 0, 0],
len: 1,
}
}
const fn two(first: u32, second: u32) -> Self {
Self {
code_points: [first, second, 0],
len: 2,
}
}
const fn three(first: u32, second: u32, third: u32) -> Self {
Self {
code_points: [first, second, third],
len: 3,
}
}
}
fn case_conversion_entry(
code_point: u32,
conversion: u32,
index: usize,
entry: u32,
) -> CaseMapping {
let run_type = (entry >> 4) & 0xf;
let data = ((entry & 0xf) << 8) | u32::from(tables::CASE_CONV_TABLE2[index]);
let run_start = entry >> 15;
let is_lower = u32::from(conversion != 0);
let mut converted = code_point;
match run_type {
RUN_TYPE_U | RUN_TYPE_L | RUN_TYPE_UF | RUN_TYPE_LF => {
if conversion == (run_type & 1) || run_type >= RUN_TYPE_UF && conversion == 2 {
let mapped_start = tables::CASE_CONV_TABLE1[data as usize] >> 15;
converted = code_point - run_start + mapped_start;
}
}
RUN_TYPE_UL => {
let offset = code_point - run_start;
if (offset & 1) == 1 - is_lower {
converted = (offset ^ 1) + run_start;
}
}
RUN_TYPE_LSU => {
let offset = code_point - run_start;
if offset == 1 {
converted = (i64::from(code_point) + (2 * i64::from(is_lower) - 1)) as u32;
} else if offset == (1 - is_lower) * 2 {
converted = (i64::from(code_point) + (2 * i64::from(is_lower) - 1) * 2) as u32;
}
}
RUN_TYPE_U2L_399_EXT2 => {
if conversion != 0 {
converted = code_point - run_start
+ u32::from(tables::CASE_CONV_EXT[(data & 0x3f) as usize]);
} else {
return CaseMapping::two(
code_point - run_start + u32::from(tables::CASE_CONV_EXT[(data >> 6) as usize]),
0x399,
);
}
}
RUN_TYPE_UF_D20 => {
if conversion != 1 {
converted = data + u32::from(conversion == 2) * 0x20;
}
}
RUN_TYPE_UF_D1_EXT => {
if conversion != 1 {
converted =
u32::from(tables::CASE_CONV_EXT[data as usize]) + u32::from(conversion == 2);
}
}
RUN_TYPE_U_EXT | RUN_TYPE_LF_EXT => {
if is_lower == run_type - RUN_TYPE_U_EXT {
converted = u32::from(tables::CASE_CONV_EXT[data as usize]);
}
}
RUN_TYPE_UF_EXT2 => {
if conversion != 1 {
return CaseMapping::two(
code_point - run_start + u32::from(tables::CASE_CONV_EXT[(data >> 6) as usize]),
u32::from(tables::CASE_CONV_EXT[(data & 0x3f) as usize]),
);
}
}
RUN_TYPE_LF_EXT2 => {
if conversion != 0 {
return CaseMapping::two(
code_point - run_start + u32::from(tables::CASE_CONV_EXT[(data >> 6) as usize]),
u32::from(tables::CASE_CONV_EXT[(data & 0x3f) as usize]),
);
}
}
RUN_TYPE_UF_EXT3 => {
if conversion != 1 {
return CaseMapping::three(
u32::from(tables::CASE_CONV_EXT[(data >> 8) as usize]),
u32::from(tables::CASE_CONV_EXT[((data >> 4) & 0xf) as usize]),
u32::from(tables::CASE_CONV_EXT[(data & 0xf) as usize]),
);
}
}
_ => unreachable!("invalid QuickJS case-conversion run type"),
}
CaseMapping::one(converted)
}
fn case_conversion(code_point: u32, to_lower: bool) -> CaseMapping {
if code_point < 128 {
let converted = if to_lower && (u32::from(b'A')..=u32::from(b'Z')).contains(&code_point) {
code_point + u32::from(b'a' - b'A')
} else if !to_lower && (u32::from(b'a')..=u32::from(b'z')).contains(&code_point) {
code_point - u32::from(b'a' - b'A')
} else {
code_point
};
return CaseMapping::one(converted);
}
let mut lower = 0;
let mut upper = tables::CASE_CONV_TABLE1.len();
while lower < upper {
let middle = (lower + upper) / 2;
let entry = tables::CASE_CONV_TABLE1[middle];
let run_start = entry >> 15;
let run_len = (entry >> 8) & 0x7f;
if code_point < run_start {
upper = middle;
} else if code_point >= run_start + run_len {
lower = middle + 1;
} else {
return case_conversion_entry(code_point, u32::from(to_lower), middle, entry);
}
}
CaseMapping::one(code_point)
}
pub fn regexp_canonicalize(code_point: u32, unicode: bool) -> u32 {
if code_point < 128 {
if unicode && (u32::from(b'A')..=u32::from(b'Z')).contains(&code_point) {
return code_point + u32::from(b'a' - b'A');
}
if !unicode && (u32::from(b'a')..=u32::from(b'z')).contains(&code_point) {
return code_point - u32::from(b'a' - b'A');
}
return code_point;
}
let mut lower = 0;
let mut upper = tables::CASE_CONV_TABLE1.len();
while lower < upper {
let middle = (lower + upper) / 2;
let entry = tables::CASE_CONV_TABLE1[middle];
let run_start = entry >> 15;
let run_len = (entry >> 8) & 0x7f;
if code_point < run_start {
upper = middle;
} else if code_point >= run_start + run_len {
lower = middle + 1;
} else {
let mapping =
case_conversion_entry(code_point, if unicode { 2 } else { 0 }, middle, entry);
if mapping.len == 1 {
let mapped = mapping.code_points[0];
return if unicode || mapped >= 128 {
mapped
} else {
code_point
};
}
if unicode {
return match code_point {
0xfb06 => 0xfb05,
0x1fd3 => 0x0390,
0x1fe3 => 0x03b0,
_ => code_point,
};
}
return code_point;
}
}
code_point
}
const CASE_U_RUN_MASK: u16 = (1_u16 << RUN_TYPE_U)
| (1_u16 << RUN_TYPE_UF)
| (1_u16 << RUN_TYPE_UL)
| (1_u16 << RUN_TYPE_LSU)
| (1_u16 << RUN_TYPE_U2L_399_EXT2)
| (1_u16 << RUN_TYPE_UF_D20)
| (1_u16 << RUN_TYPE_UF_D1_EXT)
| (1_u16 << RUN_TYPE_U_EXT)
| (1_u16 << RUN_TYPE_UF_EXT2)
| (1_u16 << RUN_TYPE_UF_EXT3);
const CASE_F_RUN_MASK: u16 = (1_u16 << RUN_TYPE_UF)
| (1_u16 << RUN_TYPE_LF)
| (1_u16 << RUN_TYPE_UL)
| (1_u16 << RUN_TYPE_LSU)
| (1_u16 << RUN_TYPE_U2L_399_EXT2)
| (1_u16 << RUN_TYPE_LF_EXT)
| (1_u16 << RUN_TYPE_LF_EXT2)
| (1_u16 << RUN_TYPE_UF_D20)
| (1_u16 << RUN_TYPE_UF_D1_EXT)
| (1_u16 << RUN_TYPE_UF_EXT2)
| (1_u16 << RUN_TYPE_UF_EXT3);
pub fn regexp_canonicalize_range_pairs(ranges: &[(u32, u32)], unicode: bool) -> Vec<(u32, u32)> {
let ranges = normalize_half_open_ranges(
ranges
.iter()
.filter(|(start, end)| start <= end)
.map(|(start, end)| (*start, end.saturating_add(1)))
.collect(),
);
let mask = regexp_case_change_mask(unicode);
let affected = intersect_half_open_ranges(&ranges, &mask);
let unaffected = subtract_half_open_ranges(&ranges, &mask);
let mut canonicalized = Vec::new();
canonicalized.reserve(affected.iter().fold(0_usize, |count, (start, end)| {
count.saturating_add((*end - *start) as usize)
}));
for (start, end) in affected {
canonicalized.extend((start..end).map(|code_point| {
let mapped = regexp_canonicalize(code_point, unicode);
(mapped, mapped + 1)
}));
}
canonicalized.extend(unaffected);
normalize_half_open_ranges(canonicalized)
.into_iter()
.map(|(start, end)| (start, end - 1))
.collect()
}
fn regexp_case_change_mask(unicode: bool) -> Vec<(u32, u32)> {
let run_mask = if unicode {
CASE_F_RUN_MASK
} else {
CASE_U_RUN_MASK
};
let mut ranges = Vec::new();
for entry in tables::CASE_CONV_TABLE1 {
let run_type = (entry >> 4) & 0xf;
if run_mask & (1_u16 << run_type) == 0 {
continue;
}
let mut start = entry >> 15;
let len = (entry >> 8) & 0x7f;
match run_type {
RUN_TYPE_UL => {
if !unicode {
start += 1;
}
for offset in (0..len).step_by(2) {
ranges.push((start + offset, start + offset + 1));
}
}
RUN_TYPE_LSU => {
if unicode {
ranges.push((start, start + 1));
}
ranges.push((start + 1, start + 2));
if !unicode {
ranges.push((start + 2, start + 3));
}
}
_ => ranges.push((start, start + len)),
}
}
normalize_half_open_ranges(ranges)
}
fn intersect_half_open_ranges(left: &[(u32, u32)], right: &[(u32, u32)]) -> Vec<(u32, u32)> {
let mut intersection = Vec::new();
let mut left_index = 0;
let mut right_index = 0;
while left_index < left.len() && right_index < right.len() {
let (left_start, left_end) = left[left_index];
let (right_start, right_end) = right[right_index];
let start = left_start.max(right_start);
let end = left_end.min(right_end);
if start < end {
intersection.push((start, end));
}
if left_end <= right_end {
left_index += 1;
} else {
right_index += 1;
}
}
intersection
}
fn subtract_half_open_ranges(ranges: &[(u32, u32)], excluded: &[(u32, u32)]) -> Vec<(u32, u32)> {
let mut result = Vec::new();
let mut excluded_index = 0;
for &(start, end) in ranges {
while excluded_index < excluded.len() && excluded[excluded_index].1 <= start {
excluded_index += 1;
}
let mut cursor = start;
let mut index = excluded_index;
while index < excluded.len() && excluded[index].0 < end {
let (excluded_start, excluded_end) = excluded[index];
if cursor < excluded_start {
result.push((cursor, excluded_start.min(end)));
}
cursor = cursor.max(excluded_end);
if cursor >= end {
break;
}
index += 1;
}
if cursor < end {
result.push((cursor, end));
}
}
result
}
fn normalize_half_open_ranges(mut ranges: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
ranges.retain(|(start, end)| start < end);
ranges.sort_unstable();
let mut normalized: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
if let Some((_, previous_end)) = normalized.last_mut()
&& start <= *previous_end
{
*previous_end = (*previous_end).max(end);
} else {
normalized.push((start, end));
}
}
normalized
}
fn is_cased(code_point: u32) -> bool {
let mut lower = 0;
let mut upper = tables::CASE_CONV_TABLE1.len();
while lower < upper {
let middle = (lower + upper) / 2;
let entry = tables::CASE_CONV_TABLE1[middle];
let run_start = entry >> 15;
let run_len = (entry >> 8) & 0x7f;
if code_point < run_start {
upper = middle;
} else if code_point >= run_start + run_len {
lower = middle + 1;
} else {
return true;
}
}
is_in_table(code_point, &tables::CASED_TABLE, &tables::CASED_INDEX)
}
fn is_case_ignorable(code_point: u32) -> bool {
is_in_table(
code_point,
&tables::CASE_IGNORABLE_TABLE,
&tables::CASE_IGNORABLE_INDEX,
)
}
fn next_code_point<U: CodeUnits + ?Sized>(input: &U, index: &mut usize) -> u32 {
let first = input
.unit_at(*index)
.expect("case conversion advanced past the input");
*index += 1;
if (0xd800..=0xdbff).contains(&first)
&& input
.unit_at(*index)
.is_some_and(|second| (0xdc00..=0xdfff).contains(&second))
{
let second = input
.unit_at(*index)
.expect("checked trail surrogate disappeared");
*index += 1;
0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00)
} else {
u32::from(first)
}
}
fn previous_code_point<U: CodeUnits + ?Sized>(input: &U, index: &mut usize) -> u32 {
if *index == 0 {
return 0;
}
*index -= 1;
let second = input
.unit_at(*index)
.expect("case conversion retreated past the input");
if (0xdc00..=0xdfff).contains(&second) && *index > 0 {
let first = input
.unit_at(*index - 1)
.expect("checked lead surrogate disappeared");
if (0xd800..=0xdbff).contains(&first) {
*index -= 1;
return 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00);
}
}
u32::from(second)
}
fn is_final_sigma<U: CodeUnits + ?Sized>(input: &U, sigma_position: usize) -> bool {
let mut index = sigma_position;
let previous = loop {
let code_point = previous_code_point(input, &mut index);
if !is_case_ignorable(code_point) {
break code_point;
}
};
if !is_cased(previous) {
return false;
}
let mut index = sigma_position + 1;
loop {
if index >= input.unit_len() {
return true;
}
let code_point = next_code_point(input, &mut index);
if !is_case_ignorable(code_point) {
return !is_cased(code_point);
}
}
}
pub fn convert_case_with_limit<U: CodeUnits + ?Sized>(
input: &U,
upper: bool,
max_len: usize,
) -> Result<CodeUnitBuf, StringError> {
if input.unit_len() == 0 {
return Ok(CodeUnitBuf::Latin1(Vec::new()));
}
let mut output = CaseStringBuffer::new(input.unit_len(), max_len)?;
let mut index = 0;
while index < input.unit_len() {
let position = index;
let code_point = next_code_point(input, &mut index);
let mapping = if !upper && code_point == 0x3a3 && is_final_sigma(input, position) {
CaseMapping::one(0x3c2)
} else {
case_conversion(code_point, !upper)
};
for mapped in &mapping.code_points[..mapping.len] {
output.push_code_point(*mapped)?;
}
}
Ok(output.finish())
}
#[cfg(test)]
mod tests {
use super::{
case_conversion, convert_case_with_limit, fail_case_reservation_after_for_test,
fail_next_case_reservation_for_test, is_case_ignorable, is_cased,
normalize_half_open_ranges, regexp_canonicalize, regexp_canonicalize_range_pairs,
regexp_case_change_mask, tables,
};
use crate::unicode::{CodeUnitBuf, MAX_STRING_LEN, StringError};
fn js(text: &str) -> Vec<u16> {
text.encode_utf16().collect()
}
fn convert(input: &[u16], upper: bool) -> CodeUnitBuf {
convert_case_with_limit(input, upper, MAX_STRING_LEN).unwrap()
}
fn text(output: &CodeUnitBuf) -> String {
String::from_utf16_lossy(&output.to_utf16())
}
#[test]
fn generated_tables_are_pinned_to_quickjs_unicode_17() {
assert_eq!(
tables::SOURCE_SHA256,
"cf782bc7a07549e976f606bd3cb8555858482b279574554dcb8d46412986006c"
);
assert_eq!(tables::CASE_CONV_TABLE1.len(), 378);
assert_eq!(tables::CASE_CONV_TABLE2.len(), 378);
assert_eq!(tables::CASE_CONV_EXT.len(), 58);
assert_eq!(tables::CASED_TABLE.len(), 190);
assert_eq!(tables::CASED_INDEX.len(), 18);
assert_eq!(tables::CASE_IGNORABLE_TABLE.len(), 785);
assert_eq!(tables::CASE_IGNORABLE_INDEX.len(), 75);
}
#[test]
fn regexp_canonicalization_matches_legacy_and_unicode_rules() {
assert_eq!(regexp_canonicalize(u32::from(b'a'), false), u32::from(b'A'));
assert_eq!(regexp_canonicalize(u32::from(b'A'), false), u32::from(b'A'));
assert_eq!(regexp_canonicalize(u32::from(b'A'), true), u32::from(b'a'));
assert_eq!(regexp_canonicalize(u32::from(b'a'), true), u32::from(b'a'));
assert_eq!(regexp_canonicalize(0x017f, false), 0x017f);
assert_eq!(regexp_canonicalize(0x212a, false), 0x212a);
assert_eq!(regexp_canonicalize(0x017f, true), u32::from(b's'));
assert_eq!(regexp_canonicalize(0x212a, true), u32::from(b'k'));
assert_eq!(regexp_canonicalize(0xfb06, true), 0xfb05);
assert_eq!(regexp_canonicalize(0x1fd3, true), 0x0390);
assert_eq!(regexp_canonicalize(0x1fe3, true), 0x03b0);
}
#[test]
fn regexp_case_change_masks_match_quickjs_unicode_17_shape() {
for (unicode, expected_ranges, expected_points) in
[(false, 634, 1_580_u32), (true, 637, 1_585_u32)]
{
let mask = regexp_case_change_mask(unicode);
assert_eq!(mask.len(), expected_ranges);
assert_eq!(
mask.iter().map(|(start, end)| end - start).sum::<u32>(),
expected_points,
);
}
}
#[test]
fn range_canonicalization_matches_pointwise_quickjs_mapping() {
fn naive(ranges: &[(u32, u32)], unicode: bool) -> Vec<(u32, u32)> {
let mapped = ranges
.iter()
.flat_map(|(start, end)| *start..=*end)
.map(|code_point| {
let mapped = regexp_canonicalize(code_point, unicode);
(mapped, mapped + 1)
})
.collect();
normalize_half_open_ranges(mapped)
.into_iter()
.map(|(start, end)| (start, end - 1))
.collect()
}
let probes = [
(0x0041, 0x007a),
(0x017e, 0x0181),
(0x0390, 0x03b0),
(0x1fd3, 0x1fe3),
(0xfb05, 0xfb06),
(0x10400, 0x1044f),
];
for unicode in [false, true] {
assert_eq!(
regexp_canonicalize_range_pairs(&probes, unicode),
naive(&probes, unicode),
);
let complete_mask = regexp_case_change_mask(unicode)
.into_iter()
.map(|(start, end)| (start, end - 1))
.collect::<Vec<_>>();
assert_eq!(
regexp_canonicalize_range_pairs(&complete_mask, unicode),
naive(&complete_mask, unicode),
);
}
assert_eq!(
regexp_canonicalize_range_pairs(&[(0xfb06, 0xfb06)], true),
[(0xfb05, 0xfb05)],
);
assert_eq!(
regexp_canonicalize_range_pairs(&[(0x1fd3, 0x1fd3)], true),
[(0x0390, 0x0390)],
);
assert_eq!(
regexp_canonicalize_range_pairs(&[(0x1fe3, 0x1fe3)], true),
[(0x03b0, 0x03b0)],
);
}
#[test]
fn full_unicode_range_canonicalization_stays_compact() {
let ranges = regexp_canonicalize_range_pairs(&[(0, 0x10_ffff)], true);
assert!(ranges.len() <= 700);
for code_point in [
0,
u32::from(b'A'),
u32::from(b'a'),
0x017f,
0x212a,
0x1fd3,
0xfb06,
0x10_ffff,
] {
let mapped = regexp_canonicalize(code_point, true);
assert!(
ranges
.iter()
.any(|(start, end)| mapped >= *start && mapped <= *end),
"missing canonical representative {mapped:#x}",
);
}
}
#[test]
fn conversion_ports_ascii_simple_extended_and_multicodepoint_runs() {
let input = js("aZ ß ffi ʼn İ 𐐀𐐨");
assert_eq!(text(&convert(&input, true)), "AZ SS FFI ʼN İ 𐐀𐐀");
assert_eq!(text(&convert(&input, false)), "az ß ffi ʼn i\u{307} 𐐨𐐨");
let greek = js("ΐ");
assert_eq!(text(&convert(&greek, true)), "Ι\u{308}\u{301}");
}
#[test]
fn lowercase_uses_quickjs_final_sigma_context() {
for (source, expected) in [
("ΟΣ", "ος"),
("ΟΣΑ", "οσα"),
("Σ", "σ"),
("A\u{301}Σ", "a\u{301}ς"),
("AΣ\u{301}", "aς\u{301}"),
("AΣ\u{301}B", "aσ\u{301}b"),
("AΣ'", "aς'"),
("AΣ'B", "aσ'b"),
] {
let source = js(source);
assert_eq!(text(&convert(&source, false)), expected);
}
}
#[test]
fn raw_utf16_surrogates_are_preserved_while_pairs_are_decoded() {
let input = [0xd800, 0x61, 0xdc00, 0xd801, 0xdc00, 0x5a, 0xdfff];
assert_eq!(
convert(&input, true).to_utf16(),
[0xd800, 0x41, 0xdc00, 0xd801, 0xdc00, 0x5a, 0xdfff]
);
assert_eq!(
convert(&input, false).to_utf16(),
[0xd800, 0x61, 0xdc00, 0xd801, 0xdc28, 0x7a, 0xdfff]
);
}
#[test]
fn buffer_starts_narrow_then_widens_or_stays_narrow_from_content() {
let narrows = js("Ÿ");
let narrowed = convert(&narrows, false);
assert_eq!(text(&narrowed), "ÿ");
assert!(matches!(narrowed, CodeUnitBuf::Latin1(_)));
let widens = [0xff_u8];
let widened = convert_case_with_limit(&widens[..], true, MAX_STRING_LEN).unwrap();
assert_eq!(text(&widened), "Ÿ");
assert!(matches!(widened, CodeUnitBuf::Utf16(_)));
}
#[test]
fn empty_identity_length_limit_and_reservation_failure_match_string_buffer() {
let empty: [u16; 0] = [];
fail_next_case_reservation_for_test();
let converted_empty = convert_case_with_limit(&empty[..], true, 0).unwrap();
assert_eq!(converted_empty, CodeUnitBuf::Latin1(Vec::new()));
let ascii = js("a");
assert_eq!(
convert_case_with_limit(&ascii[..], true, 1),
Err(StringError::OutOfMemory)
);
let sharp_s = js("ß");
assert_eq!(
convert_case_with_limit(&sharp_s[..], true, 1),
Err(StringError::TooLong)
);
assert_eq!(
text(&convert_case_with_limit(&sharp_s[..], false, 1).unwrap()),
"ß"
);
}
#[test]
fn widening_and_growth_reservation_failures_are_recoverable() {
let widens = js("ÿ");
fail_case_reservation_after_for_test(1);
assert_eq!(
convert_case_with_limit(&widens[..], true, MAX_STRING_LEN),
Err(StringError::OutOfMemory)
);
let expands = js("ß");
fail_case_reservation_after_for_test(1);
assert_eq!(
convert_case_with_limit(&expands[..], true, MAX_STRING_LEN),
Err(StringError::OutOfMemory)
);
assert_eq!(
text(&convert_case_with_limit(&expands[..], true, MAX_STRING_LEN).unwrap()),
"SS"
);
}
#[test]
fn cased_and_case_ignorable_property_boundaries_are_stable() {
for code_point in [0x41, 0x2b0, 0x345, 0x1f88, 0x10400, 0x1d7c4] {
assert!(is_cased(code_point), "U+{code_point:04X}");
}
for code_point in [0, 0x20, 0x300, 0x1f600, 0x10ffff] {
assert!(!is_cased(code_point), "U+{code_point:04X}");
}
for code_point in [0x27, 0x2e, 0x300, 0x5bd, 0x200d, 0xe0100] {
assert!(is_case_ignorable(code_point), "U+{code_point:04X}");
}
for code_point in [0, 0x41, 0x5be, 0x1f600, 0x10ffff] {
assert!(!is_case_ignorable(code_point), "U+{code_point:04X}");
}
}
#[test]
fn table_mapping_has_expected_unicode_17_samples() {
let lower = case_conversion(0x0130, true);
assert_eq!(&lower.code_points[..lower.len], [0x69, 0x307]);
let upper = case_conversion(0xfb03, false);
assert_eq!(&upper.code_points[..upper.len], [0x46, 0x46, 0x49]);
assert_eq!(case_conversion(0x10d70, false).code_points[0], 0x10d50);
assert_eq!(case_conversion(0x10d50, true).code_points[0], 0x10d70);
}
#[test]
fn exhaustive_kernel_fingerprint_matches_pinned_quickjs_c() {
use crate::unicode::tests::hash_u32;
let mut case_hash = 14_695_981_039_346_656_037_u64;
let mut property_hash = 14_695_981_039_346_656_037_u64;
let mut cased_count = 0_u32;
let mut ignorable_count = 0_u32;
for code_point in 0..=0x10_ffff {
let cased = is_cased(code_point);
let ignorable = is_case_ignorable(code_point);
cased_count += u32::from(cased);
ignorable_count += u32::from(ignorable);
property_hash = hash_u32(property_hash, code_point);
property_hash = hash_u32(property_hash, u32::from(cased));
property_hash = hash_u32(property_hash, u32::from(ignorable));
for (conversion, to_lower) in [(0, false), (1, true)] {
let mapping = case_conversion(code_point, to_lower);
case_hash = hash_u32(case_hash, code_point);
case_hash = hash_u32(case_hash, conversion);
case_hash = hash_u32(case_hash, mapping.len as u32);
for mapped in &mapping.code_points[..mapping.len] {
case_hash = hash_u32(case_hash, *mapped);
}
}
}
assert_eq!(cased_count, 4_632);
assert_eq!(ignorable_count, 2_794);
assert_eq!(property_hash, 0x0f20_329d_dcab_a159);
assert_eq!(case_hash, 0x24ab_cb80_d55c_587e);
}
}