1use super::{CodeUnitBuf, CodeUnits, MAX_STRING_LEN, StringError, index_position};
10#[cfg(any(test, feature = "test-hooks"))]
11use std::cell::Cell;
12use std::cmp::Ordering;
13
14mod tables {
15 include!("generated/unicode/unicode_normalize_tables.rs");
16}
17
18const DECOMPOSITION_MAX_LEN: usize = 18;
19
20const DECOMP_TYPE_C1: u32 = 0;
21const DECOMP_TYPE_L1: u32 = 1;
22const DECOMP_TYPE_L7: u32 = 7;
23const DECOMP_TYPE_LL1: u32 = 8;
24const DECOMP_TYPE_LL2: u32 = 9;
25const DECOMP_TYPE_S1: u32 = 10;
26const DECOMP_TYPE_S5: u32 = 14;
27const DECOMP_TYPE_I1: u32 = 15;
28const DECOMP_TYPE_I2_0: u32 = 16;
29const DECOMP_TYPE_I4_2: u32 = 21;
30const DECOMP_TYPE_B1: u32 = 22;
31const DECOMP_TYPE_B8: u32 = 29;
32const DECOMP_TYPE_B18: u32 = 30;
33const DECOMP_TYPE_LS2: u32 = 31;
34const DECOMP_TYPE_PAT3: u32 = 32;
35const DECOMP_TYPE_S2_UL: u32 = 33;
36const DECOMP_TYPE_LS2_UL: u32 = 34;
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum NormalizationForm {
41 Nfc,
42 Nfd,
43 Nfkc,
44 Nfkd,
45}
46
47impl NormalizationForm {
48 const fn is_compatibility(self) -> bool {
49 matches!(self, Self::Nfkc | Self::Nfkd)
50 }
51
52 const fn is_decomposed(self) -> bool {
53 matches!(self, Self::Nfd | Self::Nfkd)
54 }
55}
56
57#[cfg(any(test, feature = "test-hooks"))]
58thread_local! {
59 static FAIL_NEXT_NORMALIZE_RESERVATION: Cell<bool> = const { Cell::new(false) };
60}
61
62#[cfg(any(test, feature = "test-hooks"))]
66pub fn fail_next_normalize_reservation_for_test() {
67 FAIL_NEXT_NORMALIZE_RESERVATION.with(|armed| {
68 assert!(
69 !armed.replace(true),
70 "normalization reservation failure was already armed"
71 );
72 });
73}
74
75fn try_reserve_exact<T>(values: &mut Vec<T>, additional: usize) -> Result<(), StringError> {
76 #[cfg(any(test, feature = "test-hooks"))]
77 if FAIL_NEXT_NORMALIZE_RESERVATION.with(|armed| armed.replace(false)) {
78 return Err(StringError::OutOfMemory);
79 }
80 values
81 .try_reserve_exact(additional)
82 .map_err(|_| StringError::OutOfMemory)
83}
84
85fn push_fallible<T>(values: &mut Vec<T>, value: T) -> Result<(), StringError> {
86 if values.len() == values.capacity() {
87 let additional = (values.capacity() / 2).max(1);
92 try_reserve_exact(values, additional)?;
93 }
94 values.push(value);
95 Ok(())
96}
97
98fn get_u16(data: &[u8], position: usize) -> u32 {
99 u32::from(data[position]) | (u32::from(data[position + 1]) << 8)
100}
101
102fn short_code(code: u32) -> u32 {
103 const SHORT_TABLE: [u32; 2] = [0x2044, 0x2215];
104 if code < 0x80 {
105 code
106 } else if code < 0xd0 {
107 code - 0x80 + 0x300
108 } else {
109 SHORT_TABLE[(code - 0xd0) as usize]
110 }
111}
112
113fn lower_simple(mut code_point: u32) -> u32 {
114 if code_point < 0x100 || (0x410..=0x42f).contains(&code_point) {
115 code_point += 0x20;
116 } else {
117 code_point += 1;
118 }
119 code_point
120}
121
122fn decomposition_entry(
123 output: &mut [u32; DECOMPOSITION_MAX_LEN],
124 code_point: u32,
125 table_index: usize,
126 run_start: u32,
127 run_len: u32,
128 run_type: u32,
129) -> usize {
130 if run_type == DECOMP_TYPE_C1 {
131 output[0] = u32::from(tables::UNICODE_DECOMP_TABLE2[table_index]);
132 return 1;
133 }
134
135 let data_offset = usize::from(tables::UNICODE_DECOMP_TABLE2[table_index]);
136 let data = &tables::UNICODE_DECOMP_DATA[data_offset..];
137 let delta = (code_point - run_start) as usize;
138
139 match run_type {
140 DECOMP_TYPE_L1..=DECOMP_TYPE_L7 => {
141 let len = (run_type - DECOMP_TYPE_L1 + 1) as usize;
142 let data = &data[delta * len * 2..];
143 for (index, slot) in output[..len].iter_mut().enumerate() {
144 *slot = get_u16(data, index * 2);
145 if *slot == 0 {
146 return 0;
147 }
148 }
149 len
150 }
151 DECOMP_TYPE_LL1..=DECOMP_TYPE_LL2 => {
152 let len = (run_type - DECOMP_TYPE_LL1 + 1) as usize;
153 let high_bits_offset = run_len as usize * len * 2;
154 for (item_index, slot) in (delta * len..).zip(output[..len].iter_mut()) {
155 let high_bits =
156 (data[high_bits_offset + item_index / 4] >> ((item_index % 4) * 2)) & 3;
157 *slot = get_u16(data, item_index * 2) | (u32::from(high_bits) << 16);
158 if *slot == 0 {
159 return 0;
160 }
161 }
162 len
163 }
164 DECOMP_TYPE_S1..=DECOMP_TYPE_S5 => {
165 let len = (run_type - DECOMP_TYPE_S1 + 1) as usize;
166 let data = &data[delta * len..];
167 for (index, slot) in output[..len].iter_mut().enumerate() {
168 *slot = short_code(u32::from(data[index]));
169 if *slot == 0 {
170 return 0;
171 }
172 }
173 len
174 }
175 DECOMP_TYPE_I1 => {
176 output[0] = get_u16(data, 0) + code_point - run_start;
177 1
178 }
179 DECOMP_TYPE_I2_0..=DECOMP_TYPE_I4_2 => {
180 let len = (2 + ((run_type - DECOMP_TYPE_I2_0) >> 1)) as usize;
181 let incremented = ((run_type - DECOMP_TYPE_I2_0) & 1) as usize + usize::from(len > 2);
182 for (index, slot) in output[..len].iter_mut().enumerate() {
183 *slot = get_u16(data, index * 2);
184 if index == incremented {
185 *slot += code_point - run_start;
186 }
187 }
188 len
189 }
190 DECOMP_TYPE_B1..=DECOMP_TYPE_B8 | DECOMP_TYPE_B18 => {
191 let len = if run_type == DECOMP_TYPE_B18 {
192 18
193 } else {
194 (run_type - DECOMP_TYPE_B1 + 1) as usize
195 };
196 let minimum = get_u16(data, 0);
197 let data = &data[2 + delta * len..];
198 for (index, slot) in output[..len].iter_mut().enumerate() {
199 *slot = if data[index] == 0xff {
200 0x20
201 } else {
202 minimum + u32::from(data[index])
203 };
204 }
205 len
206 }
207 DECOMP_TYPE_LS2 => {
208 let data = &data[delta * 3..];
209 output[0] = get_u16(data, 0);
210 if output[0] == 0 {
211 return 0;
212 }
213 output[1] = short_code(u32::from(data[2]));
214 2
215 }
216 DECOMP_TYPE_PAT3 => {
217 output[0] = get_u16(data, 0);
218 output[2] = get_u16(data, 2);
219 output[1] = get_u16(data, 4 + delta * 2);
220 3
221 }
222 DECOMP_TYPE_S2_UL | DECOMP_TYPE_LS2_UL => {
223 let mut data = data;
224 let mut first;
225 if run_type == DECOMP_TYPE_S2_UL {
226 data = &data[delta & !1..];
227 first = short_code(u32::from(data[0]));
228 data = &data[1..];
229 } else {
230 data = &data[(delta >> 1) * 3..];
231 first = get_u16(data, 0);
232 data = &data[2..];
233 }
234 if delta & 1 != 0 {
235 first = lower_simple(first);
236 }
237 output[0] = first;
238 output[1] = short_code(u32::from(data[0]));
239 2
240 }
241 _ => 0,
242 }
243}
244
245fn decomposition(
246 output: &mut [u32; DECOMPOSITION_MAX_LEN],
247 code_point: u32,
248 compatibility: bool,
249) -> usize {
250 let mut lower = 0_usize;
251 let mut upper = tables::UNICODE_DECOMP_TABLE1.len();
252 while lower < upper {
253 let index = (lower + upper) / 2;
254 let entry = tables::UNICODE_DECOMP_TABLE1[index];
255 let run_start = entry >> 14;
256 let run_len = (entry >> 7) & 0x7f;
257 if code_point < run_start {
258 upper = index;
259 } else if code_point >= run_start + run_len {
260 lower = index + 1;
261 } else {
262 let is_compatibility = entry & 1 != 0;
263 if is_compatibility && !compatibility {
264 return 0;
265 }
266 let run_type = (entry >> 1) & 0x3f;
267 return decomposition_entry(output, code_point, index, run_start, run_len, run_type);
268 }
269 }
270 0
271}
272
273fn append_decomposed(
274 output: &mut Vec<u32>,
275 code_point: u32,
276 compatibility: bool,
277) -> Result<(), StringError> {
278 if (0xac00..0xd7a4).contains(&code_point) {
279 let syllable = code_point - 0xac00;
280 push_fallible(output, 0x1100 + syllable / 588)?;
281 push_fallible(output, 0x1161 + (syllable % 588) / 28)?;
282 let trailing = syllable % 28;
283 if trailing != 0 {
284 push_fallible(output, 0x11a7 + trailing)?;
285 }
286 return Ok(());
287 }
288
289 let mut mapped = [0_u32; DECOMPOSITION_MAX_LEN];
290 let mapped_len = decomposition(&mut mapped, code_point, compatibility);
291 if mapped_len == 0 {
292 push_fallible(output, code_point)
293 } else {
294 for mapped_code_point in &mapped[..mapped_len] {
295 append_decomposed(output, *mapped_code_point, compatibility)?;
296 }
297 Ok(())
298 }
299}
300
301fn combining_class(code_point: u32) -> u32 {
302 let Some((mut code, mut position)) = index_position(code_point, &tables::UNICODE_CC_INDEX)
303 else {
304 return 0;
305 };
306
307 loop {
308 let byte = tables::UNICODE_CC_TABLE[position];
309 position += 1;
310 let class_type = byte >> 6;
311 let mut run_len = u32::from(byte & 0x3f);
312 if (48..56).contains(&run_len) {
313 run_len = ((run_len - 48) << 8) | u32::from(tables::UNICODE_CC_TABLE[position]);
314 position += 1;
315 run_len += 48;
316 } else if run_len >= 56 {
317 run_len = ((run_len - 56) << 16)
318 | (u32::from(tables::UNICODE_CC_TABLE[position]) << 8)
319 | u32::from(tables::UNICODE_CC_TABLE[position + 1]);
320 position += 2;
321 run_len += 48 + (1 << 11);
322 }
323 let explicit_class = if class_type <= 1 {
324 let class = tables::UNICODE_CC_TABLE[position];
325 position += 1;
326 Some(class)
327 } else {
328 None
329 };
330 let run_end = code + run_len + 1;
331 if code_point < run_end {
332 return match class_type {
333 0 => u32::from(explicit_class.expect("class-zero run lost its payload")),
334 1 => {
335 u32::from(explicit_class.expect("linear class run lost its payload"))
336 + code_point
337 - code
338 }
339 2 => 0,
340 _ => 230,
341 };
342 }
343 code = run_end;
344 }
345}
346
347fn canonical_order(code_points: &mut [u32]) {
348 let mut index = 0;
349 while index < code_points.len() {
350 if combining_class(code_points[index]) == 0 {
351 index += 1;
352 continue;
353 }
354
355 let start = index;
356 let mut end = start + 1;
357 while end < code_points.len() && combining_class(code_points[end]) != 0 {
358 let code_point = code_points[end];
359 let class = combining_class(code_point);
360 let mut insertion = end;
361 while insertion > start && combining_class(code_points[insertion - 1]) > class {
362 code_points[insertion] = code_points[insertion - 1];
363 insertion -= 1;
364 }
365 code_points[insertion] = code_point;
366 end += 1;
367 }
368 index = end;
369 }
370}
371
372fn unicode_compose_pair(first: u32, second: u32) -> Option<u32> {
373 let mut lower = 0_usize;
374 let mut upper = tables::UNICODE_COMP_TABLE.len();
375 while lower < upper {
376 let index = (lower + upper) / 2;
377 let decomposition_index = tables::UNICODE_COMP_TABLE[index];
378 let table_index = usize::from(decomposition_index >> 6);
379 let run_offset = u32::from(decomposition_index & 0x3f);
380 let entry = tables::UNICODE_DECOMP_TABLE1[table_index];
381 let run_start = entry >> 14;
382 let run_len = (entry >> 7) & 0x7f;
383 let run_type = (entry >> 1) & 0x3f;
384 let composed = run_start + run_offset;
385 let mut pair = [0_u32; DECOMPOSITION_MAX_LEN];
386 let pair_len = decomposition_entry(
387 &mut pair,
388 composed,
389 table_index,
390 run_start,
391 run_len,
392 run_type,
393 );
394 debug_assert_eq!(pair_len, 2);
395 match (first, second).cmp(&(pair[0], pair[1])) {
396 Ordering::Less => upper = index,
397 Ordering::Greater => lower = index + 1,
398 Ordering::Equal => return Some(composed),
399 }
400 }
401 None
402}
403
404fn compose_pair(first: u32, second: u32) -> Option<u32> {
405 if (0x1100..0x1100 + 19).contains(&first) && (0x1161..0x1161 + 21).contains(&second) {
406 Some(0xac00 + (first - 0x1100) * 588 + (second - 0x1161) * 28)
407 } else if (0xac00..0xac00 + 11_172).contains(&first)
408 && (first - 0xac00).is_multiple_of(28)
409 && (0x11a7..0x11a7 + 28).contains(&second)
410 {
411 Some(first + second - 0x11a7)
412 } else {
413 unicode_compose_pair(first, second)
414 }
415}
416
417fn compose(code_points: &mut Vec<u32>) {
418 if code_points.len() <= 1 {
419 return;
420 }
421
422 let input_len = code_points.len();
423 let mut input = 1_usize;
424 let mut output_len = 1_usize;
425 while input < input_len {
426 let mut last_class = combining_class(code_points[input]);
427 let mut starter = output_len.cast_signed() - 1;
428 let mut blocked = false;
429 while starter >= 0 {
430 let class = combining_class(code_points[starter.cast_unsigned()]);
431 if class == 0 {
432 break;
433 }
434 if class >= last_class {
435 blocked = true;
436 break;
437 }
438 last_class = 256;
439 starter -= 1;
440 }
441
442 if !blocked && starter >= 0 {
443 let starter = starter.cast_unsigned();
444 if let Some(composed) = compose_pair(code_points[starter], code_points[input]) {
445 code_points[starter] = composed;
446 input += 1;
447 continue;
448 }
449 }
450
451 code_points[output_len] = code_points[input];
452 output_len += 1;
453 input += 1;
454 }
455 code_points.truncate(output_len);
456}
457
458fn next_code_point<U: CodeUnits + ?Sized>(input: &U, index: &mut usize) -> u32 {
459 let first = input
460 .unit_at(*index)
461 .expect("normalization advanced past its input");
462 *index += 1;
463 if (0xd800..=0xdbff).contains(&first)
464 && input
465 .unit_at(*index)
466 .is_some_and(|second| (0xdc00..=0xdfff).contains(&second))
467 {
468 let second = input
469 .unit_at(*index)
470 .expect("checked trail surrogate disappeared");
471 *index += 1;
472 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00)
473 } else {
474 u32::from(first)
475 }
476}
477
478pub fn normalize_code_points<U: CodeUnits + ?Sized>(
481 input: &U,
482 form: NormalizationForm,
483) -> Result<Vec<u32>, StringError> {
484 let mut output = Vec::new();
485 try_reserve_exact(&mut output, input.unit_len())?;
486
487 if form == NormalizationForm::Nfc && input.all_latin1() {
488 output.extend((0..input.unit_len()).map(|index| {
489 u32::from(
490 input
491 .unit_at(index)
492 .expect("Latin-1 scan stayed inside the input"),
493 )
494 }));
495 return Ok(output);
496 }
497
498 let mut index = 0;
499 while index < input.unit_len() {
500 let code_point = next_code_point(input, &mut index);
501 append_decomposed(&mut output, code_point, form.is_compatibility())?;
502 }
503 canonical_order(&mut output);
504 if !form.is_decomposed() {
505 compose(&mut output);
506 }
507 Ok(output)
508}
509
510fn string_from_code_points_with_limit(
511 code_points: &[u32],
512 max_len: usize,
513) -> Result<CodeUnitBuf, StringError> {
514 let max_len = max_len.min(MAX_STRING_LEN);
515 let mut utf16_len = 0_usize;
516 let mut wide = false;
517 for code_point in code_points {
518 debug_assert!(*code_point <= 0x10_ffff);
519 utf16_len = utf16_len
520 .checked_add(usize::from(*code_point >= 0x1_0000) + 1)
521 .filter(|length| *length <= max_len)
522 .ok_or(StringError::TooLong)?;
523 wide |= *code_point >= 0x100;
524 }
525
526 if !wide {
527 let mut output = Vec::new();
528 try_reserve_exact(&mut output, utf16_len)?;
529 output.extend(code_points.iter().map(|code_point| *code_point as u8));
530 return Ok(CodeUnitBuf::Latin1(output));
531 }
532
533 let mut output = Vec::new();
534 try_reserve_exact(&mut output, utf16_len)?;
535 for code_point in code_points {
536 if *code_point < 0x1_0000 {
537 output.push(*code_point as u16);
538 } else {
539 let adjusted = *code_point - 0x1_0000;
540 output.push(0xd800 | (adjusted >> 10) as u16);
541 output.push(0xdc00 | (adjusted & 0x3ff) as u16);
542 }
543 }
544 Ok(CodeUnitBuf::Utf16(output))
545}
546
547pub fn normalize_with_limit<U: CodeUnits + ?Sized>(
550 input: &U,
551 form: NormalizationForm,
552 max_len: usize,
553) -> Result<CodeUnitBuf, StringError> {
554 if max_len >= MAX_STRING_LEN {
555 return normalize(input, form);
556 }
557 let output = normalize_code_points(input, form)?;
558 string_from_code_points_with_limit(&output, max_len)
559}
560
561pub fn normalize<U: CodeUnits + ?Sized>(
565 input: &U,
566 form: NormalizationForm,
567) -> Result<CodeUnitBuf, StringError> {
568 let output = normalize_code_points(input, form)?;
569 string_from_code_points_with_limit(&output, MAX_STRING_LEN)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::{
575 DECOMPOSITION_MAX_LEN, NormalizationForm, combining_class, compose_pair, decomposition,
576 decomposition_entry, fail_next_normalize_reservation_for_test, normalize,
577 normalize_code_points, normalize_with_limit, push_fallible, tables,
578 };
579 use crate::unicode::{CodeUnitBuf, StringError};
580
581 fn js(text: &str) -> Vec<u16> {
582 text.encode_utf16().collect()
583 }
584
585 fn text(output: &CodeUnitBuf) -> String {
586 String::from_utf16_lossy(&output.to_utf16())
587 }
588
589 fn normalized(source: &str, form: NormalizationForm) -> String {
590 text(&normalize(&js(source)[..], form).unwrap())
591 }
592
593 #[test]
594 fn generated_tables_are_pinned_to_quickjs_unicode_17() {
595 assert_eq!(
596 tables::SOURCE_SHA256,
597 "cf782bc7a07549e976f606bd3cb8555858482b279574554dcb8d46412986006c"
598 );
599 assert_eq!(tables::UNICODE_CC_TABLE.len(), 937);
600 assert_eq!(tables::UNICODE_CC_INDEX.len(), 90);
601 assert_eq!(tables::UNICODE_DECOMP_TABLE1.len(), 709);
602 assert_eq!(tables::UNICODE_DECOMP_TABLE2.len(), 709);
603 assert_eq!(tables::UNICODE_DECOMP_DATA.len(), 9_452);
604 assert_eq!(tables::UNICODE_COMP_TABLE.len(), 965);
605 }
606
607 #[test]
608 fn canonical_and_compatibility_forms_match_quickjs_samples() {
609 assert_eq!(normalized("e\u{301}", NormalizationForm::Nfc), "é");
610 assert_eq!(normalized("é", NormalizationForm::Nfd), "e\u{301}");
611 assert_eq!(normalized("\u{fb01}", NormalizationForm::Nfc), "fi");
612 assert_eq!(normalized("\u{fb01}", NormalizationForm::Nfkc), "fi");
613 assert_eq!(normalized("\u{a0}", NormalizationForm::Nfkd), " ");
614 }
615
616 #[test]
617 fn combining_marks_are_stably_ordered_and_block_composition() {
618 assert_eq!(
619 normalized("a\u{315}\u{300}", NormalizationForm::Nfc),
620 "à\u{315}"
621 );
622 assert_eq!(
623 normalized("a\u{301}\u{300}", NormalizationForm::Nfc),
624 "á\u{300}"
625 );
626 assert_eq!(
627 normalized("\u{344}", NormalizationForm::Nfc),
628 "\u{308}\u{301}"
629 );
630 assert_eq!(combining_class(0x300), 230);
631 assert_eq!(combining_class(0x315), 232);
632 assert_eq!(combining_class(0x34f), 0);
633 }
634
635 #[test]
636 fn hangul_decomposition_and_composition_follow_quickjs_boundaries() {
637 assert_eq!(normalized("각", NormalizationForm::Nfd), "각");
638 assert_eq!(normalized("각", NormalizationForm::Nfc), "각");
639 assert_eq!(compose_pair(0x1100, 0x1161), Some(0xac00));
640 assert_eq!(compose_pair(0xac00, 0x11a8), Some(0xac01));
641 assert_eq!(compose_pair(0xac00, 0x11a7), Some(0xac00));
642 }
643
644 #[test]
645 fn valid_pairs_are_decoded_while_lone_surrogates_are_preserved() {
646 let input = [0xd800_u16, 0x65, 0x301, 0xdc00, 0xd83d, 0xde00, 0xdfff];
647 let output = normalize(&input[..], NormalizationForm::Nfc).unwrap();
648 assert_eq!(
649 output.to_utf16(),
650 [0xd800, 0xe9, 0xdc00, 0xd83d, 0xde00, 0xdfff]
651 );
652 assert_eq!(
653 normalize_code_points(&input[..], NormalizationForm::Nfc).unwrap(),
654 [0xd800, 0xe9, 0xdc00, 0x1f600, 0xdfff]
655 );
656 }
657
658 #[test]
659 fn output_uses_quickjs_narrow_or_wide_storage_from_content() {
660 let narrow = normalize(&js("plain latin-1 ÿ")[..], NormalizationForm::Nfc).unwrap();
661 assert!(matches!(narrow, CodeUnitBuf::Latin1(_)));
662
663 let narrowed = normalize(&js("\u{a0}")[..], NormalizationForm::Nfkc).unwrap();
664 assert_eq!(text(&narrowed), " ");
665 assert!(matches!(narrowed, CodeUnitBuf::Latin1(_)));
666
667 let composed_narrow = normalize(&js("e\u{301}")[..], NormalizationForm::Nfc).unwrap();
668 assert!(matches!(composed_narrow, CodeUnitBuf::Latin1(_)));
669
670 let wide = normalize(&js("a\u{304}")[..], NormalizationForm::Nfc).unwrap();
671 assert!(matches!(wide, CodeUnitBuf::Utf16(_)));
672 }
673
674 #[test]
675 fn normalization_reports_the_utf16_output_length_limit() {
676 let expands = js("\u{fd}ffi");
677 assert_eq!(
678 normalize_with_limit(&expands[..], NormalizationForm::Nfkd, 4),
679 Err(StringError::TooLong)
680 );
681 assert_eq!(
682 text(&normalize_with_limit(&expands[..], NormalizationForm::Nfkd, 5).unwrap()),
683 "y\u{301}ffi"
684 );
685
686 let astral = js("😀");
687 assert_eq!(
688 normalize_with_limit(&astral[..], NormalizationForm::Nfc, 1),
689 Err(StringError::TooLong)
690 );
691 }
692
693 #[test]
694 fn normalization_reservation_failure_is_recoverable() {
695 let input = js("e\u{301}");
696 fail_next_normalize_reservation_for_test();
697 assert_eq!(
698 normalize(&input[..], NormalizationForm::Nfc),
699 Err(StringError::OutOfMemory)
700 );
701 assert_eq!(
702 text(&normalize(&input[..], NormalizationForm::Nfc).unwrap()),
703 "é"
704 );
705 }
706
707 #[test]
708 fn fallible_output_growth_is_geometric_for_expanding_decompositions() {
709 let mut values = Vec::new();
710 let mut previous_capacity = values.capacity();
711 let mut growths = 0;
712 for value in 0..16_384 {
713 push_fallible(&mut values, value).unwrap();
714 if values.capacity() != previous_capacity {
715 assert!(
716 values.capacity() >= previous_capacity + (previous_capacity / 2).max(1),
717 "normalization output did not retain QuickJS-style geometric growth",
718 );
719 previous_capacity = values.capacity();
720 growths += 1;
721 }
722 }
723 assert!(growths < 32, "normalization output grew {growths} times");
724
725 let source = js(&"\u{fdFA}".repeat(1_024));
726 let output = normalize(&source[..], NormalizationForm::Nfkd).unwrap();
727 assert_eq!(output.to_utf16().len(), 18 * 1_024);
728 }
729
730 #[test]
731 fn pinned_decomposition_and_composition_samples_are_stable() {
732 let mut mapping = [0_u32; DECOMPOSITION_MAX_LEN];
733 let len = decomposition(&mut mapping, 0x1e69, false);
734 assert_eq!(&mapping[..len], [0x1e63, 0x307]);
735 let len = decomposition(&mut mapping, 0xfb03, false);
736 assert_eq!(len, 0);
737 let len = decomposition(&mut mapping, 0xfb03, true);
738 assert_eq!(&mapping[..len], [0x66, 0x66, 0x69]);
739 assert_eq!(compose_pair(0x1e63, 0x307), Some(0x1e69));
740 assert_eq!(compose_pair(0x308, 0x301), Some(0x344));
741 }
742
743 #[test]
744 fn exhaustive_table_readers_match_pinned_quickjs_c_fingerprints() {
745 use crate::unicode::tests::hash_u32;
746
747 let mut decomposition_hash = 14_695_981_039_346_656_037_u64;
748 let mut class_hash = 14_695_981_039_346_656_037_u64;
749 let mut canonical_count = 0_u32;
750 let mut compatibility_count = 0_u32;
751 let mut nonzero_class_count = 0_u32;
752 let mut mapping = [0_u32; DECOMPOSITION_MAX_LEN];
753 for code_point in 0..=0x10_ffff {
754 let class = combining_class(code_point);
755 nonzero_class_count += u32::from(class != 0);
756 class_hash = hash_u32(class_hash, code_point);
757 class_hash = hash_u32(class_hash, class);
758
759 for compatibility in [false, true] {
760 let len = decomposition(&mut mapping, code_point, compatibility);
761 if compatibility {
762 compatibility_count += u32::from(len != 0);
763 } else {
764 canonical_count += u32::from(len != 0);
765 }
766 decomposition_hash = hash_u32(decomposition_hash, code_point);
767 decomposition_hash = hash_u32(decomposition_hash, u32::from(compatibility));
768 decomposition_hash = hash_u32(decomposition_hash, len as u32);
769 for mapped in &mapping[..len] {
770 decomposition_hash = hash_u32(decomposition_hash, *mapped);
771 }
772 }
773 }
774
775 assert_eq!(canonical_count, 2_081);
776 assert_eq!(compatibility_count, 5_914);
777 assert_eq!(nonzero_class_count, 968);
778 assert_eq!(decomposition_hash, 6_126_396_769_325_200_388);
779 assert_eq!(class_hash, 2_580_281_225_329_042_492);
780
781 let mut composition_hash = 14_695_981_039_346_656_037_u64;
782 for (index, decomposition_index) in tables::UNICODE_COMP_TABLE.iter().enumerate() {
783 let table_index = usize::from(decomposition_index >> 6);
784 let run_offset = u32::from(decomposition_index & 0x3f);
785 let entry = tables::UNICODE_DECOMP_TABLE1[table_index];
786 let run_start = entry >> 14;
787 let run_len = (entry >> 7) & 0x7f;
788 let run_type = (entry >> 1) & 0x3f;
789 let composed = run_start + run_offset;
790 let pair_len = decomposition_entry(
791 &mut mapping,
792 composed,
793 table_index,
794 run_start,
795 run_len,
796 run_type,
797 );
798 let actual = compose_pair(mapping[0], mapping[1]).unwrap();
799 composition_hash = hash_u32(composition_hash, index as u32);
800 composition_hash = hash_u32(composition_hash, pair_len as u32);
801 composition_hash = hash_u32(composition_hash, mapping[0]);
802 composition_hash = hash_u32(composition_hash, mapping[1]);
803 composition_hash = hash_u32(composition_hash, actual);
804 }
805 assert_eq!(composition_hash, 17_411_631_189_690_117_515);
806 }
807}