icu_normalizer/lib.rs
1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5// https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations
6#![cfg_attr(not(any(test, doc)), no_std)]
7#![cfg_attr(
8 not(test),
9 deny(
10 clippy::indexing_slicing,
11 clippy::unwrap_used,
12 clippy::expect_used,
13 clippy::panic,
14 )
15)]
16#![warn(missing_docs)]
17
18//! Normalizing text into Unicode Normalization Forms.
19//!
20//! This module is published as its own crate ([`icu_normalizer`](https://docs.rs/icu_normalizer/latest/icu_normalizer/))
21//! and as part of the [`icu`](https://docs.rs/icu/latest/icu/) crate. See the latter for more details on the ICU4X project.
22//!
23//! # Functionality
24//!
25//! The top level of the crate provides normalization of input into the four normalization forms defined in [UAX #15: Unicode
26//! Normalization Forms](https://www.unicode.org/reports/tr15/): NFC, NFD, NFKC, and NFKD.
27//!
28//! Three kinds of contiguous inputs are supported: known-well-formed UTF-8 (`&str`), potentially-not-well-formed UTF-8,
29//! and potentially-not-well-formed UTF-16. Additionally, an iterator over `char` can be wrapped in a normalizing iterator.
30//!
31//! The `uts46` module provides the combination of mapping and normalization operations for [UTS #46: Unicode IDNA
32//! Compatibility Processing](https://www.unicode.org/reports/tr46/). This functionality is not meant to be used by
33//! applications directly. Instead, it is meant as a building block for a full implementation of UTS #46, such as the
34//! [`idna`](https://docs.rs/idna/latest/idna/) crate.
35//!
36//! The `properties` module provides the non-recursive canonical decomposition operation on a per `char` basis and
37//! the canonical compositon operation given two `char`s. It also provides access to the Canonical Combining Class
38//! property. These operations are primarily meant for [HarfBuzz](https://harfbuzz.github.io/), the types
39//! [`CanonicalComposition`](properties::CanonicalComposition), [`CanonicalDecomposition`](properties::CanonicalDecomposition),
40//! and [`CanonicalCombiningClassMap`](properties::CanonicalCombiningClassMap) implement the [`harfbuzz_traits`] if
41//! the `harfbuzz_traits` Cargo feature is enabled.
42//!
43//! Notably, this normalizer does _not_ provide the normalization “quick check” that can result in “maybe” in
44//! addition to “yes” and “no”. The normalization checks provided by this crate always give a definitive
45//! non-“maybe” answer.
46//!
47//! # Examples
48//!
49//! ```
50//! let nfc = icu_normalizer::ComposingNormalizerBorrowed::new_nfc();
51//! assert_eq!(nfc.normalize("a\u{0308}"), "ä");
52//! assert!(nfc.is_normalized("ä"));
53//!
54//! let nfd = icu_normalizer::DecomposingNormalizerBorrowed::new_nfd();
55//! assert_eq!(nfd.normalize("ä"), "a\u{0308}");
56//! assert!(!nfd.is_normalized("ä"));
57//! ```
58
59extern crate alloc;
60
61#[cfg(feature = "serde")]
62type Trie<'trie> = CodePointTrie<'trie, u32>;
63
64#[cfg(not(feature = "serde"))]
65type Trie<'trie> = FastCodePointTrie<'trie, u32>;
66
67type CombiningBuffer = SmallVec<[CharacterAndClass; 2]>;
68
69type CompositionTrie<'trie> = FastCodePointTrie<'trie, u16>;
70
71// We don't depend on icu_properties to minimize deps, but we want to be able
72// to ensure we're using the right CCC values
73macro_rules! ccc {
74 ($name:ident, $num:expr) => {
75 const {
76 #[cfg(feature = "icu_properties")]
77 if icu_properties::props::CanonicalCombiningClass::$name.to_icu4c_value() != $num {
78 panic!("icu_normalizer has incorrect ccc values")
79 }
80 CanonicalCombiningClass::from_icu4c_value($num)
81 }
82 };
83}
84
85#[cfg(feature = "harfbuzz_traits")]
86mod harfbuzz;
87#[cfg(feature = "latin1")]
88pub mod latin1;
89pub mod properties;
90pub mod provider;
91pub mod uts46;
92
93#[cfg(feature = "serde")]
94use crate::provider::CanonicalCompositions;
95use crate::provider::CanonicalCompositionsNew;
96use crate::provider::DecompositionData;
97use crate::provider::NormalizerNfdDataV1;
98use crate::provider::NormalizerNfkdDataV1;
99use crate::provider::NormalizerUts46DataV1;
100use alloc::borrow::Cow;
101use alloc::string::String;
102use core::char::REPLACEMENT_CHARACTER;
103use core::marker::PhantomData;
104#[cfg(feature = "serde")]
105use icu_collections::char16trie::Char16Trie;
106#[cfg(feature = "serde")]
107use icu_collections::char16trie::Char16TrieIterator;
108#[cfg(feature = "serde")]
109use icu_collections::char16trie::TrieResult;
110use icu_collections::codepointtrie::AbstractCodePointTrie;
111use icu_collections::codepointtrie::CharIterWithTrie;
112use icu_collections::codepointtrie::CharsWithTrieDefaultForAsciiEx;
113use icu_collections::codepointtrie::CodePointTrie;
114use icu_collections::codepointtrie::FastCodePointTrie;
115use icu_collections::codepointtrie::WithTrie;
116#[cfg(feature = "icu_properties")]
117use icu_properties::props::CanonicalCombiningClass;
118use icu_provider::prelude::*;
119use provider::DecompositionTables;
120#[cfg(feature = "serde")]
121use provider::NormalizerNfcV1;
122use provider::NormalizerNfcV2;
123use provider::NormalizerNfdTablesV1;
124use provider::NormalizerNfkdTablesV1;
125use smallvec::SmallVec;
126#[cfg(feature = "utf16_iter")]
127use utf16_iter::Utf16CharsWithTrieEx;
128#[cfg(feature = "utf8_iter")]
129use utf8_iter::Utf8CharsEx;
130#[cfg(feature = "utf8_iter")]
131use utf8_iter::Utf8CharsWithTrieDefaultForAsciiEx;
132use zerovec::{zeroslice, ZeroSlice};
133
134// The optimizations in the area where `likely` is used
135// are extremely brittle. `likely` is useful in the typed-trie
136// case on the UTF-16 fast path, but in order not to disturb
137// the untyped-trie case on the UTF-16 fast path, make the
138// annotations no-ops in the untyped-trie case.
139
140// `cold_path` and `likely` come from
141// https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3 .
142// See https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3#commitcomment-164768806
143// for permission to relicense under Unicode-3.0.
144
145#[cfg(not(feature = "serde"))]
146#[inline(always)]
147#[cold]
148fn cold_path() {}
149
150#[cfg(not(feature = "serde"))]
151#[inline(always)]
152pub(crate) fn likely(b: bool) -> bool {
153 if b {
154 true
155 } else {
156 cold_path();
157 false
158 }
159}
160
161#[cfg(not(feature = "serde"))]
162#[inline(always)]
163pub(crate) fn unlikely(b: bool) -> bool {
164 if b {
165 cold_path();
166 true
167 } else {
168 false
169 }
170}
171
172// End import from https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3 .
173
174/// No-op for typed trie case.
175#[cfg(feature = "serde")]
176#[inline(always)]
177fn likely(b: bool) -> bool {
178 b
179}
180
181/// No-op for typed trie case.
182#[cfg(feature = "serde")]
183#[inline(always)]
184fn unlikely(b: bool) -> bool {
185 b
186}
187
188/// This type exists as a shim for icu_properties CanonicalCombiningClass when the crate is disabled
189/// It should not be exposed to users.
190#[cfg(not(feature = "icu_properties"))]
191#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
192struct CanonicalCombiningClass(pub(crate) u8);
193
194#[cfg(not(feature = "icu_properties"))]
195impl CanonicalCombiningClass {
196 const fn from_icu4c_value(v: u8) -> Self {
197 Self(v)
198 }
199 const fn to_icu4c_value(self) -> u8 {
200 self.0
201 }
202}
203
204const CCC_NOT_REORDERED: CanonicalCombiningClass = ccc!(NotReordered, 0);
205const CCC_ABOVE: CanonicalCombiningClass = ccc!(Above, 230);
206
207/// Treatment of the ignorable marker (0xFFFFFFFF) in data.
208#[derive(Debug, PartialEq, Eq)]
209enum IgnorableBehavior {
210 /// 0xFFFFFFFF in data is not supported.
211 Unsupported,
212 /// Ignorables are ignored.
213 Ignored,
214 /// Ignorables are treated as singleton decompositions
215 /// to the REPLACEMENT CHARACTER.
216 ReplacementCharacter,
217}
218
219pub(crate) trait IteratorPolicy {
220 const IGNORABLE_BEHAVIOR: IgnorableBehavior;
221}
222
223#[derive(Debug)]
224struct Uax15Policy;
225
226impl IteratorPolicy for Uax15Policy {
227 const IGNORABLE_BEHAVIOR: IgnorableBehavior = IgnorableBehavior::Unsupported;
228}
229
230/// Marker for UTS 46 ignorables.
231///
232/// See trie-value-format.md
233const IGNORABLE_MARKER: u32 = 0xFFFFFFFF;
234
235/// Marker that the decomposition does not round trip via NFC.
236///
237/// See trie-value-format.md
238const NON_ROUND_TRIP_MARKER: u32 = 1 << 30;
239
240/// Marker that the first character of the decomposition
241/// can combine backwards.
242///
243/// See trie-value-format.md
244const BACKWARD_COMBINING_MARKER: u32 = 1 << 31;
245
246/// Mask for the bits have to be zero for this to be a BMP
247/// singleton decomposition, or value baked into the surrogate
248/// range.
249///
250/// See trie-value-format.md
251const HIGH_ZEROS_MASK: u32 = 0x3FFF0000;
252
253/// Mask for the bits have to be zero for this to be a complex
254/// decomposition.
255///
256/// See trie-value-format.md
257const LOW_ZEROS_MASK: u32 = 0xFFE0;
258
259/// Checks if a trie value carries a (non-zero) canonical
260/// combining class.
261///
262/// See trie-value-format.md
263fn trie_value_has_ccc(trie_value: u32) -> bool {
264 (trie_value & 0x3FFFFE00) == 0xD800
265}
266
267/// Checks if the trie signifies a special non-starter decomposition.
268///
269/// See trie-value-format.md
270fn trie_value_indicates_special_non_starter_decomposition(trie_value: u32) -> bool {
271 (trie_value & 0x3FFFFF00) == 0xD900
272}
273
274/// Checks if the trie signifies a non-decomposing non-starter.
275///
276/// See trie-value-format.md
277fn trie_value_indicates_non_decomposing_non_starter(trie_value: u32) -> bool {
278 (trie_value & 0x3FFFFF00) == 0xD800
279}
280
281/// Checks if a trie value signifies a character whose decomposition
282/// starts with a non-starter.
283///
284/// See trie-value-format.md
285fn decomposition_starts_with_non_starter(trie_value: u32) -> bool {
286 trie_value_has_ccc(trie_value)
287}
288
289/// Extracts a canonical combining class (possibly zero) from a trie value.
290///
291/// See trie-value-format.md
292fn ccc_from_trie_value(trie_value: u32) -> CanonicalCombiningClass {
293 if trie_value_has_ccc(trie_value) {
294 CanonicalCombiningClass::from_icu4c_value(trie_value as u8)
295 } else {
296 CCC_NOT_REORDERED
297 }
298}
299
300/// The tail (everything after the first character) of the NFKD form U+FDFA
301/// as 16-bit units.
302static FDFA_NFKD: [u16; 17] = [
303 0x644, 0x649, 0x20, 0x627, 0x644, 0x644, 0x647, 0x20, 0x639, 0x644, 0x64A, 0x647, 0x20, 0x648,
304 0x633, 0x644, 0x645,
305];
306
307/// Marker value for U+FDFA in NFKD. (Unified with Hangul syllable marker,
308/// but they differ by `NON_ROUND_TRIP_MARKER`.)
309///
310/// See trie-value-format.md
311const FDFA_MARKER: u16 = 1;
312
313// These constants originate from page 143 of Unicode 14.0
314/// Syllable base
315const HANGUL_S_BASE: u32 = 0xAC00;
316/// Lead jamo base
317const HANGUL_L_BASE: u32 = 0x1100;
318/// Vowel jamo base
319const HANGUL_V_BASE: u32 = 0x1161;
320/// Trail jamo base (deliberately off by one to account for the absence of a trail)
321const HANGUL_T_BASE: u32 = 0x11A7;
322/// Lead jamo count
323const HANGUL_L_COUNT: u32 = 19;
324/// Vowel jamo count
325const HANGUL_V_COUNT: u32 = 21;
326/// Trail jamo count (deliberately off by one to account for the absence of a trail)
327const HANGUL_T_COUNT: u32 = 28;
328/// Vowel jamo count times trail jamo count
329const HANGUL_N_COUNT: u32 = 588;
330/// Syllable count
331const HANGUL_S_COUNT: u32 = 11172;
332/// One past the conjoining jamo block
333#[cfg(feature = "serde")]
334const HANGUL_JAMO_LIMIT: u32 = 0x1200;
335/// Trie value base corresponding for L
336const HANGUL_L_TRIE_VAL_BASE: u16 = 0xD6A7;
337
338/// If `opt` is `Some`, unwrap it. If `None`, panic if debug assertions
339/// are enabled and return `default` if debug assertions are not enabled.
340///
341/// Use this only if the only reason why `opt` could be `None` is bogus
342/// data from the provider.
343#[inline(always)]
344fn unwrap_or_gigo<T>(opt: Option<T>, default: T) -> T {
345 if let Some(val) = opt {
346 val
347 } else {
348 // GIGO case
349 debug_assert!(false);
350 default
351 }
352}
353
354/// Convert a `u32` _obtained from data provider data_ to `char`.
355#[inline(always)]
356fn char_from_u32(u: u32) -> char {
357 unwrap_or_gigo(core::char::from_u32(u), REPLACEMENT_CHARACTER)
358}
359
360/// Convert a `u16` _obtained from data provider data_ to `char`.
361#[inline(always)]
362fn char_from_u16(u: u16) -> char {
363 char_from_u32(u32::from(u))
364}
365
366const EMPTY_U16: &ZeroSlice<u16> = zeroslice![];
367
368const EMPTY_CHAR: &ZeroSlice<char> = zeroslice![];
369
370#[inline(always)]
371fn in_inclusive_range(c: char, start: char, end: char) -> bool {
372 u32::from(c).wrapping_sub(u32::from(start)) <= (u32::from(end) - u32::from(start))
373}
374
375#[inline(always)]
376#[cfg(feature = "utf16_iter")]
377fn in_inclusive_range16(u: u16, start: u16, end: u16) -> bool {
378 u.wrapping_sub(start) <= (end - start)
379}
380
381#[derive(Debug)]
382pub(crate) enum CanonicalCompositionsPayload {
383 Current(DataPayload<NormalizerNfcV2>),
384 #[cfg(feature = "serde")]
385 Legacy(DataPayload<NormalizerNfcV1>),
386}
387
388impl<'data> CanonicalCompositionsPayload {
389 pub(crate) fn as_borrowed(&'data self) -> CanonicalCompositionsBorrowed<'data> {
390 match self {
391 CanonicalCompositionsPayload::Current(data_payload) => {
392 CanonicalCompositionsBorrowed::Current(data_payload.get())
393 }
394 #[cfg(feature = "serde")]
395 CanonicalCompositionsPayload::Legacy(data_payload) => {
396 CanonicalCompositionsBorrowed::Legacy(data_payload.get())
397 }
398 }
399 }
400}
401
402#[derive(Debug, Copy, Clone)]
403pub(crate) enum CanonicalCompositionsBorrowed<'data> {
404 Current(&'data CanonicalCompositionsNew<'data>),
405 #[cfg(feature = "serde")]
406 Legacy(&'data CanonicalCompositions<'data>),
407}
408
409impl CanonicalCompositionsBorrowed<'static> {
410 pub(crate) const fn static_to_owned(self) -> CanonicalCompositionsPayload {
411 match self {
412 CanonicalCompositionsBorrowed::Current(s) => {
413 CanonicalCompositionsPayload::Current(DataPayload::from_static_ref(s))
414 }
415 #[cfg(feature = "serde")]
416 CanonicalCompositionsBorrowed::Legacy(s) => {
417 CanonicalCompositionsPayload::Legacy(DataPayload::from_static_ref(s))
418 }
419 }
420 }
421}
422
423impl<'data> CanonicalCompositionsBorrowed<'data> {
424 pub(crate) fn as_ref(&'data self) -> CanonicalCompositionsRef<'data> {
425 match self {
426 CanonicalCompositionsBorrowed::Current(s) => CanonicalCompositionsRef::Current(
427 <&CompositionTrie<'data>>::try_from(&s.trie)
428 .unwrap_or_else(|_| unreachable!("Incompatible data")),
429 &s.linear16,
430 &s.linear24,
431 ),
432 #[cfg(feature = "serde")]
433 CanonicalCompositionsBorrowed::Legacy(s) => {
434 CanonicalCompositionsRef::Legacy(s.canonical_compositions.clone())
435 }
436 }
437 }
438}
439
440#[derive(Debug)]
441pub(crate) enum CanonicalCompositionsRef<'data> {
442 Current(
443 &'data CompositionTrie<'data>,
444 &'data ZeroSlice<(u16, u16)>,
445 &'data ZeroSlice<(char, char)>,
446 ),
447 #[cfg(feature = "serde")]
448 Legacy(Char16Trie<'data>),
449}
450
451impl<'data> CanonicalCompositionsRef<'data> {
452 /// Performs canonical composition (including Hangul) on a pair of
453 /// characters or returns `None` if these characters don't compose.
454 /// Composition exclusions are taken into account.
455 ///
456 /// TODO: Have the caller retain more state and have this function return
457 /// more information that is useful for retaining information between
458 /// attempts to compose in a sequence of such attempts:
459 ///
460 /// * We can return the linear search slice when we search through it but don't find anything.
461 /// * We can know that no further matches are possible.
462 /// * We can know that the starter was a special ASCII vowel.
463 /// * We can know that we just formed a Hangul LV syllable.
464 pub(crate) fn compose(&self, starter: char, second: char) -> Option<char> {
465 match self {
466 CanonicalCompositionsRef::Current(trie, linear16, linear24) => {
467 // According to Compiler Explorer, the `match` optimizes to a bitfield lookup.
468 // Don't bother optimizing manually without inspecting the generated assembly.
469 let (primary, secondary) = match starter {
470 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U' => {
471 // This special case balances out the max length of entries
472 // in `linear` so that no entry exceeds 10 items as of Unicode 17.
473 (second, starter)
474 }
475 _ => (starter, second),
476 };
477 let packed = trie.scalar(primary);
478 let len = usize::from(packed & 0b1111);
479 let index = usize::from(packed >> 4);
480 if let Some(slice16) = linear16.get_subslice(index..index + len) {
481 let secondary32 = u32::from(secondary);
482 for (candidate, composed) in slice16.iter() {
483 if u32::from(candidate) == secondary32 {
484 return Some(char_from_u16(composed));
485 }
486 }
487 return None;
488 }
489
490 if packed < 0b1000_0000_0000_0000 {
491 debug_assert_eq!(packed, 0b0111_1111_1111_1111);
492 return None;
493 }
494
495 // Mask off the bit that was the most-significant bit in `u16` before we
496 // shifted right by 4.
497 let index = index & 0b1_11111_11111; // 11 bits set
498 if let Some(slice24) = linear24.get_subslice(index..index + len) {
499 for (candidate, composed) in slice24.iter() {
500 if candidate == secondary {
501 return Some(composed);
502 }
503 }
504 return None;
505 }
506
507 // Handle Hangul L after non-BMP, because HarfBuzz isn't actually supposed
508 // to exercise this case and in the normalizer itself, we come here only
509 // in NFKC in the case of enclosed Hangul.
510 if packed >= HANGUL_L_TRIE_VAL_BASE {
511 // If the debug asserts fail, we have a GIGO case.
512 debug_assert!(u32::from(primary).wrapping_sub(HANGUL_L_BASE) < HANGUL_L_COUNT);
513 debug_assert_eq!(
514 u32::from(packed - HANGUL_L_TRIE_VAL_BASE),
515 u32::from(primary).wrapping_sub(HANGUL_L_BASE) * HANGUL_N_COUNT
516 );
517
518 let v = u32::from(second).wrapping_sub(HANGUL_V_BASE);
519 if v < HANGUL_V_COUNT {
520 // Acconding to Compiler Explorer, multiplication by `HANGUL_T_COUNT`
521 // optimizes to not actually using a multiplication instruction.
522 let lv = u32::from(packed - HANGUL_L_TRIE_VAL_BASE) + v * HANGUL_T_COUNT;
523 // SAFETY: Safe, because the inputs are known to be in range. Notably
524 // packed cannot have been above 0xFFFF, since it came from `u16`.
525 // That is, this must be in scalar value range. However, the result
526 // can still be GIGO if the trie value does not contain the right value
527 // within its possible range, in which case either of the above debug
528 // assertions should fail.
529 return Some(unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lv) });
530 }
531 return None;
532 }
533
534 // `starter` is Hangul LV unless GIGO. If the debug asserts fail, we have a GIGO case.
535 debug_assert!(u32::from(primary).wrapping_sub(HANGUL_S_BASE) < HANGUL_S_COUNT);
536 debug_assert_eq!(
537 u32::from(primary).wrapping_sub(HANGUL_S_BASE) % HANGUL_T_COUNT,
538 0
539 );
540 if in_inclusive_range(secondary, '\u{11A8}', '\u{11C2}') {
541 let lvt = u32::from(primary) + (u32::from(secondary) - HANGUL_T_BASE);
542 if lvt < 0xD800 {
543 // SAFETY: Immediately above we checked that `c32` is below the surrogate
544 // range. (Not using `char::from_u32` itself as a micro optimization.)
545 // This is only a check about the safe `char` range. The result could
546 // still be GIGO, wich which case either of the above debug assertions
547 // should fail.
548 return Some(unsafe { char::from_u32_unchecked(lvt) });
549 } else {
550 // GIGO
551 // Asserting `false`, although either of the above two debug assertions
552 // should already have caught this case.
553 debug_assert!(false);
554 }
555 }
556 None
557 }
558 #[cfg(feature = "serde")]
559 CanonicalCompositionsRef::Legacy(char16_trie) => {
560 Self::compose_legacy(char16_trie.iter(), starter, second)
561 }
562 }
563 }
564
565 /// Performs canonical composition (including Hangul) on a pair of
566 /// characters on the assumption that the second one is a starter
567 /// or returns `None` if these characters don't compose.
568 /// Composition exclusions are taken into account.
569 ///
570 /// The returned boolean can be true only if `char` a Hangul LV syllable.
571 ///
572 /// The argument `starter_is_lv` must be set either to false or to the value
573 /// that this method previously returned alongside `starter`.
574 pub(crate) fn compose_starter(
575 &self,
576 starter: char,
577 second: char,
578 starter_is_lv: bool,
579 ) -> Option<(char, bool)> {
580 if starter_is_lv {
581 debug_assert!(u32::from(starter).wrapping_sub(HANGUL_S_BASE) < HANGUL_S_COUNT);
582 debug_assert_eq!(
583 u32::from(starter).wrapping_sub(HANGUL_S_BASE) % HANGUL_T_COUNT,
584 0
585 );
586 if in_inclusive_range(second, '\u{11A8}', '\u{11C2}') {
587 // We take the perf hit of checking the returned character for range
588 // even though we could omit the check if we trusted 100% that the
589 // other code has no mistakes regarding the stated required semantics
590 // of `starter_is_lv`.
591 return Some((
592 char_from_u32(u32::from(starter) + (u32::from(second) - HANGUL_T_BASE)),
593 false,
594 ));
595 }
596 return None;
597 }
598 match self {
599 CanonicalCompositionsRef::Current(trie, linear16, linear24) => {
600 // We assume that future versions of Unicode won't introduce starters
601 // that would compose with ASCII vowels.
602 let primary = starter;
603 let secondary = second;
604 let packed = trie.scalar(primary);
605
606 if packed >= HANGUL_L_TRIE_VAL_BASE {
607 // If the debug asserts fail, we have a GIGO case.
608 debug_assert!(u32::from(primary).wrapping_sub(HANGUL_L_BASE) < HANGUL_L_COUNT);
609 debug_assert_eq!(
610 u32::from(packed - HANGUL_L_TRIE_VAL_BASE),
611 u32::from(primary).wrapping_sub(HANGUL_L_BASE) * HANGUL_N_COUNT
612 );
613
614 let v = u32::from(second).wrapping_sub(HANGUL_V_BASE);
615 if v < HANGUL_V_COUNT {
616 // Acconding to Compiler Explorer, multiplication by `HANGUL_T_COUNT`
617 // optimizes to not actually using a multiplication instruction.
618 let lv = u32::from(packed - HANGUL_L_TRIE_VAL_BASE) + v * HANGUL_T_COUNT;
619 // SAFETY: Safe, because the inputs are known to be in range. Notably
620 // packed cannot have been above 0xFFFF, since it came from `u16`.
621 // That is, this must be in scalar value range. However, the result
622 // can still be GIGO if the trie value does not contain the right value
623 // within its possible range, in which case either of the above debug
624 // assertions should fail.
625 return Some((
626 unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lv) },
627 true,
628 ));
629 }
630 return None;
631 }
632
633 // Putting the Hangul case above, because in NFD Hangul the above case
634 // happens with each syllable whereas with the languages for which the
635 // case immediately below is relevant, the case occurs only once in a
636 // while.
637
638 let len = usize::from(packed & 0b1111);
639 let index = usize::from(packed >> 4);
640 if let Some(slice16) = linear16.get_subslice(index..index + len) {
641 let secondary32 = u32::from(secondary);
642 for (candidate, composed) in slice16.iter() {
643 if u32::from(candidate) == secondary32 {
644 return Some((char_from_u16(composed), false));
645 }
646 }
647 return None;
648 }
649
650 if packed < 0b1000_0000_0000_0000 {
651 debug_assert_eq!(packed, 0b0111_1111_1111_1111);
652 return None;
653 }
654 // Mask off the bit that was the most-significant bit in `u16` before we
655 // shifted right by 4.
656 let index = index & 0b1_11111_11111; // 11 bits set
657 if let Some(slice24) = linear24.get_subslice(index..index + len) {
658 for (candidate, composed) in slice24.iter() {
659 if candidate == secondary {
660 return Some((composed, false));
661 }
662 }
663 return None;
664 }
665 // `starter` is Hangul LV unless GIGO. If the debug asserts fail, we have a GIGO case.
666 debug_assert!(u32::from(primary).wrapping_sub(HANGUL_S_BASE) < HANGUL_S_COUNT);
667 debug_assert_eq!(
668 u32::from(primary).wrapping_sub(HANGUL_S_BASE) % HANGUL_T_COUNT,
669 0
670 );
671 if in_inclusive_range(secondary, '\u{11A8}', '\u{11C2}') {
672 let lvt = u32::from(primary) + (u32::from(secondary) - HANGUL_T_BASE);
673 if lvt < 0xD800 {
674 // SAFETY: Immediately above we checked that `c32` is below the surrogate
675 // range. (Not using `char::from_u32` itself as a micro optimization.)
676 // This is only a check about the safe `char` range. The result could
677 // still be GIGO, wich which case either of the above debug assertions
678 // should fail.
679 return Some((unsafe { char::from_u32_unchecked(lvt) }, false));
680 } else {
681 // GIGO
682 // Asserting `false`, although either of the above two debug assertions
683 // should already have caught this case.
684 debug_assert!(false);
685 }
686 }
687 None
688 }
689 #[cfg(feature = "serde")]
690 CanonicalCompositionsRef::Legacy(char16_trie) => {
691 Self::compose_legacy(char16_trie.iter(), starter, second).map(|c| (c, false))
692 }
693 }
694 }
695
696 #[cfg(feature = "serde")]
697 #[cold]
698 #[inline(never)]
699 fn compose_legacy(mut iter: Char16TrieIterator, starter: char, second: char) -> Option<char> {
700 let v = u32::from(second).wrapping_sub(HANGUL_V_BASE);
701 if v >= HANGUL_JAMO_LIMIT - HANGUL_V_BASE {
702 // To make the trie smaller, the pairs are stored second character first.
703 // Given how this method is used in ways where it's known that `second`
704 // is or isn't a starter. We could potentially split the trie into two
705 // tries depending on whether `second` is a starter.
706 match iter.next(second) {
707 TrieResult::NoMatch => None,
708 TrieResult::NoValue => match iter.next(starter) {
709 TrieResult::NoMatch => None,
710 TrieResult::FinalValue(i) => {
711 if let Some(c) = char::from_u32(i as u32) {
712 Some(c)
713 } else {
714 // GIGO case
715 debug_assert!(false);
716 None
717 }
718 }
719 TrieResult::NoValue | TrieResult::Intermediate(_) => {
720 // GIGO case
721 debug_assert!(false);
722 None
723 }
724 },
725 TrieResult::FinalValue(_) | TrieResult::Intermediate(_) => {
726 // GIGO case
727 debug_assert!(false);
728 None
729 }
730 }
731 } else {
732 if v < HANGUL_V_COUNT {
733 let l = u32::from(starter).wrapping_sub(HANGUL_L_BASE);
734 if l < HANGUL_L_COUNT {
735 let lv = l * HANGUL_N_COUNT + v * HANGUL_T_COUNT;
736 // Safe, because the inputs are known to be in range.
737 return Some(unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lv) });
738 }
739 return None;
740 }
741 if in_inclusive_range(second, '\u{11A8}', '\u{11C2}') {
742 let lv = u32::from(starter).wrapping_sub(HANGUL_S_BASE);
743 if lv < HANGUL_S_COUNT && lv % HANGUL_T_COUNT == 0 {
744 let lvt = lv + (u32::from(second) - HANGUL_T_BASE);
745 // Safe, because the inputs are known to be in range.
746 return Some(unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lvt) });
747 }
748 }
749 None
750 }
751 }
752}
753
754/// See trie-value-format.md
755#[inline(always)]
756fn starter_and_decomposes_to_self_impl(trie_val: u32) -> bool {
757 // The REPLACEMENT CHARACTER has `NON_ROUND_TRIP_MARKER` set,
758 // and this function needs to ignore that.
759 (trie_val & !(BACKWARD_COMBINING_MARKER | NON_ROUND_TRIP_MARKER)) == 0
760}
761
762/// See trie-value-format.md
763#[inline(always)]
764#[cfg(feature = "utf8_iter")]
765pub fn starter_and_decomposes_to_self_except_replacement(trie_val: u32) -> bool {
766 // This intentionally leaves `NON_ROUND_TRIP_MARKER` in the value
767 // to be compared with zero. U+FFFD has that flag set despite really
768 // being being round-tripping in order to make UTF-8 errors
769 // ineligible for passthrough.
770 (trie_val & !BACKWARD_COMBINING_MARKER) == 0
771}
772
773/// See trie-value-format.md
774#[inline(always)]
775fn potential_passthrough_and_cannot_combine_backwards(trie_val: u32) -> bool {
776 (trie_val & (NON_ROUND_TRIP_MARKER | BACKWARD_COMBINING_MARKER)) == 0
777}
778
779/// Struct for holding together a character and the value
780/// looked up for it from the NFD trie in a more explicit
781/// way than an anonymous pair.
782/// Also holds a flag about the supplementary-trie provenance.
783#[derive(Debug, PartialEq, Eq)]
784struct CharacterAndTrieValue {
785 character: char,
786 /// See trie-value-format.md
787 trie_val: u32,
788}
789
790impl CharacterAndTrieValue {
791 #[inline(always)]
792 pub fn new(c: char, trie_value: u32) -> Self {
793 CharacterAndTrieValue {
794 character: c,
795 trie_val: trie_value,
796 }
797 }
798
799 #[inline(always)]
800 pub fn starter_and_decomposes_to_self(&self) -> bool {
801 starter_and_decomposes_to_self_impl(self.trie_val)
802 }
803
804 /// See trie-value-format.md
805 #[inline(always)]
806 pub fn can_combine_backwards(&self) -> bool {
807 (self.trie_val & BACKWARD_COMBINING_MARKER) != 0
808 }
809 /// See trie-value-format.md
810 #[inline(always)]
811 pub fn potential_passthrough(&self) -> bool {
812 (self.trie_val & NON_ROUND_TRIP_MARKER) == 0
813 }
814}
815
816/// Pack a `char` and a `CanonicalCombiningClass` in
817/// 32 bits (the former in the lower 24 bits and the
818/// latter in the high 8 bits). The latter can be
819/// initialized to 0xFF upon creation, in which case
820/// it can be actually set later by calling
821/// `set_ccc_from_trie_if_not_already_set`. This is
822/// a micro optimization to avoid the Canonical
823/// Combining Class trie lookup when there is only
824/// one combining character in a sequence. This type
825/// is intentionally non-`Copy` to get compiler help
826/// in making sure that the class is set on the
827/// instance on which it is intended to be set
828/// and not on a temporary copy.
829///
830/// Note that 0xFF is won't be assigned to an actual
831/// canonical combining class per definition D104
832/// in The Unicode Standard.
833//
834// NOTE: The Pernosco debugger has special knowledge
835// of this struct. Please do not change the bit layout
836// or the crate-module-qualified name of this struct
837// without coordination.
838#[derive(Debug)]
839struct CharacterAndClass(u32);
840
841impl<'data> CharacterAndClass {
842 pub fn new(c: char, ccc: CanonicalCombiningClass) -> Self {
843 CharacterAndClass(u32::from(c) | (u32::from(ccc.to_icu4c_value()) << 24))
844 }
845 pub fn new_with_placeholder(c: char) -> Self {
846 CharacterAndClass(u32::from(c) | ((0xFF) << 24))
847 }
848 pub fn new_with_trie_value(c_tv: CharacterAndTrieValue) -> Self {
849 Self::new(c_tv.character, ccc_from_trie_value(c_tv.trie_val))
850 }
851 pub fn new_starter(c: char) -> Self {
852 CharacterAndClass(u32::from(c))
853 }
854 /// This method must exist for Pernosco to apply its special rendering.
855 /// Also, this must not be dead code!
856 pub fn character(&self) -> char {
857 // SAFETY: Safe, because the low 24 bits came from a `char`
858 // originally.
859 unsafe { char::from_u32_unchecked(self.0 & 0xFFFFFF) }
860 }
861 /// This method must exist for Pernosco to apply its special rendering.
862 pub fn ccc(&self) -> CanonicalCombiningClass {
863 CanonicalCombiningClass::from_icu4c_value((self.0 >> 24) as u8)
864 }
865
866 pub fn character_and_ccc(&self) -> (char, CanonicalCombiningClass) {
867 (self.character(), self.ccc())
868 }
869 pub fn set_ccc_from_trie_if_not_already_set<T: AbstractCodePointTrie<'data, u32>>(
870 &mut self,
871 trie: &'data T,
872 ) {
873 if self.0 >> 24 != 0xFF {
874 return;
875 }
876 let scalar = self.0 & 0xFFFFFF;
877 // SAFETY: Safe, because the low 24 bits came from a `char`
878 // originally.
879 self.0 = ((ccc_from_trie_value(trie.scalar(unsafe { char::from_u32_unchecked(scalar) }))
880 .to_icu4c_value() as u32)
881 << 24)
882 | scalar;
883 }
884}
885
886/// An iterator adaptor that turns an `Iterator` over `char` into
887/// a lazily-decomposed `char` sequence.
888#[derive(Debug)]
889pub struct Decomposition<'data, I>
890where
891 I: Iterator<Item = char>,
892{
893 inner: DecompositionInner<
894 'data,
895 CharIterWithTrie<'data, Trie<'data>, u32, I>,
896 Trie<'data>,
897 Uax15Policy,
898 >,
899}
900
901impl<'data, I> Decomposition<'data, I>
902where
903 I: Iterator<Item = char>,
904{
905 /// Constructs a decomposing iterator adapter from a delegate
906 /// iterator and references to the necessary data, without
907 /// supplementary data.
908 ///
909 /// Use `DecomposingNormalizer::normalize_iter()` instead unless
910 /// there's a good reason to use this constructor directly.
911 ///
912 /// Public but hidden in order to be able to use this from the
913 /// collator.
914 #[doc(hidden)] // used in older versions of collator
915 #[deprecated = "Use `new_decomposition()` instead"]
916 pub fn new(
917 delegate: I,
918 decompositions: &'data DecompositionData,
919 tables: &'data DecompositionTables,
920 ) -> Self {
921 let mut ret = Self {
922 inner: DecompositionInner::new_with_supplements(
923 CharIterWithTrie::new(
924 delegate,
925 #[allow(clippy::useless_conversion)]
926 <&Trie<'data>>::try_from(&decompositions.trie)
927 .unwrap_or_else(|_| unreachable!("Incompatible data")),
928 ),
929 tables,
930 None,
931 ),
932 };
933 let _ = ret.next();
934 ret
935 }
936}
937
938impl<I> Iterator for Decomposition<'_, I>
939where
940 I: Iterator<Item = char>,
941{
942 type Item = char;
943
944 #[inline]
945 fn next(&mut self) -> Option<char> {
946 self.inner.next()
947 }
948}
949
950/// The iterator first yields an extra U+FFFD and then
951/// the sequence actually corresponding to the input.
952#[doc(hidden)] // used in collator
953#[inline(always)]
954pub fn new_decomposition<'data, I, T>(
955 delegate: I,
956 tables: &'data DecompositionTables,
957) -> impl Iterator<Item = char> + 'data
958where
959 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32> + 'data,
960 T: AbstractCodePointTrie<'data, u32> + 'data,
961{
962 DecompositionInner::<'data, I, T, Uax15Policy>::new_with_supplements(delegate, tables, None)
963}
964
965#[derive(Debug)]
966struct DecompositionInner<'data, I, T, P>
967where
968 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
969 T: AbstractCodePointTrie<'data, u32>,
970 P: IteratorPolicy,
971{
972 // See trie-value-format.md for the trie wrapped in `delegate`
973 delegate: I,
974 buffer: CombiningBuffer,
975 /// The index of the next item to be read from `buffer`.
976 /// The purpose if this index is to avoid having to move
977 /// the rest upon every read.
978 buffer_pos: usize,
979 // At the start of `next()` if not `None`, this is a pending unnormalized
980 // starter. When `Decomposition` appears alone, this is never a non-starter.
981 // However, when `Decomposition` appears inside a `Composition`, this
982 // may become a non-starter before `decomposing_next()` is called.
983 pending: Option<CharacterAndTrieValue>, // None at end of stream
984 scalars16: &'data ZeroSlice<u16>,
985 scalars24: &'data ZeroSlice<char>,
986 supplementary_scalars16: &'data ZeroSlice<u16>,
987 supplementary_scalars24: &'data ZeroSlice<char>,
988 _phantom_p: PhantomData<P>,
989 _phantom_t: PhantomData<T>,
990}
991
992impl<'data, I, T, P> DecompositionInner<'data, I, T, P>
993where
994 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
995 T: AbstractCodePointTrie<'data, u32> + 'data,
996 P: IteratorPolicy,
997{
998 /// Constructs a decomposing iterator adapter from a delegate
999 /// iterator and references to the necessary data, including
1000 /// supplementary data.
1001 ///
1002 /// The iterator first yields a U+0000 and only then the sequence
1003 /// corresponding to the input. Unfortunately, due to the way
1004 /// stack placement of structs work in Rust, the caller is responsible
1005 /// for dealing with the initial U+0000. Alternatively, callers in this
1006 /// crate file can (and should) call `init()`.
1007 #[inline(always)]
1008 fn new_with_supplements(
1009 delegate: I,
1010 tables: &'data DecompositionTables,
1011 supplementary_tables: Option<&'data DecompositionTables>,
1012 ) -> Self {
1013 DecompositionInner::<I, T, P> {
1014 delegate,
1015 buffer: SmallVec::new(), // Normalized
1016 buffer_pos: 0,
1017 // Initialize with a placeholder starter in case
1018 // the real stream starts with a non-starter.
1019 pending: Some(CharacterAndTrieValue::new('\u{0}', 0)),
1020 scalars16: &tables.scalars16,
1021 scalars24: &tables.scalars24,
1022 supplementary_scalars16: if let Some(supplementary) = supplementary_tables {
1023 &supplementary.scalars16
1024 } else {
1025 EMPTY_U16
1026 },
1027 supplementary_scalars24: if let Some(supplementary) = supplementary_tables {
1028 &supplementary.scalars24
1029 } else {
1030 EMPTY_CHAR
1031 },
1032 _phantom_p: PhantomData,
1033 _phantom_t: PhantomData,
1034 }
1035 }
1036
1037 /// Simplified alternative to calling `next()` and discarding the value after constructing this struct.
1038 fn init(&mut self) {
1039 self.pending = None;
1040 self.gather_and_sort_combining(0);
1041 }
1042
1043 fn push_decomposition16(
1044 &mut self,
1045 offset: usize,
1046 len: usize,
1047 only_non_starters_in_trail: bool,
1048 slice16: &ZeroSlice<u16>,
1049 ) -> (char, usize) {
1050 let (starter, tail) = slice16
1051 .get_subslice(offset..offset + len)
1052 .and_then(|slice| slice.split_first())
1053 .map_or_else(
1054 || {
1055 // GIGO case
1056 debug_assert!(false);
1057 (REPLACEMENT_CHARACTER, EMPTY_U16)
1058 },
1059 |(first, trail)| (char_from_u16(first), trail),
1060 );
1061 if only_non_starters_in_trail {
1062 // All the rest are combining
1063 self.buffer.extend(
1064 tail.iter()
1065 .map(|u| CharacterAndClass::new_with_placeholder(char_from_u16(u))),
1066 );
1067 (starter, 0)
1068 } else {
1069 let mut i = 0;
1070 let mut combining_start = 0;
1071 for u in tail.iter() {
1072 let ch = char_from_u16(u);
1073 let trie_value = self.delegate.trie().scalar(ch);
1074 self.buffer.push(CharacterAndClass::new_with_trie_value(
1075 CharacterAndTrieValue::new(ch, trie_value),
1076 ));
1077 i += 1;
1078 // Half-width kana and iota subscript don't occur in the tails
1079 // of these multicharacter decompositions.
1080 if !decomposition_starts_with_non_starter(trie_value) {
1081 combining_start = i;
1082 }
1083 }
1084 (starter, combining_start)
1085 }
1086 }
1087
1088 fn push_decomposition32(
1089 &mut self,
1090 offset: usize,
1091 len: usize,
1092 only_non_starters_in_trail: bool,
1093 slice32: &ZeroSlice<char>,
1094 ) -> (char, usize) {
1095 let (starter, tail) = slice32
1096 .get_subslice(offset..offset + len)
1097 .and_then(|slice| slice.split_first())
1098 .unwrap_or_else(|| {
1099 // GIGO case
1100 debug_assert!(false);
1101 (REPLACEMENT_CHARACTER, EMPTY_CHAR)
1102 });
1103 if only_non_starters_in_trail {
1104 // All the rest are combining
1105 self.buffer
1106 .extend(tail.iter().map(CharacterAndClass::new_with_placeholder));
1107 (starter, 0)
1108 } else {
1109 let mut i = 0;
1110 let mut combining_start = 0;
1111 for ch in tail.iter() {
1112 let trie_value = self.delegate.trie().scalar(ch);
1113 self.buffer.push(CharacterAndClass::new_with_trie_value(
1114 CharacterAndTrieValue::new(ch, trie_value),
1115 ));
1116 i += 1;
1117 // Half-width kana and iota subscript don't occur in the tails
1118 // of these multicharacter decompositions.
1119 if !decomposition_starts_with_non_starter(trie_value) {
1120 combining_start = i;
1121 }
1122 }
1123 (starter, combining_start)
1124 }
1125 }
1126
1127 fn delegate_next_no_pending(&mut self) -> Option<CharacterAndTrieValue> {
1128 debug_assert!(self.pending.is_none());
1129 loop {
1130 let (c, trie_val) = self.delegate.next()?;
1131
1132 if trie_val == IGNORABLE_MARKER {
1133 match P::IGNORABLE_BEHAVIOR {
1134 IgnorableBehavior::Unsupported => {
1135 debug_assert!(false);
1136 }
1137 IgnorableBehavior::ReplacementCharacter => {
1138 return Some(CharacterAndTrieValue::new(
1139 c,
1140 u32::from(REPLACEMENT_CHARACTER) | NON_ROUND_TRIP_MARKER,
1141 ));
1142 }
1143 IgnorableBehavior::Ignored => {
1144 // Else ignore this character by reading the next one from the delegate.
1145 continue;
1146 }
1147 }
1148 }
1149 return Some(CharacterAndTrieValue::new(c, trie_val));
1150 }
1151 }
1152
1153 fn delegate_next(&mut self) -> Option<CharacterAndTrieValue> {
1154 if let Some(pending) = self.pending.take() {
1155 // Only happens as part of `Composition` and as part of
1156 // the contiguous-buffer methods of `DecomposingNormalizer`.
1157 // I.e. does not happen as part of standalone iterator
1158 // usage of `Decomposition`.
1159 Some(pending)
1160 } else {
1161 self.delegate_next_no_pending()
1162 }
1163 }
1164
1165 fn decomposing_next(&mut self, c_and_trie_val: CharacterAndTrieValue) -> char {
1166 let (starter, combining_start) = {
1167 let c = c_and_trie_val.character;
1168 // See trie-value-format.md
1169 let decomposition = c_and_trie_val.trie_val;
1170 // The REPLACEMENT CHARACTER has `NON_ROUND_TRIP_MARKER` set,
1171 // and that flag needs to be ignored here.
1172 if (decomposition & !(BACKWARD_COMBINING_MARKER | NON_ROUND_TRIP_MARKER)) == 0 {
1173 // The character is its own decomposition
1174 (c, 0)
1175 } else {
1176 let high_zeros = (decomposition & HIGH_ZEROS_MASK) == 0;
1177 let low_zeros = (decomposition & LOW_ZEROS_MASK) == 0;
1178 if !high_zeros && !low_zeros {
1179 // Decomposition into two BMP characters: starter and non-starter
1180 let starter = char_from_u32(decomposition & 0x7FFF);
1181 let combining = char_from_u32((decomposition >> 15) & 0x7FFF);
1182 self.buffer
1183 .push(CharacterAndClass::new_with_placeholder(combining));
1184 (starter, 0)
1185 } else if high_zeros {
1186 // Do the check by looking at `c` instead of looking at a marker
1187 // in `singleton` below, because if we looked at the trie value,
1188 // we'd still have to check that `c` is in the Hangul syllable
1189 // range in order for the subsequent interpretations as `char`
1190 // to be safe.
1191 // Alternatively, `FDFA_MARKER` and the Hangul marker could
1192 // be unified. That would add a branch for Hangul and remove
1193 // a branch from singleton decompositions. It seems more
1194 // important to favor Hangul syllables than singleton
1195 // decompositions.
1196 // Note that it would be valid to hoist this Hangul check
1197 // one or even two steps earlier in this check hierarchy.
1198 // Right now, it's assumed the kind of decompositions into
1199 // BMP starter and non-starter, which occur in many languages,
1200 // should be checked before Hangul syllables, which are about
1201 // one language specifically. Hopefully, we get some
1202 // instruction-level parallelism out of the disjointness of
1203 // operations on `c` and `decomposition`.
1204 let hangul_offset = u32::from(c).wrapping_sub(HANGUL_S_BASE); // SIndex in the spec
1205 if hangul_offset < HANGUL_S_COUNT {
1206 debug_assert_eq!(decomposition, 1);
1207 // Hangul syllable
1208 // The math here comes from page 144 of Unicode 14.0
1209 let l = hangul_offset / HANGUL_N_COUNT;
1210 let v = (hangul_offset % HANGUL_N_COUNT) / HANGUL_T_COUNT;
1211 let t = hangul_offset % HANGUL_T_COUNT;
1212
1213 // The unsafe blocks here are OK, because the values stay
1214 // within the Hangul jamo block and, therefore, the scalar
1215 // value range by construction.
1216 self.buffer.push(CharacterAndClass::new_starter(unsafe {
1217 core::char::from_u32_unchecked(HANGUL_V_BASE + v)
1218 }));
1219 let first = unsafe { core::char::from_u32_unchecked(HANGUL_L_BASE + l) };
1220 if t != 0 {
1221 self.buffer.push(CharacterAndClass::new_starter(unsafe {
1222 core::char::from_u32_unchecked(HANGUL_T_BASE + t)
1223 }));
1224 (first, 2)
1225 } else {
1226 (first, 1)
1227 }
1228 } else {
1229 let singleton = decomposition as u16;
1230 if singleton != FDFA_MARKER {
1231 // Decomposition into one BMP character
1232 let starter = char_from_u16(singleton);
1233 (starter, 0)
1234 } else {
1235 // Special case for the NFKD form of U+FDFA.
1236 self.buffer.extend(FDFA_NFKD.map(|u| {
1237 // SAFETY: `FDFA_NFKD` is known not to contain
1238 // surrogates.
1239 CharacterAndClass::new_starter(unsafe {
1240 core::char::from_u32_unchecked(u32::from(u))
1241 })
1242 }));
1243 ('\u{0635}', 17)
1244 }
1245 }
1246 } else {
1247 debug_assert!(low_zeros);
1248 // Only 12 of 14 bits used as of Unicode 16.
1249 let offset = (((decomposition & !(0b11 << 30)) >> 16) as usize) - 1;
1250 // Only 3 of 4 bits used as of Unicode 16.
1251 let len_bits = decomposition & 0b1111;
1252 let only_non_starters_in_trail = (decomposition & 0b10000) != 0;
1253 if offset < self.scalars16.len() {
1254 self.push_decomposition16(
1255 offset,
1256 (len_bits + 2) as usize,
1257 only_non_starters_in_trail,
1258 self.scalars16,
1259 )
1260 } else if offset < self.scalars16.len() + self.scalars24.len() {
1261 self.push_decomposition32(
1262 offset - self.scalars16.len(),
1263 (len_bits + 1) as usize,
1264 only_non_starters_in_trail,
1265 self.scalars24,
1266 )
1267 } else if offset
1268 < self.scalars16.len()
1269 + self.scalars24.len()
1270 + self.supplementary_scalars16.len()
1271 {
1272 self.push_decomposition16(
1273 offset - (self.scalars16.len() + self.scalars24.len()),
1274 (len_bits + 2) as usize,
1275 only_non_starters_in_trail,
1276 self.supplementary_scalars16,
1277 )
1278 } else {
1279 self.push_decomposition32(
1280 offset
1281 - (self.scalars16.len()
1282 + self.scalars24.len()
1283 + self.supplementary_scalars16.len()),
1284 (len_bits + 1) as usize,
1285 only_non_starters_in_trail,
1286 self.supplementary_scalars24,
1287 )
1288 }
1289 }
1290 }
1291 };
1292 // Either we're inside `Composition` or `self.pending.is_none()`.
1293
1294 self.gather_and_sort_combining(combining_start);
1295 starter
1296 }
1297
1298 // This function exists as a borrow check helper.
1299 #[inline(always)]
1300 fn sort_slice_by_ccc(slice: &mut [CharacterAndClass], trie: &'data T) {
1301 // We don't look up the canonical combining class for starters
1302 // of for single combining characters between starters. When
1303 // there's more than one combining character between starters,
1304 // we look up the canonical combining class for each character
1305 // exactly once.
1306 if slice.len() < 2 {
1307 return;
1308 }
1309 slice
1310 .iter_mut()
1311 .for_each(|cc| cc.set_ccc_from_trie_if_not_already_set(trie));
1312 slice.sort_by_key(|cc| cc.ccc());
1313 }
1314
1315 #[cold]
1316 #[inline(never)]
1317 fn push_special_decomposition(buffer: &mut CombiningBuffer, c: char) {
1318 // The Tibetan special cases are starters that decompose into non-starters.
1319 let mapped = match c {
1320 '\u{0340}' => {
1321 // COMBINING GRAVE TONE MARK
1322 CharacterAndClass::new('\u{0300}', CCC_ABOVE)
1323 }
1324 '\u{0341}' => {
1325 // COMBINING ACUTE TONE MARK
1326 CharacterAndClass::new('\u{0301}', CCC_ABOVE)
1327 }
1328 '\u{0343}' => {
1329 // COMBINING GREEK KORONIS
1330 CharacterAndClass::new('\u{0313}', CCC_ABOVE)
1331 }
1332 '\u{0344}' => {
1333 // COMBINING GREEK DIALYTIKA TONOS
1334 buffer.push(CharacterAndClass::new('\u{0308}', CCC_ABOVE));
1335 CharacterAndClass::new('\u{0301}', CCC_ABOVE)
1336 }
1337 '\u{0F73}' => {
1338 // TIBETAN VOWEL SIGN II
1339 buffer.push(CharacterAndClass::new('\u{0F71}', ccc!(CCC129, 129)));
1340 CharacterAndClass::new('\u{0F72}', ccc!(CCC130, 130))
1341 }
1342 '\u{0F75}' => {
1343 // TIBETAN VOWEL SIGN UU
1344 buffer.push(CharacterAndClass::new('\u{0F71}', ccc!(CCC129, 129)));
1345 CharacterAndClass::new('\u{0F74}', ccc!(CCC132, 132))
1346 }
1347 '\u{0F81}' => {
1348 // TIBETAN VOWEL SIGN REVERSED II
1349 buffer.push(CharacterAndClass::new('\u{0F71}', ccc!(CCC129, 129)));
1350 CharacterAndClass::new('\u{0F80}', ccc!(CCC130, 130))
1351 }
1352 '\u{FF9E}' => {
1353 // HALFWIDTH KATAKANA VOICED SOUND MARK
1354 CharacterAndClass::new('\u{3099}', ccc!(KanaVoicing, 8))
1355 }
1356 '\u{FF9F}' => {
1357 // HALFWIDTH KATAKANA VOICED SOUND MARK
1358 CharacterAndClass::new('\u{309A}', ccc!(KanaVoicing, 8))
1359 }
1360 _ => {
1361 // GIGO case
1362 debug_assert!(false);
1363 CharacterAndClass::new_with_placeholder(REPLACEMENT_CHARACTER)
1364 }
1365 };
1366 buffer.push(mapped);
1367 }
1368
1369 fn gather_and_sort_combining(&mut self, combining_start: usize) {
1370 // Not a `for` loop to avoid holding a mutable reference to `self` across
1371 // the loop body.
1372 while let Some(ch_and_trie_val) = self.delegate_next() {
1373 if !trie_value_has_ccc(ch_and_trie_val.trie_val) {
1374 self.pending = Some(ch_and_trie_val);
1375 break;
1376 } else if !trie_value_indicates_special_non_starter_decomposition(
1377 ch_and_trie_val.trie_val,
1378 ) {
1379 self.buffer
1380 .push(CharacterAndClass::new_with_trie_value(ch_and_trie_val));
1381 } else {
1382 Self::push_special_decomposition(&mut self.buffer, ch_and_trie_val.character);
1383 }
1384 }
1385 // Slicing succeeds by construction; we've always ensured that `combining_start`
1386 // is in permissible range.
1387 #[expect(clippy::indexing_slicing)]
1388 Self::sort_slice_by_ccc(&mut self.buffer[combining_start..], self.delegate.trie());
1389 }
1390}
1391
1392impl<'data, I, T, P> Iterator for DecompositionInner<'data, I, T, P>
1393where
1394 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
1395 T: AbstractCodePointTrie<'data, u32> + 'data,
1396 P: IteratorPolicy,
1397{
1398 type Item = char;
1399
1400 #[inline]
1401 fn next(&mut self) -> Option<char> {
1402 if let Some(ret) = self.buffer.get(self.buffer_pos).map(|c| c.character()) {
1403 self.buffer_pos += 1;
1404 if self.buffer_pos == self.buffer.len() {
1405 self.buffer.clear();
1406 self.buffer_pos = 0;
1407 }
1408 return Some(ret);
1409 }
1410 debug_assert_eq!(self.buffer_pos, 0);
1411 let c_and_trie_val = self.pending.take()?;
1412 Some(self.decomposing_next(c_and_trie_val))
1413 }
1414}
1415
1416/// An iterator adaptor that turns an `Iterator` over `char` into
1417/// a lazily-decomposed and then canonically composed `char` sequence.
1418#[derive(Debug)]
1419pub struct Composition<'data, I>
1420where
1421 I: Iterator<Item = char>,
1422{
1423 inner: CompositionInner<
1424 'data,
1425 CharIterWithTrie<'data, Trie<'data>, u32, I>,
1426 Trie<'data>,
1427 Uax15Policy,
1428 >,
1429}
1430
1431impl<I> Iterator for Composition<'_, I>
1432where
1433 I: Iterator<Item = char>,
1434{
1435 type Item = char;
1436
1437 #[inline]
1438 fn next(&mut self) -> Option<char> {
1439 self.inner.next()
1440 }
1441}
1442
1443#[derive(Debug)]
1444struct CompositionInner<'data, I, T, P>
1445where
1446 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
1447 T: AbstractCodePointTrie<'data, u32>,
1448 P: IteratorPolicy,
1449{
1450 /// The decomposing part of the normalizer than operates before
1451 /// the canonical composition is performed on its output.
1452 decomposition: DecompositionInner<'data, I, T, P>,
1453 /// Non-Hangul canonical composition data.
1454 canonical_compositions: CanonicalCompositionsRef<'data>,
1455 /// To make `next()` yield in cases where there's a non-composing
1456 /// starter in the decomposition buffer, we put it here to let it
1457 /// wait for the next `next()` call (or a jump forward within the
1458 /// `next()` call).
1459 unprocessed_starter: Option<char>,
1460}
1461
1462impl<'data, I, T, P> CompositionInner<'data, I, T, P>
1463where
1464 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
1465 T: AbstractCodePointTrie<'data, u32>,
1466 P: IteratorPolicy,
1467{
1468 #[inline(always)]
1469 fn new(
1470 decomposition: DecompositionInner<'data, I, T, P>,
1471 canonical_compositions: CanonicalCompositionsRef<'data>,
1472 ) -> Self {
1473 Self {
1474 decomposition,
1475 canonical_compositions,
1476 unprocessed_starter: None,
1477 }
1478 }
1479
1480 /// Performs canonical composition (including Hangul) on a pair of
1481 /// characters or returns `None` if these characters don't compose.
1482 /// Composition exclusions are taken into account.
1483 #[inline(always)]
1484 pub(crate) fn compose(&self, starter: char, second: char) -> Option<char> {
1485 self.canonical_compositions.compose(starter, second)
1486 }
1487
1488 /// Performs canonical composition (including Hangul) on a pair of
1489 /// characters on the assumption that the second one is a starter
1490 /// or returns `None` if these characters don't compose.
1491 /// Composition exclusions are taken into account.
1492 ///
1493 /// The returned boolean can be true only if `char` a Hangul LV syllable.
1494 ///
1495 /// The argument `starter_is_lv` must be set either to false or to the value
1496 /// that this method previously returned alongside `starter`.
1497 #[inline(always)]
1498 pub(crate) fn compose_starter(
1499 &self,
1500 starter: char,
1501 second: char,
1502 starter_is_lv: bool,
1503 ) -> Option<(char, bool)> {
1504 self.canonical_compositions
1505 .compose_starter(starter, second, starter_is_lv)
1506 }
1507}
1508
1509impl<'data, I, T, P> Iterator for CompositionInner<'data, I, T, P>
1510where
1511 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
1512 T: AbstractCodePointTrie<'data, u32> + 'data,
1513 P: IteratorPolicy,
1514{
1515 type Item = char;
1516
1517 #[inline]
1518 fn next(&mut self) -> Option<char> {
1519 let mut undecomposed_starter = CharacterAndTrieValue::new('\u{0}', 0); // The compiler can't figure out that this gets overwritten before use.
1520 if self.unprocessed_starter.is_none() {
1521 // The loop is only broken out of as goto forward
1522 #[expect(clippy::never_loop)]
1523 loop {
1524 if let Some((character, ccc)) = self
1525 .decomposition
1526 .buffer
1527 .get(self.decomposition.buffer_pos)
1528 .map(|c| c.character_and_ccc())
1529 {
1530 self.decomposition.buffer_pos += 1;
1531 if self.decomposition.buffer_pos == self.decomposition.buffer.len() {
1532 self.decomposition.buffer.clear();
1533 self.decomposition.buffer_pos = 0;
1534 }
1535 if ccc == CCC_NOT_REORDERED {
1536 // Previous decomposition contains a starter. This must
1537 // now become the `unprocessed_starter` for it to have
1538 // a chance to compose with the upcoming characters.
1539 //
1540 // E.g. parenthesized Hangul in NFKC comes through here,
1541 // but suitable composition exclusion could exercise this
1542 // in NFC.
1543 self.unprocessed_starter = Some(character);
1544 break; // We already have a starter, so skip taking one from `pending`.
1545 }
1546 return Some(character);
1547 }
1548 debug_assert_eq!(self.decomposition.buffer_pos, 0);
1549 undecomposed_starter = self.decomposition.pending.take()?;
1550 if undecomposed_starter.potential_passthrough() {
1551 // TODO(#2385): In the NFC case (moot for NFKC and UTS46), if the upcoming
1552 // character is not below `decomposition_passthrough_bound` but is
1553 // below `composition_passthrough_bound`, we read from the trie
1554 // unnecessarily.
1555 if let Some(upcoming) = self.decomposition.delegate_next_no_pending() {
1556 let cannot_combine_backwards = !upcoming.can_combine_backwards();
1557 self.decomposition.pending = Some(upcoming);
1558 if cannot_combine_backwards {
1559 // Fast-track succeeded!
1560 return Some(undecomposed_starter.character);
1561 }
1562 } else {
1563 // End of stream
1564 return Some(undecomposed_starter.character);
1565 }
1566 }
1567 break; // Not actually looping
1568 }
1569 }
1570 let mut starter = '\u{0}'; // The compiler can't figure out this gets overwritten before use.
1571 // It would be fancier to bundle `starter` and `starter_is_lv` into an encapsulating
1572 // struct, but that would result in lots of useless assignments to `starter_is_lv`.
1573 // Using `debug_assert!(!starter_is_lv);` a lot instead.
1574 let mut starter_is_lv = false;
1575
1576 // The point of having this boolean is to have only one call site to
1577 // `self.decomposition.decomposing_next`, which is hopefully beneficial for
1578 // code size under inlining.
1579 let mut attempt_composition = false;
1580 loop {
1581 if let Some(unprocessed) = self.unprocessed_starter.take() {
1582 debug_assert_eq!(undecomposed_starter, CharacterAndTrieValue::new('\u{0}', 0));
1583 debug_assert_eq!(starter, '\u{0}');
1584 debug_assert!(!starter_is_lv);
1585 starter = unprocessed;
1586 } else {
1587 debug_assert_eq!(self.decomposition.buffer_pos, 0);
1588 let next_starter = self.decomposition.decomposing_next(undecomposed_starter);
1589 if !attempt_composition {
1590 debug_assert!(!starter_is_lv);
1591 starter = next_starter;
1592 } else if let Some((composed, is_lv)) =
1593 self.compose_starter(starter, next_starter, starter_is_lv)
1594 {
1595 // Normal non-enclosed Hangul is composed here.
1596 starter_is_lv = is_lv;
1597 starter = composed;
1598 } else {
1599 // This is our yield point. We'll pick this up above in the
1600 // next call to `next()`.
1601 self.unprocessed_starter = Some(next_starter);
1602 return Some(starter);
1603 }
1604 }
1605 // We first loop by index to avoid moving the contents of `buffer`, but
1606 // if there's a discontiguous match, we'll start modifying `buffer` instead.
1607 loop {
1608 let (character, ccc) = if let Some((character, ccc)) = self
1609 .decomposition
1610 .buffer
1611 .get(self.decomposition.buffer_pos)
1612 .map(|c| c.character_and_ccc())
1613 {
1614 (character, ccc)
1615 } else {
1616 self.decomposition.buffer.clear();
1617 self.decomposition.buffer_pos = 0;
1618 break;
1619 };
1620 starter_is_lv = false;
1621 // In NFKC, enclosed Hangul is recomposed here.
1622 if let Some(composed) = self.compose(starter, character) {
1623 debug_assert!(!starter_is_lv);
1624 starter = composed;
1625 self.decomposition.buffer_pos += 1;
1626 continue;
1627 }
1628 let mut most_recent_skipped_ccc = ccc;
1629 {
1630 let _ = self
1631 .decomposition
1632 .buffer
1633 .drain(0..self.decomposition.buffer_pos);
1634 }
1635 self.decomposition.buffer_pos = 0;
1636 if most_recent_skipped_ccc == CCC_NOT_REORDERED {
1637 // We failed to compose a starter. Discontiguous match not allowed.
1638 // We leave the starter in `buffer` for `next()` to find.
1639 return Some(starter);
1640 }
1641 // TODO: Make use of `compose` having figured out that no other matches are
1642 // possible, either.
1643 let mut i = 1; // We have skipped one non-starter.
1644 while let Some((character, ccc)) = self
1645 .decomposition
1646 .buffer
1647 .get(i)
1648 .map(|c| c.character_and_ccc())
1649 {
1650 if ccc == CCC_NOT_REORDERED {
1651 // Discontiguous match not allowed.
1652 return Some(starter);
1653 }
1654 debug_assert!(ccc >= most_recent_skipped_ccc);
1655 if ccc != most_recent_skipped_ccc {
1656 // `character` is a non-starter, so we could use a variant of
1657 // `compose` that omits all the Hangul cases.
1658 // TODO: Make use of above `compose` having already done the trie lookup,
1659 // so the linear slice could be reused here.
1660 if let Some(composed) = self.compose(starter, character) {
1661 self.decomposition.buffer.remove(i);
1662 debug_assert!(!starter_is_lv);
1663 starter = composed;
1664 continue;
1665 }
1666 }
1667 most_recent_skipped_ccc = ccc;
1668 i += 1;
1669 }
1670 break;
1671 }
1672
1673 debug_assert_eq!(self.decomposition.buffer_pos, 0);
1674
1675 if !self.decomposition.buffer.is_empty() {
1676 return Some(starter);
1677 }
1678 // Now we need to check if composition with an upcoming starter is possible.
1679 #[expect(clippy::unwrap_used)]
1680 if self.decomposition.pending.is_some() {
1681 // We know that `pending_starter` decomposes to start with a starter.
1682 // Otherwise, it would have been moved to `self.decomposition.buffer`
1683 // by `self.decomposing_next()`. We do this set lookup here in order
1684 // to get an opportunity to go back to the fast track.
1685 // Note that this check has to happen _after_ checking that `pending`
1686 // holds a character, because this flag isn't defined to be meaningful
1687 // when `pending` isn't holding a character.
1688 let pending = self.decomposition.pending.as_ref().unwrap();
1689 if !pending.can_combine_backwards() {
1690 // Won't combine backwards anyway.
1691 return Some(starter);
1692 }
1693 // Consume what we peeked. `unwrap` OK, because we checked `is_some()`
1694 // above.
1695 undecomposed_starter = self.decomposition.pending.take().unwrap();
1696 // The following line is OK, because we're about to loop back
1697 // to `self.decomposition.decomposing_next(c);`, which will
1698 // restore the between-`next()`-calls invariant of `pending`
1699 // before this function returns.
1700 attempt_composition = true;
1701 continue;
1702 }
1703 // End of input
1704 return Some(starter);
1705 }
1706 }
1707}
1708
1709macro_rules! composing_normalize_to {
1710 ($(#[$meta:meta])*,
1711 $normalize_to:ident,
1712 $write:path,
1713 $slice:ty,
1714 $prolog:block,
1715 $always_valid_utf:literal,
1716 $as_slice:ident,
1717 $fast:block,
1718 $text:ident,
1719 $sink:ident,
1720 $composition:ident,
1721 $undecomposed_starter:ident,
1722 $pending_slice:ident,
1723 $len_utf:ident,
1724 $self:ident,
1725 $chars_with_trie:ident,
1726 ) => {
1727 $(#[$meta])*
1728 pub fn $normalize_to<W: $write + ?Sized>(
1729 &$self,
1730 $text: $slice,
1731 $sink: &mut W,
1732 ) -> core::fmt::Result {
1733 $prolog
1734 let mut $composition = $self.normalize_iter_private::<_, Trie, Uax15Policy>($text.$chars_with_trie($self.trie()));
1735 let _ = $composition.decomposition.init(); // Discard the U+0000.
1736
1737 for cc in $composition.decomposition.buffer.drain(..) {
1738 $sink.write_char(cc.character())?;
1739 }
1740
1741 'outer: loop {
1742 debug_assert_eq!($composition.decomposition.buffer_pos, 0);
1743 let mut $undecomposed_starter =
1744 if let Some(pending) = $composition.decomposition.pending.take() {
1745 pending
1746 } else {
1747 return Ok(());
1748 };
1749 if $undecomposed_starter.potential_passthrough()
1750 {
1751 // We don't know if a `REPLACEMENT_CHARACTER` occurred in the slice or
1752 // was returned in response to an error by the iterator. Assume the
1753 // latter for correctness even though it pessimizes the former.
1754 if $always_valid_utf || $undecomposed_starter.character != REPLACEMENT_CHARACTER {
1755 let $pending_slice = &$text[$text.len() - $composition.decomposition.delegate.$as_slice().len() - $undecomposed_starter.character.$len_utf()..];
1756 // The `$fast` block must either:
1757 // 1. Return due to reaching EOF
1758 // 2. Leave a starter with its trie value in `$undecomposed_starter`
1759 // and, if there is still more input, leave the next character
1760 // and its trie value in `$composition.decomposition.pending`.
1761 $fast
1762 }
1763 }
1764 // Fast track above, full algorithm below
1765 let mut starter = $composition
1766 .decomposition
1767 .decomposing_next($undecomposed_starter);
1768 'bufferloop: loop {
1769 // We first loop by index to avoid moving the contents of `buffer`, but
1770 // if there's a discontiguous match, we'll start modifying `buffer` instead.
1771 loop {
1772 let (character, ccc) = if let Some((character, ccc)) = $composition
1773 .decomposition
1774 .buffer
1775 .get($composition.decomposition.buffer_pos)
1776 .map(|c| c.character_and_ccc())
1777 {
1778 (character, ccc)
1779 } else {
1780 $composition.decomposition.buffer.clear();
1781 $composition.decomposition.buffer_pos = 0;
1782 break;
1783 };
1784 // In NFKC, enclosed Hangul get recomposed here.
1785 // Furthermore, in NFC if input has lv followed by t, lv gets
1786 // decomposed above and recomposed here.
1787 if let Some(composed) = $composition.compose(starter, character) {
1788 starter = composed;
1789 $composition.decomposition.buffer_pos += 1;
1790 continue;
1791 }
1792 let mut most_recent_skipped_ccc = ccc;
1793 if most_recent_skipped_ccc == CCC_NOT_REORDERED {
1794 // We failed to compose a starter. Discontiguous match not allowed.
1795 // Write the current `starter` we've been composing, make the unmatched
1796 // starter in the buffer the new `starter` (we know it's been decomposed)
1797 // and process the rest of the buffer with that as the starter.
1798 $sink.write_char(starter)?;
1799 starter = character;
1800 $composition.decomposition.buffer_pos += 1;
1801 continue 'bufferloop;
1802 } else {
1803 {
1804 let _ = $composition
1805 .decomposition
1806 .buffer
1807 .drain(0..$composition.decomposition.buffer_pos);
1808 }
1809 $composition.decomposition.buffer_pos = 0;
1810 }
1811 let mut i = 1; // We have skipped one non-starter.
1812 while let Some((character, ccc)) = $composition
1813 .decomposition
1814 .buffer
1815 .get(i)
1816 .map(|c| c.character_and_ccc())
1817 {
1818 if ccc == CCC_NOT_REORDERED {
1819 // Discontiguous match not allowed.
1820 $sink.write_char(starter)?;
1821 for cc in $composition.decomposition.buffer.drain(..i) {
1822 $sink.write_char(cc.character())?;
1823 }
1824 starter = character;
1825 {
1826 let removed = $composition.decomposition.buffer.remove(0);
1827 debug_assert_eq!(starter, removed.character());
1828 }
1829 debug_assert_eq!($composition.decomposition.buffer_pos, 0);
1830 continue 'bufferloop;
1831 }
1832 debug_assert!(ccc >= most_recent_skipped_ccc);
1833 if ccc != most_recent_skipped_ccc {
1834 // `character` is a non-starter, so we could use a variant of
1835 // `compose` that omits all the Hangul cases.
1836 if let Some(composed) =
1837 $composition.compose(starter, character)
1838 {
1839 $composition.decomposition.buffer.remove(i);
1840 starter = composed;
1841 continue;
1842 }
1843 }
1844 most_recent_skipped_ccc = ccc;
1845 i += 1;
1846 }
1847 break;
1848 }
1849 debug_assert_eq!($composition.decomposition.buffer_pos, 0);
1850
1851 if !$composition.decomposition.buffer.is_empty() {
1852 $sink.write_char(starter)?;
1853 for cc in $composition.decomposition.buffer.drain(..) {
1854 $sink.write_char(cc.character())?;
1855 }
1856 // We had non-empty buffer, so can't compose with upcoming.
1857 continue 'outer;
1858 }
1859 // We can loop back in case we compose a Hangul LV. Looping back
1860 // makes this code much simpler than trying to have a special
1861 // case that advances the underlying iterator in the branch that
1862 // now says `continue;` below.
1863 let mut starter_is_lv = false;
1864 loop {
1865 // Now we need to check if composition with an upcoming starter is possible.
1866 if $composition.decomposition.pending.is_some() {
1867 // We know that `pending_starter` decomposes to start with a starter.
1868 // Otherwise, it would have been moved to `composition.decomposition.buffer`
1869 // by `composition.decomposing_next()`. We do this set lookup here in order
1870 // to get an opportunity to go back to the fast track.
1871 // Note that this check has to happen _after_ checking that `pending`
1872 // holds a character, because this flag isn't defined to be meaningful
1873 // when `pending` isn't holding a character.
1874 let pending = $composition.decomposition.pending.as_ref().unwrap();
1875 if !pending.can_combine_backwards()
1876 {
1877 // Won't combine backwards anyway.
1878 $sink.write_char(starter)?;
1879 continue 'outer;
1880 }
1881 let pending_starter = $composition.decomposition.pending.take().unwrap();
1882 let decomposed = $composition.decomposition.decomposing_next(pending_starter);
1883 // Normal non-enclosed Hangul is composed here. The case where we have LV and T,
1884 // but LV was not composed here previously is possible.
1885 if let Some((composed, is_lv)) = $composition.compose_starter(starter, decomposed, starter_is_lv) {
1886 starter = composed;
1887 if is_lv && $composition.decomposition.buffer.is_empty() {
1888 starter_is_lv = true;
1889 // TODO: Put a Hangul fast-path that deals with conjoining jamo and ASCII
1890 // in a manner specialized for the UTF (i.e. not doing surrogate checks,
1891 // since surrogates are neither conjoining jamo nor ASCII) here.
1892 // https://github.com/unicode-org/icu4x/issues/7516
1893 continue;
1894 }
1895 } else {
1896 $sink.write_char(starter)?;
1897 starter = decomposed;
1898 }
1899 continue 'bufferloop;
1900 }
1901 break;
1902 }
1903 // End of input
1904 $sink.write_char(starter)?;
1905 return Ok(());
1906 } // 'bufferloop
1907 }
1908 }
1909 };
1910}
1911
1912macro_rules! decomposing_normalize_to {
1913 ($(#[$meta:meta])*,
1914 $normalize_to:ident,
1915 $write:path,
1916 $slice:ty,
1917 $prolog:block,
1918 $as_slice:ident,
1919 $fast:block,
1920 $text:ident,
1921 $sink:ident,
1922 $decomposition:ident,
1923 $undecomposed_starter:ident,
1924 $pending_slice:ident,
1925 $outer:lifetime, // loop labels use lifetime tokens
1926 $self:ident,
1927 $chars_with_trie:ident,
1928 ) => {
1929 $(#[$meta])*
1930 pub fn $normalize_to<W: $write + ?Sized>(
1931 &$self,
1932 $text: $slice,
1933 $sink: &mut W,
1934 ) -> core::fmt::Result {
1935 $prolog
1936
1937 let mut $decomposition = $self.normalize_iter_private::<_, Trie, Uax15Policy>($text.$chars_with_trie($self.trie()));
1938 let _ = $decomposition.init(); // Discard the U+0000.
1939
1940 $outer: loop {
1941 for cc in $decomposition.buffer.drain(..) {
1942 $sink.write_char(cc.character())?;
1943 }
1944 debug_assert_eq!($decomposition.buffer_pos, 0);
1945 let mut $undecomposed_starter = if let Some(pending) = $decomposition.pending.take() {
1946 pending
1947 } else {
1948 return Ok(());
1949 };
1950 loop {
1951 if $undecomposed_starter.starter_and_decomposes_to_self() {
1952 // Don't bother including `undecomposed_starter` in a contiguous buffer
1953 // write: Just write it right away:
1954 $sink.write_char($undecomposed_starter.character)?;
1955
1956 let $pending_slice = $decomposition.delegate.$as_slice();
1957 $fast
1958 }
1959 debug_assert!($decomposition.pending.is_none());
1960 let c_and_trie_val_unless_at_end = if let Some((upcoming, trie_val)) = $decomposition.delegate.next() {
1961 if likely(!decomposition_starts_with_non_starter(trie_val)) {
1962 Some(CharacterAndTrieValue::new(upcoming, trie_val))
1963 } else {
1964 $decomposition.pending = Some(CharacterAndTrieValue::new(upcoming, trie_val));
1965 break;
1966 }
1967 } else {
1968 None
1969 };
1970 // The upcoming character cannot sort into the tail of this decomposition,
1971 // so, for performance, let's write decomposition directly here without
1972 // going via `$decomposition.buffer`. This wall of (edited) copypaste is
1973 // crucial for performance competitiveness with ICU4C.
1974
1975 // Start edited copypaste from `decomposing_next`
1976
1977 let c = $undecomposed_starter.character;
1978 // See trie-value-format.md
1979 let decomposition = $undecomposed_starter.trie_val;
1980 // The REPLACEMENT CHARACTER has `NON_ROUND_TRIP_MARKER` set,
1981 // and that flag needs to be ignored here.
1982 if unlikely((decomposition & !(BACKWARD_COMBINING_MARKER | NON_ROUND_TRIP_MARKER)) == 0) {
1983 // The character is its own decomposition
1984 $sink.write_char(c)?;
1985 } else {
1986 let high_zeros = (decomposition & HIGH_ZEROS_MASK) == 0;
1987 let low_zeros = (decomposition & LOW_ZEROS_MASK) == 0;
1988 if !high_zeros && !low_zeros {
1989 // Decomposition into two BMP characters: starter and non-starter
1990 let starter = char_from_u32(decomposition & 0x7FFF);
1991 let combining = char_from_u32((decomposition >> 15) & 0x7FFF);
1992 $sink.write_char(starter)?;
1993 $sink.write_char(combining)?;
1994 } else if high_zeros {
1995 // Do the check by looking at `c` instead of looking at a marker
1996 // in `singleton` below, because if we looked at the trie value,
1997 // we'd still have to check that `c` is in the Hangul syllable
1998 // range in order for the subsequent interpretations as `char`
1999 // to be safe.
2000 // Alternatively, `FDFA_MARKER` and the Hangul marker could
2001 // be unified. That would add a branch for Hangul and remove
2002 // a branch from singleton decompositions. It seems more
2003 // important to favor Hangul syllables than singleton
2004 // decompositions.
2005 // Note that it would be valid to hoist this Hangul check
2006 // one or even two steps earlier in this check hierarchy.
2007 // Right now, it's assumed the kind of decompositions into
2008 // BMP starter and non-starter, which occur in many languages,
2009 // should be checked before Hangul syllables, which are about
2010 // one language specifically. Hopefully, we get some
2011 // instruction-level parallelism out of the disjointness of
2012 // operations on `c` and `decomposition`.
2013 let hangul_offset = u32::from(c).wrapping_sub(HANGUL_S_BASE); // SIndex in the spec
2014 if hangul_offset < HANGUL_S_COUNT {
2015 debug_assert_eq!(decomposition, 1);
2016 // Hangul syllable
2017 // The math here comes from page 144 of Unicode 14.0
2018 let l = hangul_offset / HANGUL_N_COUNT;
2019 let v = (hangul_offset % HANGUL_N_COUNT) / HANGUL_T_COUNT;
2020 let t = hangul_offset % HANGUL_T_COUNT;
2021
2022 // The unsafe blocks here are OK, because the values stay
2023 // within the Hangul jamo block and, therefore, the scalar
2024 // value range by construction.
2025 $sink.write_char(unsafe { core::char::from_u32_unchecked(HANGUL_L_BASE + l) })?;
2026 $sink.write_char(unsafe {
2027 core::char::from_u32_unchecked(HANGUL_V_BASE + v)
2028 })?;
2029 if t != 0 {
2030 $sink.write_char(unsafe {
2031 core::char::from_u32_unchecked(HANGUL_T_BASE + t)
2032 })?;
2033 }
2034 } else {
2035 let singleton = decomposition as u16;
2036 if singleton != FDFA_MARKER {
2037 // Decomposition into one BMP character
2038 let starter = char_from_u16(singleton);
2039 $sink.write_char(starter)?;
2040 } else {
2041 // Special case for the NFKD form of U+FDFA.
2042 $sink.write_char('\u{0635}')?;
2043 for u in FDFA_NFKD {
2044 // SAFETY: `FDFA_NFKD` is known not to contain
2045 // surrogates.
2046 $sink.write_char(unsafe { core::char::from_u32_unchecked(u32::from(u)) })?;
2047 }
2048 }
2049 }
2050 } else {
2051 debug_assert!(low_zeros);
2052 // Only 12 of 14 bits used as of Unicode 16.
2053 let offset = (((decomposition & !(0b11 << 30)) >> 16) as usize) - 1;
2054 // Only 3 of 4 bits used as of Unicode 16.
2055 let len_bits = decomposition & 0b1111;
2056 if let Some(subslice) = $decomposition.scalars16.get_subslice(offset..offset+((len_bits + 2) as usize)) {
2057 for u in subslice.iter() {
2058 $sink.write_char(char_from_u16(u))?;
2059 }
2060 } else {
2061 let offset = offset - $decomposition.scalars16.len();
2062 if let Some(subslice) = $decomposition.scalars24.get_subslice(offset..offset+((len_bits + 1) as usize)) {
2063 for c in subslice.iter() {
2064 $sink.write_char(c)?;
2065 }
2066 } else {
2067 let offset = offset - $decomposition.scalars24.len();
2068 if let Some(subslice) = $decomposition.supplementary_scalars16.get_subslice(offset..offset+((len_bits + 2) as usize)) {
2069 for u in subslice.iter() {
2070 $sink.write_char(char_from_u16(u))?;
2071 }
2072 } else {
2073 let offset = offset - $decomposition.supplementary_scalars16.len();
2074 if let Some(subslice) = $decomposition.supplementary_scalars24.get_subslice(offset..offset+((len_bits + 1) as usize)) {
2075 for c in subslice.iter() {
2076 $sink.write_char(c)?;
2077 }
2078 } else {
2079 // GIGO case
2080 debug_assert!(false);
2081 }
2082 }
2083 }
2084 }
2085 }
2086 }
2087
2088 // End edited copypaste from `decomposing_next`
2089
2090 if let Some(c_and_trie_val) = c_and_trie_val_unless_at_end {
2091 $undecomposed_starter = c_and_trie_val;
2092 continue;
2093 }
2094 return Ok(());
2095 }
2096 let starter = $decomposition.decomposing_next($undecomposed_starter);
2097 $sink.write_char(starter)?;
2098 }
2099 }
2100 };
2101}
2102
2103macro_rules! normalizer_methods {
2104 () => {
2105 /// Normalize a string slice into a `Cow<'a, str>`.
2106 pub fn normalize<'a>(&self, text: &'a str) -> Cow<'a, str> {
2107 let (head, tail) = self.split_normalized(text);
2108 if tail.is_empty() {
2109 return Cow::Borrowed(head);
2110 }
2111 let mut ret = String::new();
2112 ret.reserve(text.len());
2113 ret.push_str(head);
2114 let _ = self.normalize_to(tail, &mut ret);
2115 Cow::Owned(ret)
2116 }
2117
2118 /// Split a string slice into maximum normalized prefix and unnormalized suffix
2119 /// such that the concatenation of the prefix and the normalization of the suffix
2120 /// is the normalization of the whole input.
2121 pub fn split_normalized<'a>(&self, text: &'a str) -> (&'a str, &'a str) {
2122 let up_to = self.is_normalized_up_to(text);
2123 text.split_at_checked(up_to).unwrap_or_else(|| {
2124 // Internal bug, not even GIGO, never supposed to happen
2125 debug_assert!(false);
2126 ("", text)
2127 })
2128 }
2129
2130 /// Return the index a string slice is normalized up to.
2131 fn is_normalized_up_to(&self, text: &str) -> usize {
2132 let mut sink = IsNormalizedSinkStr::new(text);
2133 let _ = self.normalize_to(text, &mut sink);
2134 text.len() - sink.remaining_len()
2135 }
2136
2137 /// Check whether a string slice is normalized.
2138 pub fn is_normalized(&self, text: &str) -> bool {
2139 self.is_normalized_up_to(text) == text.len()
2140 }
2141
2142 /// Normalize a slice of potentially-invalid UTF-16 into a `Cow<'a, [u16]>`.
2143 ///
2144 /// Unpaired surrogates are mapped to the REPLACEMENT CHARACTER
2145 /// before normalizing.
2146 ///
2147 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2148 #[cfg(feature = "utf16_iter")]
2149 pub fn normalize_utf16<'a>(&self, text: &'a [u16]) -> Cow<'a, [u16]> {
2150 let (head, tail) = self.split_normalized_utf16(text);
2151 if tail.is_empty() {
2152 return Cow::Borrowed(head);
2153 }
2154 let mut ret = alloc::vec::Vec::with_capacity(text.len());
2155 ret.extend_from_slice(head);
2156 let _ = self.normalize_utf16_to(tail, &mut ret);
2157 Cow::Owned(ret)
2158 }
2159
2160 /// Split a slice of potentially-invalid UTF-16 into maximum normalized (and valid)
2161 /// prefix and unnormalized suffix such that the concatenation of the prefix and the
2162 /// normalization of the suffix is the normalization of the whole input.
2163 ///
2164 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2165 #[cfg(feature = "utf16_iter")]
2166 pub fn split_normalized_utf16<'a>(&self, text: &'a [u16]) -> (&'a [u16], &'a [u16]) {
2167 let up_to = self.is_normalized_utf16_up_to(text);
2168 text.split_at_checked(up_to).unwrap_or_else(|| {
2169 // Internal bug, not even GIGO, never supposed to happen
2170 debug_assert!(false);
2171 (&[], text)
2172 })
2173 }
2174
2175 /// Return the index a slice of potentially-invalid UTF-16 is normalized up to.
2176 ///
2177 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2178 #[cfg(feature = "utf16_iter")]
2179 fn is_normalized_utf16_up_to(&self, text: &[u16]) -> usize {
2180 let mut sink = IsNormalizedSinkUtf16::new(text);
2181 let _ = self.normalize_utf16_to(text, &mut sink);
2182 text.len() - sink.remaining_len()
2183 }
2184
2185 /// Checks whether a slice of potentially-invalid UTF-16 is normalized.
2186 ///
2187 /// Unpaired surrogates are treated as the REPLACEMENT CHARACTER.
2188 ///
2189 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2190 #[cfg(feature = "utf16_iter")]
2191 pub fn is_normalized_utf16(&self, text: &[u16]) -> bool {
2192 self.is_normalized_utf16_up_to(text) == text.len()
2193 }
2194
2195 /// Normalize a slice of potentially-invalid UTF-8 into a `Cow<'a, str>`.
2196 ///
2197 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
2198 /// according to the WHATWG Encoding Standard.
2199 ///
2200 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
2201 #[cfg(feature = "utf8_iter")]
2202 pub fn normalize_utf8<'a>(&self, text: &'a [u8]) -> Cow<'a, str> {
2203 let (head, tail) = self.split_normalized_utf8(text);
2204 if tail.is_empty() {
2205 return Cow::Borrowed(head);
2206 }
2207 let mut ret = String::new();
2208 ret.reserve(text.len());
2209 ret.push_str(head);
2210 let _ = self.normalize_utf8_to(tail, &mut ret);
2211 Cow::Owned(ret)
2212 }
2213
2214 /// Split a slice of potentially-invalid UTF-8 into maximum normalized (and valid)
2215 /// prefix and unnormalized suffix such that the concatenation of the prefix and the
2216 /// normalization of the suffix is the normalization of the whole input.
2217 ///
2218 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
2219 #[cfg(feature = "utf8_iter")]
2220 pub fn split_normalized_utf8<'a>(&self, text: &'a [u8]) -> (&'a str, &'a [u8]) {
2221 let up_to = self.is_normalized_utf8_up_to(text);
2222 let (head, tail) = text.split_at_checked(up_to).unwrap_or_else(|| {
2223 // Internal bug, not even GIGO, never supposed to happen
2224 debug_assert!(false);
2225 (&[], text)
2226 });
2227 // SAFETY: The normalization check also checks for
2228 // UTF-8 well-formedness.
2229 (unsafe { core::str::from_utf8_unchecked(head) }, tail)
2230 }
2231
2232 /// Return the index a slice of potentially-invalid UTF-8 is normalized up to
2233 ///
2234 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
2235 #[cfg(feature = "utf8_iter")]
2236 fn is_normalized_utf8_up_to(&self, text: &[u8]) -> usize {
2237 let mut sink = IsNormalizedSinkUtf8::new(text);
2238 let _ = self.normalize_utf8_to(text, &mut sink);
2239 text.len() - sink.remaining_len()
2240 }
2241
2242 /// Check if a slice of potentially-invalid UTF-8 is normalized.
2243 ///
2244 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
2245 /// according to the WHATWG Encoding Standard before checking.
2246 ///
2247 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
2248 #[cfg(feature = "utf8_iter")]
2249 pub fn is_normalized_utf8(&self, text: &[u8]) -> bool {
2250 self.is_normalized_utf8_up_to(text) == text.len()
2251 }
2252 };
2253}
2254
2255/// Borrowed version of a normalizer for performing decomposing normalization.
2256#[derive(Debug)]
2257pub struct DecomposingNormalizerBorrowed<'a> {
2258 decompositions: &'a DecompositionData<'a>,
2259 tables: &'a DecompositionTables<'a>,
2260 supplementary_tables: Option<&'a DecompositionTables<'a>>,
2261 decomposition_passthrough_bound: u8, // never above 0xC0
2262 composition_passthrough_bound: u16, // never above 0x0300
2263}
2264
2265impl DecomposingNormalizerBorrowed<'static> {
2266 /// Cheaply converts a [`DecomposingNormalizerBorrowed<'static>`] into a [`DecomposingNormalizer`].
2267 ///
2268 /// Note: Due to branching and indirection, using [`DecomposingNormalizer`] might inhibit some
2269 /// compile-time optimizations that are possible with [`DecomposingNormalizerBorrowed`].
2270 pub const fn static_to_owned(self) -> DecomposingNormalizer {
2271 DecomposingNormalizer {
2272 decompositions: DataPayload::from_static_ref(self.decompositions),
2273 tables: DataPayload::from_static_ref(self.tables),
2274 supplementary_tables: if let Some(s) = self.supplementary_tables {
2275 // `map` not available in const context
2276 Some(DataPayload::from_static_ref(s))
2277 } else {
2278 None
2279 },
2280 decomposition_passthrough_bound: self.decomposition_passthrough_bound,
2281 composition_passthrough_bound: self.composition_passthrough_bound,
2282 }
2283 }
2284
2285 /// NFD constructor using compiled data.
2286 ///
2287 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2288 ///
2289 /// [📚 Help choosing a constructor](icu_provider::constructors)
2290 #[cfg(feature = "compiled_data")]
2291 pub const fn new_nfd() -> Self {
2292 const _: () = assert!(
2293 provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
2294 .scalars16
2295 .const_len()
2296 + provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
2297 .scalars24
2298 .const_len()
2299 <= 0xFFF,
2300 "future extension"
2301 );
2302
2303 DecomposingNormalizerBorrowed {
2304 decompositions: provider::Baked::SINGLETON_NORMALIZER_NFD_DATA_V1,
2305 tables: provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
2306 supplementary_tables: None,
2307 decomposition_passthrough_bound: 0xC0,
2308 composition_passthrough_bound: 0x0300,
2309 }
2310 }
2311
2312 /// NFKD constructor using compiled data.
2313 ///
2314 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2315 ///
2316 /// [📚 Help choosing a constructor](icu_provider::constructors)
2317 #[cfg(feature = "compiled_data")]
2318 pub const fn new_nfkd() -> Self {
2319 const _: () = assert!(
2320 provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
2321 .scalars16
2322 .const_len()
2323 + provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
2324 .scalars24
2325 .const_len()
2326 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
2327 .scalars16
2328 .const_len()
2329 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
2330 .scalars24
2331 .const_len()
2332 <= 0xFFF,
2333 "future extension"
2334 );
2335
2336 // TODO: Perhaps hard-code these?
2337
2338 const _: () = assert!(
2339 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap <= 0x0300,
2340 "invalid"
2341 );
2342
2343 const _: () = assert!(
2344 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap >= 0x80,
2345 "invalid"
2346 );
2347
2348 let decomposition_capped =
2349 if provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap < 0xC0 {
2350 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap
2351 } else {
2352 0xC0
2353 };
2354 let composition_capped =
2355 if provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap < 0x0300 {
2356 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap
2357 } else {
2358 0x0300
2359 };
2360
2361 DecomposingNormalizerBorrowed {
2362 decompositions: provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1,
2363 tables: provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
2364 supplementary_tables: Some(provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1),
2365 decomposition_passthrough_bound: decomposition_capped as u8,
2366 composition_passthrough_bound: composition_capped,
2367 }
2368 }
2369
2370 #[cfg(feature = "compiled_data")]
2371 pub(crate) const fn new_uts46_decomposed() -> Self {
2372 const _: () = assert!(
2373 provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
2374 .scalars16
2375 .const_len()
2376 + provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
2377 .scalars24
2378 .const_len()
2379 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
2380 .scalars16
2381 .const_len()
2382 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
2383 .scalars24
2384 .const_len()
2385 <= 0xFFF,
2386 "future extension"
2387 );
2388
2389 const _: () = assert!(
2390 provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap <= 0x0300,
2391 "invalid"
2392 );
2393
2394 // Is less than 0x80!
2395
2396 let decomposition_capped =
2397 if provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap < 0xC0 {
2398 provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap
2399 } else {
2400 0xC0
2401 };
2402 let composition_capped =
2403 if provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap < 0x0300 {
2404 provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap
2405 } else {
2406 0x0300
2407 };
2408
2409 DecomposingNormalizerBorrowed {
2410 decompositions: provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1,
2411 tables: provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
2412 supplementary_tables: Some(provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1),
2413 decomposition_passthrough_bound: decomposition_capped as u8,
2414 composition_passthrough_bound: composition_capped,
2415 }
2416 }
2417}
2418
2419impl<'data> DecomposingNormalizerBorrowed<'data> {
2420 /// NFD constructor using already-loaded data.
2421 ///
2422 /// This constructor is intended for use by collations.
2423 ///
2424 /// [📚 Help choosing a constructor](icu_provider::constructors)
2425 #[doc(hidden)]
2426 pub fn new_with_data(
2427 decompositions: &'data DecompositionData<'data>,
2428 tables: &'data DecompositionTables<'data>,
2429 ) -> Self {
2430 Self {
2431 decompositions,
2432 tables,
2433 supplementary_tables: None,
2434 decomposition_passthrough_bound: 0xC0,
2435 composition_passthrough_bound: 0x0300,
2436 }
2437 }
2438
2439 /// Wraps a delegate iterator into a decomposing iterator
2440 /// adapter by using the data already held by this normalizer.
2441 #[inline]
2442 pub fn normalize_iter<I: Iterator<Item = char>>(&self, iter: I) -> Decomposition<'data, I> {
2443 let mut ret = Decomposition {
2444 inner: self.normalize_iter_private(CharIterWithTrie::new(iter, self.trie())),
2445 };
2446 ret.inner.init(); // Discard the U+0000.
2447 ret
2448 }
2449
2450 /// There's an extra U+FFFD at the start. The caller must deal with it.
2451 #[inline(always)]
2452 fn normalize_iter_private<
2453 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
2454 T: AbstractCodePointTrie<'data, u32> + 'data,
2455 P: IteratorPolicy,
2456 >(
2457 &self,
2458 iter: I,
2459 ) -> DecompositionInner<'data, I, T, P> {
2460 DecompositionInner::new_with_supplements(iter, self.tables, self.supplementary_tables)
2461 }
2462
2463 fn trie<T: AbstractCodePointTrie<'data, u32>>(&self) -> &'data T
2464 where
2465 &'data T: TryFrom<&'data CodePointTrie<'data, u32>>,
2466 {
2467 <&T>::try_from(&self.decompositions.trie)
2468 .unwrap_or_else(|_| unreachable!("Incompatible data"))
2469 }
2470
2471 normalizer_methods!();
2472
2473 decomposing_normalize_to!(
2474 /// Normalize a string slice into a `Write` sink.
2475 ,
2476 normalize_to,
2477 core::fmt::Write,
2478 &str,
2479 {
2480 },
2481 as_str,
2482 {
2483 'fast: loop {
2484 if let Some((mut upcoming, mut trie_val)) = decomposition.delegate.next() {
2485 if starter_and_decomposes_to_self_impl(trie_val) {
2486 continue 'fast;
2487 }
2488
2489 // Try to handle a single combining mark followed by a starter in a way
2490 // that avoids `decomposition.buffer`.
2491
2492 if likely(trie_value_indicates_non_decomposing_non_starter(trie_val)) {
2493 // This loop is only broken out of as goto forward.
2494 #[expect(clippy::never_loop)]
2495 loop {
2496 if let Some((after_mark, after_mark_trie_value)) = decomposition.delegate.next() {
2497 if likely(starter_and_decomposes_to_self_impl(after_mark_trie_value)) {
2498 continue 'fast;
2499 }
2500 if likely(!decomposition_starts_with_non_starter(after_mark_trie_value)) {
2501 // We have a decomposing starter.
2502 upcoming = after_mark;
2503 trie_val = after_mark_trie_value;
2504 break;
2505 }
2506 // We have another combining mark.
2507 // We put the first combining mark, which we know doesn't decompose,
2508 // directly into the buffer. We put the second one, which might decompose,
2509 // into `decomposition.pending` for `gather_and_sort_combining` to deal
2510 // with.
2511
2512 let consumed_so_far_slice = &pending_slice[..pending_slice.len()
2513 - decomposition.delegate.as_str().len()
2514 - upcoming.len_utf8()
2515 - after_mark.len_utf8()];
2516 sink.write_str(consumed_so_far_slice)?;
2517
2518 debug_assert!(decomposition.buffer.is_empty());
2519
2520 // Narrowing `trie_value` to `u8` is OK, because we already checked
2521 // `decomposition_starts_with_non_starter`.
2522 debug_assert!(trie_value_has_ccc(trie_val));
2523 decomposition.buffer.push(CharacterAndClass::new(upcoming, CanonicalCombiningClass::from_icu4c_value(trie_val as u8)));
2524
2525 decomposition.pending = Some(CharacterAndTrieValue::new(after_mark, after_mark_trie_value));
2526 decomposition.gather_and_sort_combining(0);
2527 continue 'outer;
2528 }
2529 // End of stream
2530 sink.write_str(pending_slice)?;
2531 return Ok(());
2532 }
2533 }
2534 // End skipping over single combining mark
2535
2536 let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_val);
2537 let consumed_so_far_slice = &pending_slice[..pending_slice.len()
2538 - decomposition.delegate.as_str().len()
2539 - upcoming.len_utf8()];
2540 sink.write_str(consumed_so_far_slice)?;
2541
2542 // Now let's figure out if we got a starter or a non-starter.
2543 if decomposition_starts_with_non_starter(
2544 trie_val,
2545 ) {
2546 // Let this trie value to be reprocessed in case it is
2547 // one of the rare decomposing ones.
2548 decomposition.pending = Some(upcoming_with_trie_value);
2549 decomposition.gather_and_sort_combining(0);
2550 continue 'outer;
2551 }
2552 undecomposed_starter = upcoming_with_trie_value;
2553 debug_assert!(decomposition.pending.is_none());
2554 break 'fast;
2555 }
2556 // End of stream
2557 sink.write_str(pending_slice)?;
2558 return Ok(());
2559 }
2560 },
2561 text,
2562 sink,
2563 decomposition,
2564 undecomposed_starter,
2565 pending_slice,
2566 'outer,
2567 self,
2568 chars_with_trie_default_for_ascii,
2569 );
2570
2571 decomposing_normalize_to!(
2572 /// Normalize a slice of potentially-invalid UTF-8 into a `Write` sink.
2573 ///
2574 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
2575 /// according to the WHATWG Encoding Standard.
2576 ///
2577 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
2578 #[cfg(feature = "utf8_iter")]
2579 ,
2580 normalize_utf8_to,
2581 core::fmt::Write,
2582 &[u8],
2583 {
2584 },
2585 as_slice,
2586 {
2587 'fast: loop {
2588 if let Some((mut upcoming, mut trie_val)) = decomposition.delegate.next() {
2589 if starter_and_decomposes_to_self_except_replacement(trie_val) {
2590 // Note: The trie value of the REPLACEMENT CHARACTER is
2591 // intentionally formatted to fail the
2592 // `starter_and_decomposes_to_self` test even though it
2593 // really is a starter that decomposes to self. This
2594 // Allows moving the branch on REPLACEMENT CHARACTER
2595 // below this `continue`.
2596 continue 'fast;
2597 }
2598
2599 // Try to handle a single combining mark followed by a starter in a way
2600 // that avoids `decomposition.buffer`.
2601
2602 if likely(trie_value_indicates_non_decomposing_non_starter(trie_val)) {
2603 // This loop is only broken out of as goto forward.
2604 #[expect(clippy::never_loop)]
2605 loop {
2606 if let Some((after_mark, after_mark_trie_value)) = decomposition.delegate.next() {
2607 if likely(starter_and_decomposes_to_self_except_replacement(after_mark_trie_value)) {
2608 continue 'fast;
2609 }
2610 if likely(!decomposition_starts_with_non_starter(after_mark_trie_value)) {
2611 // We have a decomposing starter.
2612 upcoming = after_mark;
2613 trie_val = after_mark_trie_value;
2614 break;
2615 }
2616 // We have another combining mark.
2617 // We put the first combining mark, which we know doesn't decompose,
2618 // directly into the buffer. We put the second one, which might decompose,
2619 // into `decomposition.pending` for `gather_and_sort_combining` to deal
2620 // with.
2621
2622 // `len_utf8` is OK, since knowing that we have two combining marks
2623 // means that neither is U+FFFD, so we didn't have a UTF-8 error.
2624 debug_assert_ne!(upcoming, '\u{FFFD}');
2625 debug_assert_ne!(after_mark, '\u{FFFD}');
2626 #[expect(clippy::indexing_slicing)]
2627 let consumed_so_far_slice = &pending_slice[..pending_slice.len()
2628 - decomposition.delegate.as_slice().len()
2629 - upcoming.len_utf8()
2630 - after_mark.len_utf8()];
2631 sink.write_str(unsafe { core::str::from_utf8_unchecked(consumed_so_far_slice) } )?;
2632
2633 debug_assert!(decomposition.buffer.is_empty());
2634
2635 // Narrowing `trie_value` to `u8` is OK, because we already checked
2636 // `decomposition_starts_with_non_starter`.
2637 debug_assert!(trie_value_has_ccc(trie_val));
2638 decomposition.buffer.push(CharacterAndClass::new(upcoming, CanonicalCombiningClass::from_icu4c_value(trie_val as u8)));
2639
2640 decomposition.pending = Some(CharacterAndTrieValue::new(after_mark, after_mark_trie_value));
2641 decomposition.gather_and_sort_combining(0);
2642 continue 'outer;
2643 }
2644 // End of stream
2645 sink.write_str(unsafe { core::str::from_utf8_unchecked(pending_slice) } )?;
2646 return Ok(());
2647 }
2648 }
2649 // End skipping over single combining mark
2650
2651 let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_val);
2652 if unlikely(upcoming == REPLACEMENT_CHARACTER) {
2653 // We might have an error, so fall out of the fast path.
2654
2655 // Since the U+FFFD might signify an error, we can't
2656 // assume `upcoming.len_utf8()` for the backoff length.
2657 #[expect(clippy::indexing_slicing)]
2658 let mut consumed_so_far = pending_slice[..pending_slice.len() - decomposition.delegate.as_slice().len()].chars();
2659 let back = consumed_so_far.next_back();
2660 debug_assert_eq!(back, Some(REPLACEMENT_CHARACTER));
2661 let consumed_so_far_slice = consumed_so_far.as_slice();
2662 sink.write_str(unsafe { core::str::from_utf8_unchecked(consumed_so_far_slice) } )?;
2663
2664 // We could call `gather_and_sort_combining` here and
2665 // `continue 'outer`, but this should be better for code
2666 // size.
2667 undecomposed_starter = upcoming_with_trie_value;
2668 debug_assert!(decomposition.pending.is_none());
2669 break 'fast;
2670 }
2671
2672 #[expect(clippy::indexing_slicing)]
2673 let consumed_so_far_slice = &pending_slice[..pending_slice.len()
2674 - decomposition.delegate.as_slice().len()
2675 - upcoming.len_utf8()];
2676 sink.write_str(unsafe { core::str::from_utf8_unchecked(consumed_so_far_slice) } )?;
2677
2678 // Now let's figure out if we got a starter or a non-starter.
2679 if decomposition_starts_with_non_starter(
2680 upcoming_with_trie_value.trie_val,
2681 ) {
2682 // Let this trie value to be reprocessed in case it is
2683 // one of the rare decomposing ones.
2684 decomposition.pending = Some(upcoming_with_trie_value);
2685 decomposition.gather_and_sort_combining(0);
2686 continue 'outer;
2687 }
2688 undecomposed_starter = upcoming_with_trie_value;
2689 debug_assert!(decomposition.pending.is_none());
2690 break 'fast;
2691 }
2692 // End of stream
2693 sink.write_str(unsafe { core::str::from_utf8_unchecked(pending_slice) } )?;
2694 return Ok(());
2695 }
2696 },
2697 text,
2698 sink,
2699 decomposition,
2700 undecomposed_starter,
2701 pending_slice,
2702 'outer,
2703 self,
2704 chars_with_trie_default_for_ascii,
2705 );
2706
2707 decomposing_normalize_to!(
2708 /// Normalize a slice of potentially-invalid UTF-16 into a `Write16` sink.
2709 ///
2710 /// Unpaired surrogates are mapped to the REPLACEMENT CHARACTER
2711 /// before normalizing.
2712 ///
2713 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2714 #[cfg(feature = "utf16_iter")]
2715 ,
2716 normalize_utf16_to,
2717 write16::Write16,
2718 &[u16],
2719 {
2720 sink.size_hint(text.len())?;
2721 },
2722 as_slice,
2723 {
2724 // This loop is only broken out of as goto forward and only as release-build recovery from
2725 // detecting an internal bug without panic. (In debug builds, internal bugs panic instead.)
2726 #[expect(clippy::never_loop)]
2727 'fastwrap: loop {
2728 // Commented out `code_unit_iter` and used `ptr` and `end` to
2729 // work around https://github.com/rust-lang/rust/issues/144684 .
2730 //
2731 // let mut code_unit_iter = decomposition.delegate.as_slice().iter();
2732 let delegate_as_slice = decomposition.delegate.as_slice();
2733 let mut ptr: *const u16 = delegate_as_slice.as_ptr();
2734 // SAFETY: materializing a pointer immediately past the end of an
2735 // allocation is OK.
2736 let end: *const u16 = unsafe { ptr.add(delegate_as_slice.len()) };
2737 let decomposition_passthrough_bound = u16::from(self.decomposition_passthrough_bound);
2738 'fast: loop {
2739 // if let Some(&upcoming_code_unit) = code_unit_iter.next() {
2740 if likely(ptr != end) {
2741 // SAFETY: We just checked that `ptr` has not reached `end`.
2742 // `ptr` always advances by one, and we always have a check
2743 // per advancement.
2744 let upcoming_code_unit = unsafe { *ptr };
2745 // SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2746 // by one points to the same allocation or to immediately
2747 // after, which is OK.
2748 ptr = unsafe { ptr.add(1) };
2749
2750 // The performance of what logically is supposed to be this
2751 // branch is _incredibly_ brittle and what LLVM ends up doing
2752 // that affects the performance of what's logically about this
2753 // decision can swing to double/halve the throughput for Basic
2754 // Latin in ways that are completely unintuitive. Basically _any_
2755 // change to _any_ code that participates in how LLVM sees the
2756 // code around here can make the perf fall over. In seems that
2757 // manually annotating this branch as likely has worse effects
2758 // on non-Basic-Latin input that the case where LLVM just happens to
2759 // do the right thing.
2760 //
2761 // What happens with this branch may depend on what sink type
2762 // this code is monomorphized over.
2763 //
2764 // What a terrible sink of developer time!
2765 if upcoming_code_unit < decomposition_passthrough_bound {
2766 continue 'fast;
2767 }
2768 // We might be doing a trie lookup by surrogate. Surrogates get
2769 // a decomposition to U+FFFD.
2770 let mut trie_value = decomposition.delegate.trie().bmp(upcoming_code_unit);
2771 if likely(starter_and_decomposes_to_self_impl(trie_value)) {
2772 continue 'fast;
2773 }
2774
2775 let mut upcoming32 = u32::from(upcoming_code_unit);
2776
2777 // We might now be looking at a surrogate.
2778 // The loop is only broken out of as goto forward
2779 #[expect(clippy::never_loop)]
2780 'surrogateloop: loop {
2781
2782 // Try to handle a single BMP combining mark followed by a starter in a way
2783 // that avoids `decomposition.buffer`. Crucial for perf competitiveness with ICU4C.
2784
2785 if likely(trie_value_indicates_non_decomposing_non_starter(trie_value)) {
2786 if likely(ptr != end) {
2787 // SAFETY: We just checked that `ptr` has not reached `end`.
2788 // `ptr` always advances by one, and we always have a check
2789 // per advancement.
2790 let after_mark_code_unit = unsafe { *ptr };
2791 // SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2792 // by one points to the same allocation or to immediately
2793 // after, which is OK.
2794 ptr = unsafe { ptr.add(1) };
2795 let after_mark_trie_value = decomposition.delegate.trie().bmp(after_mark_code_unit);
2796 if likely(starter_and_decomposes_to_self_impl(after_mark_trie_value)) {
2797 continue 'fast;
2798 }
2799 if unlikely(in_inclusive_range16(after_mark_code_unit, 0xD800, 0xDFFF)) {
2800 // We have a surrogate. Too complicated to deal with, because
2801 // it might be the first half of a combining mark.
2802 // Pretend we didn't see it.
2803
2804 // SAFETY: We just incremented `ptr`, so decrementing it
2805 // has to stay within the allocation.
2806 ptr = unsafe { ptr.sub(1) };
2807 break 'surrogateloop;
2808 }
2809 if likely(!decomposition_starts_with_non_starter(after_mark_trie_value)) {
2810 // We have a decomposing starter.
2811
2812 // No need to sync `upcoming_code_unit`, since nothing reads it below.
2813 upcoming32 = u32::from(after_mark_code_unit);
2814 trie_value = after_mark_trie_value;
2815 break 'surrogateloop;
2816 }
2817 // We have another combining mark.
2818 // We put the first combining mark, which we know doesn't decompose,
2819 // directly into the buffer. We put the second one, which might decompose,
2820 // into `decomposition.pending` for `gather_and_sort_combining` to deal
2821 // with.
2822
2823 let Some(consumed_so_far_slice) = pending_slice.get(..pending_slice.len() -
2824 // code_unit_iter.as_slice().len()
2825 // SAFETY: `ptr` and `end` have been derived from the same allocation
2826 // and `ptr` is never greater than `end`.
2827 unsafe { end.offset_from(ptr) as usize }
2828 - 2) else {
2829 // If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
2830 debug_assert!(false);
2831 // Throw away the results of the fast path.
2832 break 'fastwrap;
2833 };
2834 sink.write_slice(consumed_so_far_slice)?;
2835
2836 // Our belief that `upcoming32` is not a surrogate is based on trie data,
2837 // which might be GIGO.
2838 let upcoming = char_from_u32(upcoming32);
2839
2840 debug_assert!(decomposition.buffer.is_empty());
2841
2842 // Narrowing `trie_value` to `u8` is OK, because we already checked
2843 // `decomposition_starts_with_non_starter`.
2844 debug_assert!(trie_value_has_ccc(trie_value));
2845 decomposition.buffer.push(CharacterAndClass::new(upcoming, CanonicalCombiningClass::from_icu4c_value(trie_value as u8)));
2846
2847 // Sync with main iterator
2848 // SAFETY: `ptr` and `end` have been derived from the same allocation
2849 // and `ptr` is never greater than `end`.
2850 decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars_with_trie(decomposition.delegate.trie());
2851 // Let this trie value to be reprocessed in case it is
2852 // one of the rare decomposing ones.
2853 // SAFETY: We checked above that we don't have surrogate.
2854 let after_mark_char = unsafe { char::from_u32_unchecked(u32::from(after_mark_code_unit))};
2855 decomposition.pending = Some(CharacterAndTrieValue::new(after_mark_char, after_mark_trie_value));
2856 decomposition.gather_and_sort_combining(0);
2857 continue 'outer;
2858 }
2859 // End of stream
2860 sink.write_slice(pending_slice)?;
2861 return Ok(());
2862 }
2863
2864 // End skipping over single combining mark
2865
2866 // LLVM's optimizations are incredibly brittle for the code _above_,
2867 // and using `likely` _below_ without using it _above_ helps!
2868 // What a massive sink of developer time!
2869 // Seriously, the effect of these annotations is massively
2870 // unintuitive. Measure everything!
2871 // Notably, the `if likely(...)` formulation optimizes differently
2872 // than just putting `cold_path()` on the `else` path!
2873 let surrogate_base = upcoming32.wrapping_sub(0xD800);
2874 if likely(surrogate_base > (0xDFFF - 0xD800)) {
2875 // Not surrogate
2876 break 'surrogateloop;
2877 }
2878 if likely(surrogate_base <= (0xDBFF - 0xD800)) {
2879 // let iter_backup = code_unit_iter.clone();
2880 // if let Some(&low) = code_unit_iter.next() {
2881 if likely(ptr != end) {
2882 // SAFETY: We just checked that `ptr` has not reached `end`.
2883 // `ptr` always advances by one, and we always have a check
2884 // per advancement.
2885 let low = unsafe { *ptr };
2886 if likely(in_inclusive_range16(low, 0xDC00, 0xDFFF)) {
2887 // SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2888 // by one points to the same allocation or to immediately
2889 // after, which is OK.
2890 ptr = unsafe { ptr.add(1) };
2891
2892 upcoming32 = (upcoming32 << 10) + u32::from(low)
2893 - (((0xD800u32 << 10) - 0x10000u32) + 0xDC00u32);
2894 // Successfully-paired surrogate. Read from the trie again.
2895 trie_value = {
2896 // Semantically, this bit of conditional compilation makes no sense.
2897 // The purpose is to keep LLVM seeing the untyped trie case the way
2898 // it did before so as not to regress the performance of the untyped
2899 // case due to unintuitive optimizer effects. If you care about the
2900 // perf of the untyped trie case and have better ideas, please try
2901 // something better.
2902 #[cfg(feature = "serde")]
2903 {decomposition.delegate.trie().code_point(upcoming32)}
2904 #[cfg(not(feature = "serde"))]
2905 {decomposition.delegate.trie().supplementary(upcoming32)}
2906 };
2907 if likely(starter_and_decomposes_to_self_impl(trie_value)) {
2908 continue 'fast;
2909 }
2910 break 'surrogateloop;
2911 // } else {
2912 // code_unit_iter = iter_backup;
2913 }
2914 }
2915 }
2916 // unpaired surrogate
2917 upcoming32 = 0xFFFD; // Safe value for `char::from_u32_unchecked` and matches later potential error check.
2918 // trie_value already holds a decomposition to U+FFFD.
2919 break 'surrogateloop;
2920 }
2921
2922 let upcoming = unsafe { char::from_u32_unchecked(upcoming32) };
2923 let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_value);
2924
2925
2926 let Some(consumed_so_far_slice) = pending_slice.get(..pending_slice.len() -
2927 // code_unit_iter.as_slice().len()
2928 // SAFETY: `ptr` and `end` have been derived from the same allocation
2929 // and `ptr` is never greater than `end`.
2930 unsafe { end.offset_from(ptr) as usize }
2931 - upcoming.len_utf16()) else {
2932 // If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
2933 debug_assert!(false);
2934 // Throw away the results of the fast path.
2935 break 'fastwrap;
2936 };
2937 sink.write_slice(consumed_so_far_slice)?;
2938
2939 if decomposition_starts_with_non_starter(
2940 upcoming_with_trie_value.trie_val,
2941 ) {
2942 // Sync with main iterator
2943 // decomposition.delegate = code_unit_iter.as_slice().chars();
2944 // SAFETY: `ptr` and `end` have been derived from the same allocation
2945 // and `ptr` is never greater than `end`.
2946 decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars_with_trie(decomposition.delegate.trie());
2947 // Let this trie value to be reprocessed in case it is
2948 // one of the rare decomposing ones.
2949 decomposition.pending = Some(upcoming_with_trie_value);
2950 decomposition.gather_and_sort_combining(0);
2951 continue 'outer;
2952 }
2953 undecomposed_starter = upcoming_with_trie_value;
2954 debug_assert!(decomposition.pending.is_none());
2955 break 'fast;
2956 }
2957 // End of stream
2958 sink.write_slice(pending_slice)?;
2959 return Ok(());
2960 }
2961 // Sync the main iterator
2962 // decomposition.delegate = code_unit_iter.as_slice().chars();
2963 // SAFETY: `ptr` and `end` have been derived from the same allocation
2964 // and `ptr` is never greater than `end`.
2965 decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars_with_trie(decomposition.delegate.trie());
2966 break 'fastwrap;
2967 }
2968 },
2969 text,
2970 sink,
2971 decomposition,
2972 undecomposed_starter,
2973 pending_slice,
2974 'outer,
2975 self,
2976 chars_with_trie,
2977 );
2978}
2979
2980/// A normalizer for performing decomposing normalization.
2981#[derive(Debug)]
2982pub struct DecomposingNormalizer {
2983 decompositions: DataPayload<NormalizerNfdDataV1>,
2984 tables: DataPayload<NormalizerNfdTablesV1>,
2985 supplementary_tables: Option<DataPayload<NormalizerNfkdTablesV1>>,
2986 decomposition_passthrough_bound: u8, // never above 0xC0
2987 composition_passthrough_bound: u16, // never above 0x0300
2988}
2989
2990impl DecomposingNormalizer {
2991 /// Constructs a borrowed version of this type for more efficient querying.
2992 pub fn as_borrowed(&self) -> DecomposingNormalizerBorrowed<'_> {
2993 DecomposingNormalizerBorrowed {
2994 decompositions: self.decompositions.get(),
2995 tables: self.tables.get(),
2996 supplementary_tables: self.supplementary_tables.as_ref().map(|s| s.get()),
2997 decomposition_passthrough_bound: self.decomposition_passthrough_bound,
2998 composition_passthrough_bound: self.composition_passthrough_bound,
2999 }
3000 }
3001
3002 /// NFD constructor using compiled data.
3003 ///
3004 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
3005 ///
3006 /// [📚 Help choosing a constructor](icu_provider::constructors)
3007 #[cfg(feature = "compiled_data")]
3008 pub const fn new_nfd() -> DecomposingNormalizerBorrowed<'static> {
3009 DecomposingNormalizerBorrowed::new_nfd()
3010 }
3011
3012 icu_provider::gen_buffer_data_constructors!(
3013 () -> error: DataError,
3014 functions: [
3015 new_nfd: skip,
3016 try_new_nfd_with_buffer_provider,
3017 try_new_nfd_unstable,
3018 Self,
3019 ]
3020 );
3021
3022 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfd)]
3023 pub fn try_new_nfd_unstable<D>(provider: &D) -> Result<Self, DataError>
3024 where
3025 D: DataProvider<NormalizerNfdDataV1> + DataProvider<NormalizerNfdTablesV1> + ?Sized,
3026 {
3027 let decompositions: DataPayload<NormalizerNfdDataV1> =
3028 provider.load(Default::default())?.payload;
3029 let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
3030
3031 if tables.get().scalars16.len() + tables.get().scalars24.len() > 0xFFF {
3032 // The data is from a future where there exists a normalization flavor whose
3033 // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
3034 // of space. If a good use case from such a decomposition flavor arises, we can
3035 // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
3036 // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
3037 // since for now the masks are hard-coded, error out.
3038 return Err(
3039 DataError::custom("future extension").with_marker(NormalizerNfdTablesV1::INFO)
3040 );
3041 }
3042
3043 let cap = decompositions.get().passthrough_cap;
3044 if cap > 0x0300 {
3045 return Err(DataError::custom("invalid").with_marker(NormalizerNfdDataV1::INFO));
3046 }
3047 if cap < 0x80 {
3048 return Err(DataError::custom("invalid").with_marker(NormalizerNfdDataV1::INFO));
3049 }
3050 let decomposition_capped = cap.min(0xC0);
3051 let composition_capped = cap.min(0x0300);
3052
3053 Ok(DecomposingNormalizer {
3054 decompositions,
3055 tables,
3056 supplementary_tables: None,
3057 decomposition_passthrough_bound: decomposition_capped as u8,
3058 composition_passthrough_bound: composition_capped,
3059 })
3060 }
3061
3062 icu_provider::gen_buffer_data_constructors!(
3063 () -> error: DataError,
3064 functions: [
3065 new_nfkd: skip,
3066 try_new_nfkd_with_buffer_provider,
3067 try_new_nfkd_unstable,
3068 Self,
3069 ]
3070 );
3071
3072 /// NFKD constructor using compiled data.
3073 ///
3074 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
3075 ///
3076 /// [📚 Help choosing a constructor](icu_provider::constructors)
3077 #[cfg(feature = "compiled_data")]
3078 pub const fn new_nfkd() -> DecomposingNormalizerBorrowed<'static> {
3079 DecomposingNormalizerBorrowed::new_nfkd()
3080 }
3081
3082 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfkd)]
3083 pub fn try_new_nfkd_unstable<D>(provider: &D) -> Result<Self, DataError>
3084 where
3085 D: DataProvider<NormalizerNfkdDataV1>
3086 + DataProvider<NormalizerNfdTablesV1>
3087 + DataProvider<NormalizerNfkdTablesV1>
3088 + ?Sized,
3089 {
3090 let decompositions: DataPayload<NormalizerNfkdDataV1> =
3091 provider.load(Default::default())?.payload;
3092 let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
3093 let supplementary_tables: DataPayload<NormalizerNfkdTablesV1> =
3094 provider.load(Default::default())?.payload;
3095
3096 if tables.get().scalars16.len()
3097 + tables.get().scalars24.len()
3098 + supplementary_tables.get().scalars16.len()
3099 + supplementary_tables.get().scalars24.len()
3100 > 0xFFF
3101 {
3102 // The data is from a future where there exists a normalization flavor whose
3103 // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
3104 // of space. If a good use case from such a decomposition flavor arises, we can
3105 // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
3106 // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
3107 // since for now the masks are hard-coded, error out.
3108 return Err(
3109 DataError::custom("future extension").with_marker(NormalizerNfdTablesV1::INFO)
3110 );
3111 }
3112
3113 let cap = decompositions.get().passthrough_cap;
3114 if cap > 0x0300 {
3115 return Err(DataError::custom("invalid").with_marker(NormalizerNfkdDataV1::INFO));
3116 }
3117 if cap < 0x80 {
3118 return Err(DataError::custom("invalid").with_marker(NormalizerNfdDataV1::INFO));
3119 }
3120 let decomposition_capped = cap.min(0xC0);
3121 let composition_capped = cap.min(0x0300);
3122
3123 Ok(DecomposingNormalizer {
3124 decompositions: decompositions.cast(),
3125 tables,
3126 supplementary_tables: Some(supplementary_tables),
3127 decomposition_passthrough_bound: decomposition_capped as u8,
3128 composition_passthrough_bound: composition_capped,
3129 })
3130 }
3131
3132 /// UTS 46 decomposed constructor (testing only)
3133 ///
3134 /// This is a special building block normalization for IDNA. It is the decomposed counterpart of
3135 /// ICU4C's UTS 46 normalization with two exceptions: characters that UTS 46 disallows and
3136 /// ICU4C maps to U+FFFD and characters that UTS 46 maps to the empty string normalize as in
3137 /// NFD in this normalization. In both cases, the previous UTS 46 processing before using
3138 /// normalization is expected to deal with these characters. Making the disallowed characters
3139 /// behave like this is beneficial to data size, and this normalizer implementation cannot
3140 /// deal with a character normalizing to the empty string, which doesn't happen in NFD or
3141 /// NFKD as of Unicode 14.
3142 ///
3143 /// Warning: In this normalization, U+0345 COMBINING GREEK YPOGEGRAMMENI exhibits a behavior
3144 /// that no character in Unicode exhibits in NFD, NFKD, NFC, or NFKC: Case folding turns
3145 /// U+0345 from a reordered character into a non-reordered character before reordering happens.
3146 /// Therefore, the output of this normalization may differ for different inputs that are
3147 /// canonically equivalent with each other if they differ by how U+0345 is ordered relative
3148 /// to other reorderable characters.
3149 pub(crate) fn try_new_uts46_decomposed_unstable<D>(provider: &D) -> Result<Self, DataError>
3150 where
3151 D: DataProvider<NormalizerUts46DataV1>
3152 + DataProvider<NormalizerNfdTablesV1>
3153 + DataProvider<NormalizerNfkdTablesV1>
3154 // UTS 46 tables merged into CompatibilityDecompositionTablesV1
3155 + ?Sized,
3156 {
3157 let decompositions: DataPayload<NormalizerUts46DataV1> =
3158 provider.load(Default::default())?.payload;
3159 let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
3160 let supplementary_tables: DataPayload<NormalizerNfkdTablesV1> =
3161 provider.load(Default::default())?.payload;
3162
3163 if tables.get().scalars16.len()
3164 + tables.get().scalars24.len()
3165 + supplementary_tables.get().scalars16.len()
3166 + supplementary_tables.get().scalars24.len()
3167 > 0xFFF
3168 {
3169 // The data is from a future where there exists a normalization flavor whose
3170 // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
3171 // of space. If a good use case from such a decomposition flavor arises, we can
3172 // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
3173 // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
3174 // since for now the masks are hard-coded, error out.
3175 return Err(
3176 DataError::custom("future extension").with_marker(NormalizerNfdTablesV1::INFO)
3177 );
3178 }
3179
3180 let cap = decompositions.get().passthrough_cap;
3181 if cap > 0x0300 {
3182 return Err(DataError::custom("invalid").with_marker(NormalizerUts46DataV1::INFO));
3183 }
3184 // Can be below 0x80!
3185 let decomposition_capped = cap.min(0xC0);
3186 let composition_capped = cap.min(0x0300);
3187
3188 Ok(DecomposingNormalizer {
3189 decompositions: decompositions.cast(),
3190 tables,
3191 supplementary_tables: Some(supplementary_tables),
3192 decomposition_passthrough_bound: decomposition_capped as u8,
3193 composition_passthrough_bound: composition_capped,
3194 })
3195 }
3196}
3197
3198/// Borrowed version of a normalizer for performing composing normalization.
3199#[derive(Debug)]
3200pub struct ComposingNormalizerBorrowed<'a> {
3201 decomposing_normalizer: DecomposingNormalizerBorrowed<'a>,
3202 canonical_compositions: CanonicalCompositionsBorrowed<'a>,
3203}
3204
3205impl ComposingNormalizerBorrowed<'static> {
3206 /// Cheaply converts a [`ComposingNormalizerBorrowed<'static>`] into a [`ComposingNormalizer`].
3207 ///
3208 /// Note: Due to branching and indirection, using [`ComposingNormalizer`] might inhibit some
3209 /// compile-time optimizations that are possible with [`ComposingNormalizerBorrowed`].
3210 pub const fn static_to_owned(self) -> ComposingNormalizer {
3211 ComposingNormalizer {
3212 decomposing_normalizer: self.decomposing_normalizer.static_to_owned(),
3213 canonical_compositions: self.canonical_compositions.static_to_owned(),
3214 }
3215 }
3216
3217 /// NFC constructor using compiled data.
3218 ///
3219 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
3220 ///
3221 /// [📚 Help choosing a constructor](icu_provider::constructors)
3222 #[cfg(feature = "compiled_data")]
3223 pub const fn new_nfc() -> Self {
3224 ComposingNormalizerBorrowed {
3225 decomposing_normalizer: DecomposingNormalizerBorrowed::new_nfd(),
3226 canonical_compositions: CanonicalCompositionsBorrowed::Current(
3227 provider::Baked::SINGLETON_NORMALIZER_NFC_V2,
3228 ),
3229 }
3230 }
3231
3232 /// NFKC constructor using compiled data.
3233 ///
3234 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
3235 ///
3236 /// [📚 Help choosing a constructor](icu_provider::constructors)
3237 #[cfg(feature = "compiled_data")]
3238 pub const fn new_nfkc() -> Self {
3239 ComposingNormalizerBorrowed {
3240 decomposing_normalizer: DecomposingNormalizerBorrowed::new_nfkd(),
3241 canonical_compositions: CanonicalCompositionsBorrowed::Current(
3242 provider::Baked::SINGLETON_NORMALIZER_NFC_V2,
3243 ),
3244 }
3245 }
3246
3247 /// This is a special building block normalization for IDNA that implements parts of the Map
3248 /// step and the following Normalize step.
3249 ///
3250 /// Warning: In this normalization, U+0345 COMBINING GREEK YPOGEGRAMMENI exhibits a behavior
3251 /// that no character in Unicode exhibits in NFD, NFKD, NFC, or NFKC: Case folding turns
3252 /// U+0345 from a reordered character into a non-reordered character before reordering happens.
3253 /// Therefore, the output of this normalization may differ for different inputs that are
3254 /// canonically equivalents with each other if they differ by how U+0345 is ordered relative
3255 /// to other reorderable characters.
3256 #[cfg(feature = "compiled_data")]
3257 pub(crate) const fn new_uts46() -> Self {
3258 ComposingNormalizerBorrowed {
3259 decomposing_normalizer: DecomposingNormalizerBorrowed::new_uts46_decomposed(),
3260 canonical_compositions: CanonicalCompositionsBorrowed::Current(
3261 provider::Baked::SINGLETON_NORMALIZER_NFC_V2,
3262 ),
3263 }
3264 }
3265}
3266
3267impl<'data> ComposingNormalizerBorrowed<'data> {
3268 /// Wraps a delegate iterator into a composing iterator
3269 /// adapter by using the data already held by this normalizer.
3270 #[inline]
3271 pub fn normalize_iter<I: Iterator<Item = char>>(&'data self, iter: I) -> Composition<'data, I> {
3272 let mut ret = Composition {
3273 inner: self.normalize_iter_private(CharIterWithTrie::new(iter, self.trie())),
3274 };
3275 ret.inner.decomposition.init(); // Discard the U+0000.
3276 ret
3277 }
3278
3279 /// There's an extra U+FFFD at the start. The caller must deal with it.
3280 #[inline(always)]
3281 fn normalize_iter_private<
3282 I: Iterator<Item = (char, u32)> + WithTrie<'data, T, u32>,
3283 T: AbstractCodePointTrie<'data, u32> + 'data,
3284 P: IteratorPolicy,
3285 >(
3286 &'data self,
3287 iter: I,
3288 ) -> CompositionInner<'data, I, T, P> {
3289 CompositionInner::new(
3290 DecompositionInner::new_with_supplements(
3291 iter,
3292 self.decomposing_normalizer.tables,
3293 self.decomposing_normalizer.supplementary_tables,
3294 ),
3295 self.canonical_compositions.as_ref(),
3296 )
3297 }
3298
3299 fn trie<T: AbstractCodePointTrie<'data, u32>>(&self) -> &'data T
3300 where
3301 &'data T: TryFrom<&'data CodePointTrie<'data, u32>>,
3302 {
3303 self.decomposing_normalizer.trie()
3304 }
3305
3306 normalizer_methods!();
3307
3308 composing_normalize_to!(
3309 /// Normalize a string slice into a `Write` sink.
3310 ,
3311 normalize_to,
3312 core::fmt::Write,
3313 &str,
3314 {},
3315 true,
3316 as_str,
3317 {
3318 let composition_passthrough_byte_bound = if self.decomposing_normalizer.composition_passthrough_bound == 0x300 {
3319 0xCCu8
3320 } else {
3321 // We can make this fancy if a normalization other than NFC where looking at
3322 // non-ASCII lead bytes is worthwhile is ever introduced.
3323 self.decomposing_normalizer.composition_passthrough_bound.min(0x80) as u8
3324 };
3325 // Attributes have to be on blocks, so hoisting all the way here.
3326 let mut code_unit_iter = composition.decomposition.delegate.as_str().as_bytes().iter();
3327 'fast: loop {
3328 if let Some(b) = code_unit_iter.next() {
3329 let upcoming_byte = *b;
3330 if upcoming_byte < composition_passthrough_byte_bound {
3331 // Fast-track succeeded!
3332 continue 'fast;
3333 }
3334 // Begin manual inlining from `CharsWithTrie`
3335
3336 // SAFETY: Since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, we may assume that we
3337 // have a valid lead byte. We can assume that the lead byte won't be ASCII, because `composition_passthrough_byte_bound`
3338 // is never less than 0x80. Not need to check for other cases.
3339 let (upcoming, trie_val) = if upcoming_byte < 0xE0 {
3340 // Two-byte sequence.
3341 // SAFETY, since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, we may assume the
3342 // presence of a trail byte.
3343 let trail = *unsafe { code_unit_iter.next().unwrap_unchecked() };
3344 let high_five = u32::from(upcoming_byte & 0b11_111);
3345 let low_six = u32::from(trail & 0b111_111);
3346 // SAFETY: By construction, `high_five` and `low_six` conform
3347 // to the invariant of `utf8_two_byte`.
3348 let v = unsafe { composition.decomposition.delegate.trie().utf8_two_byte(high_five, low_six) };
3349 // SAFETY: Since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, `lead` must be a
3350 // valid (not overlong) two-byte lead and `trail` must be a valid
3351 // trail. Therefore, the following shift and OR stays in the
3352 // scalar value range.
3353 let c = unsafe { char::from_u32_unchecked((high_five << 6) | low_six) };
3354 (c, v)
3355 } else if upcoming_byte < 0xF0 {
3356 // Three-byte sequence.
3357 // SAFETY, since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, we may assume the
3358 // presence of two trail bytes.
3359 let second = *unsafe { code_unit_iter.next().unwrap_unchecked() };
3360 let third = *unsafe { code_unit_iter.next().unwrap_unchecked() };
3361 let high_ten = (u32::from(upcoming_byte & 0b1111) << 6) | u32::from(second & 0b111_111);
3362 let low_six = u32::from(third & 0b111_111);
3363 // SAFETY: By construction, `high_ten` and `low_six` conform
3364 // to the invariant of `utf8_three_byte`.
3365 let v = unsafe { composition.decomposition.delegate.trie().utf8_three_byte(high_ten, low_six) };
3366 // SAFETY: Since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, `lead` must be a
3367 // valid (not overlong) three-byte lead and `second` and `third`
3368 // must be valid trails. Therefore, the following shift and OR
3369 // stays in the scalar value range.
3370 let c = unsafe { char::from_u32_unchecked((high_ten << 6) | low_six) };
3371 (c, v)
3372 } else {
3373 // Four-byte sequence
3374 // SAFETY, since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, we may assume the
3375 // presence of three trail bytes.
3376 let second = *unsafe { code_unit_iter.next().unwrap_unchecked() };
3377 let third = *unsafe { code_unit_iter.next().unwrap_unchecked() };
3378 let fourth = *unsafe { code_unit_iter.next().unwrap_unchecked() };
3379 // SAFETY: Since `code_unit_iter` came from `str` and we always advance by a full UTF-8 sequence, `lead` must be a
3380 // valid (not overlong or out-of-range) four-byte lead and `second`,
3381 // `third`, and `fourth` must be valid trails. Therefore, the
3382 // following shift and OR stays in the scalar value range.
3383 let c = unsafe {
3384 char::from_u32_unchecked(
3385 (u32::from(upcoming_byte & 0b111) << 18)
3386 | (u32::from(second & 0b111_111) << 12)
3387 | (u32::from(third & 0b111_111) << 6)
3388 | u32::from(fourth & 0b111_111),
3389 )
3390 };
3391 (c, composition.decomposition.delegate.trie().supplementary(c as u32))
3392 };
3393
3394 // End manual inlining from `CharsWithTrie`
3395 if potential_passthrough_and_cannot_combine_backwards(trie_val) {
3396 continue 'fast;
3397 }
3398 // SAFETY: We've advanced `code_unit_iter` to a UTF-8 boundary.
3399 composition.decomposition.delegate = unsafe { core::str::from_utf8_unchecked(code_unit_iter.as_slice())}.chars_with_trie_default_for_ascii(composition.decomposition.delegate.trie());
3400 let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_val);
3401 // We need to fall off the fast path.
3402 composition.decomposition.pending = Some(upcoming_with_trie_value);
3403
3404 // slicing and unwrap OK, because we've just evidently read enough previously.
3405 let mut consumed_so_far = pending_slice[..pending_slice.len() - composition.decomposition.delegate.as_str().len() - upcoming.len_utf8()].chars_with_trie_default_for_ascii(composition.decomposition.delegate.trie());
3406 // Whether we could do something better than `next_back()` below is
3407 // https://github.com/unicode-org/icu4x/issues/7525
3408 // `unwrap` OK, because we've previously manage to read the previous character
3409 #[expect(clippy::unwrap_used)]
3410 let (undecomposed, undecomposed_trie_val) = consumed_so_far.next_back().unwrap();
3411 undecomposed_starter = CharacterAndTrieValue::new(undecomposed, undecomposed_trie_val);
3412 let consumed_so_far_slice = consumed_so_far.as_str();
3413 sink.write_str(consumed_so_far_slice)?;
3414 break 'fast;
3415 }
3416 // End of stream
3417 sink.write_str(pending_slice)?;
3418 return Ok(());
3419 }
3420 },
3421 text,
3422 sink,
3423 composition,
3424 undecomposed_starter,
3425 pending_slice,
3426 len_utf8,
3427 self,
3428 chars_with_trie_default_for_ascii,
3429 );
3430
3431 composing_normalize_to!(
3432 /// Normalize a slice of potentially-invalid UTF-8 into a `Write` sink.
3433 ///
3434 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
3435 /// according to the WHATWG Encoding Standard.
3436 ///
3437 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
3438 #[cfg(feature = "utf8_iter")]
3439 ,
3440 normalize_utf8_to,
3441 core::fmt::Write,
3442 &[u8],
3443 {},
3444 false,
3445 as_slice,
3446 {
3447 'fast: loop {
3448 if let Some((upcoming, trie_val)) = composition.decomposition.delegate.next() {
3449 if potential_passthrough_and_cannot_combine_backwards(trie_val) {
3450 // Note: The trie value of the REPLACEMENT CHARACTER is
3451 // intentionally formatted to fail the
3452 // `potential_passthrough_and_cannot_combine_backwards`
3453 // test even though it really is a starter that decomposes
3454 // to self and cannot combine backwards. This
3455 // Allows moving the branch on REPLACEMENT CHARACTER
3456 // below this `continue`.
3457 continue 'fast;
3458 }
3459 // We need to fall off the fast path.
3460
3461 let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_val);
3462 if unlikely(upcoming == REPLACEMENT_CHARACTER) {
3463 // Can't tell if this is an error or a literal U+FFFD in
3464 // the input. Assuming the former to be sure.
3465
3466 // Since the U+FFFD might signify an error, we can't
3467 // assume `upcoming.len_utf8()` for the backoff length.
3468 #[expect(clippy::indexing_slicing)]
3469 let mut consumed_so_far = pending_slice[..pending_slice.len() - composition.decomposition.delegate.as_slice().len()].chars();
3470 let back = consumed_so_far.next_back();
3471 debug_assert_eq!(back, Some(REPLACEMENT_CHARACTER));
3472 let consumed_so_far_slice = consumed_so_far.as_slice();
3473 sink.write_str(unsafe { core::str::from_utf8_unchecked(consumed_so_far_slice) })?;
3474 undecomposed_starter = CharacterAndTrieValue::new(REPLACEMENT_CHARACTER, 0);
3475 composition.decomposition.pending = None;
3476 break 'fast;
3477 }
3478
3479 composition.decomposition.pending = Some(upcoming_with_trie_value);
3480 // slicing and unwrap OK, because we've just evidently read enough previously.
3481 // `unwrap` OK, because we've previously manage to read the previous character
3482 #[expect(clippy::indexing_slicing)]
3483 let mut consumed_so_far = pending_slice[..pending_slice.len() - composition.decomposition.delegate.as_slice().len() - upcoming.len_utf8()].chars_with_trie_default_for_ascii(composition.decomposition.delegate.trie());
3484 #[expect(clippy::unwrap_used)]
3485 {
3486 // Whether we could do something better than `next_back()` below is
3487 // https://github.com/unicode-org/icu4x/issues/7525
3488 let (undecomposed, undecomposed_trie_val) = consumed_so_far.next_back().unwrap();
3489 undecomposed_starter = CharacterAndTrieValue::new(undecomposed, undecomposed_trie_val);
3490 }
3491 let consumed_so_far_slice = consumed_so_far.as_slice();
3492 sink.write_str(unsafe { core::str::from_utf8_unchecked(consumed_so_far_slice)})?;
3493 break 'fast;
3494 }
3495 // End of stream
3496 sink.write_str(unsafe { core::str::from_utf8_unchecked(pending_slice) })?;
3497 return Ok(());
3498 }
3499 },
3500 text,
3501 sink,
3502 composition,
3503 undecomposed_starter,
3504 pending_slice,
3505 len_utf8,
3506 self,
3507 chars_with_trie_default_for_ascii,
3508 );
3509
3510 composing_normalize_to!(
3511 /// Normalize a slice of potentially-invalid UTF-16 into a `Write16` sink.
3512 ///
3513 /// Unpaired surrogates are mapped to the REPLACEMENT CHARACTER
3514 /// before normalizing.
3515 ///
3516 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
3517 #[cfg(feature = "utf16_iter")]
3518 ,
3519 normalize_utf16_to,
3520 write16::Write16,
3521 &[u16],
3522 {
3523 sink.size_hint(text.len())?;
3524 },
3525 false,
3526 as_slice,
3527 {
3528 // This loop is only broken out of as goto forward and only as release-build recovery from
3529 // detecting an internal bug without panic. (In debug builds, internal bugs panic instead.)
3530 #[expect(clippy::never_loop)]
3531 'fastwrap: loop {
3532 // Commented out `code_unit_iter` and used `ptr` and `end` to
3533 // work around https://github.com/rust-lang/rust/issues/144684 .
3534 //
3535 // let mut code_unit_iter = composition.decomposition.delegate.as_slice().iter();
3536 let delegate_as_slice = composition.decomposition.delegate.as_slice();
3537 let mut ptr: *const u16 = delegate_as_slice.as_ptr();
3538 // SAFETY: materializing a pointer immediately past the end of an
3539 // allocation is OK.
3540 let end: *const u16 = unsafe { ptr.add(delegate_as_slice.len()) };
3541 let composition_passthrough_bound = self.decomposing_normalizer.composition_passthrough_bound;
3542 'fast: loop {
3543 // Only broken out of as goto forward
3544 'end: loop {
3545 // if let Some(&upcoming_code_unit) = code_unit_iter.next() {
3546 if likely(ptr != end) {
3547 // SAFETY: We just checked that `ptr` has not reached `end`.
3548 // `ptr` always advances by one, and we always have a check
3549 // per advancement.
3550 let mut upcoming_code_unit = unsafe { *ptr };
3551 // SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
3552 // by one points to the same allocation or to immediately
3553 // after, which is OK.
3554 ptr = unsafe { ptr.add(1) };
3555
3556 if likely(upcoming_code_unit < composition_passthrough_bound) {
3557 // No need for surrogate or U+FFFD check, because
3558 // `composition_passthrough_bound` cannot be higher than
3559 // U+0300.
3560 // Fast-track succeeded!
3561 continue 'fast;
3562 }
3563 if unlikely(in_inclusive_range16(upcoming_code_unit, 0x2013, 0x2022)) && upcoming_code_unit != 0x2017 {
3564 // Don't allow dashes and smart quotes to fall off the trie-bypass
3565 // path.
3566 // Fast-track succeeded!
3567 continue 'fast;
3568 }
3569 // This is intentionally bimodal so that if we exit the above trie-bypass path,
3570 // we stay on the trie-reading path until we've processed a non-BMP character
3571 // (likely emoji) or to the end of this passthrough run. This makes NFC faster
3572 // than ICU4C for most real-world content. The result is not optimal for NFKC
3573 // Latin, but let's take the NFC non-Latin win.
3574 let mut trie_value;
3575 let mut upcoming32; // May be surrogate
3576 loop {
3577 // We might be doing a trie lookup by surrogate. Surrogates get
3578 // a decomposition to U+FFFD.
3579 trie_value = composition.decomposition.delegate.trie().bmp(upcoming_code_unit);
3580 if likely(potential_passthrough_and_cannot_combine_backwards(trie_value)) {
3581 // Can't combine backwards, hence a plain (non-backwards-combining)
3582 // starter albeit past `composition_passthrough_bound`
3583
3584 // Fast-track succeeded!
3585 // Instead of going back to `'fast`, we stay here to skip the branch
3586 // for `composition_passthrough_bound`.
3587 if likely(ptr != end) {
3588 // SAFETY: We just checked that `ptr` has not reached `end`.
3589 // `ptr` always advances by one, and we always have a check
3590 // per advancement.
3591 upcoming_code_unit = unsafe { *ptr };
3592 // SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
3593 // by one points to the same allocation or to immediately
3594 // after, which is OK.
3595 ptr = unsafe { ptr.add(1) };
3596 continue;
3597 }
3598 break 'end;
3599 }
3600 upcoming32 = u32::from(upcoming_code_unit);
3601 break;
3602 }
3603
3604 // We might now be looking at a surrogate.
3605 // The loop is only broken out of as goto forward
3606 #[expect(clippy::never_loop)]
3607 'surrogateloop: loop {
3608 // The `likely` annotations _below_ exist to make the code _above_
3609 // go faster!
3610 let surrogate_base = upcoming32.wrapping_sub(0xD800);
3611 if likely(surrogate_base > (0xDFFF - 0xD800)) {
3612 // Not surrogate
3613 break 'surrogateloop;
3614 }
3615 if likely(surrogate_base <= (0xDBFF - 0xD800)) {
3616 // let iter_backup = code_unit_iter.clone();
3617 // if let Some(&low) = code_unit_iter.next() {
3618 if likely(ptr != end) {
3619 // SAFETY: We just checked that `ptr` has not reached `end`.
3620 // `ptr` always advances by one, and we always have a check
3621 // per advancement.
3622 let low = unsafe { *ptr };
3623 if likely(in_inclusive_range16(low, 0xDC00, 0xDFFF)) {
3624 // SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
3625 // by one points to the same allocation or to immediately
3626 // after, which is OK.
3627 ptr = unsafe { ptr.add(1) };
3628
3629 upcoming32 = (upcoming32 << 10) + u32::from(low)
3630 - (((0xD800u32 << 10) - 0x10000u32) + 0xDC00u32);
3631 // Successfully-paired surrogate. Read from the trie again.
3632 trie_value = {
3633 // Semantically, this bit of conditional compilation makes no sense.
3634 // The purpose is to keep LLVM seeing the untyped trie case the way
3635 // it did before so as not to regress the performance of the untyped
3636 // case due to unintuitive optimizer effects. If you care about the
3637 // perf of the untyped trie case and have better ideas, please try
3638 // something better.
3639 #[cfg(feature = "serde")]
3640 {composition.decomposition.delegate.trie().code_point(upcoming32)}
3641 #[cfg(not(feature = "serde"))]
3642 {composition.decomposition.delegate.trie().supplementary(upcoming32)}
3643 };
3644 if likely(potential_passthrough_and_cannot_combine_backwards(trie_value)) {
3645 // Fast-track succeeded!
3646 continue 'fast;
3647 }
3648 break 'surrogateloop;
3649 // } else {
3650 // code_unit_iter = iter_backup;
3651 }
3652 }
3653 }
3654 // unpaired surrogate
3655 upcoming32 = 0xFFFD; // Safe value for `char::from_u32_unchecked` and matches later potential error check.
3656 // trie_value already holds a decomposition to U+FFFD.
3657 debug_assert_eq!(trie_value, NON_ROUND_TRIP_MARKER | BACKWARD_COMBINING_MARKER | 0xFFFD);
3658 break 'surrogateloop;
3659 }
3660
3661 // SAFETY: upcoming32 can no longer be a surrogate.
3662 let upcoming = unsafe { char::from_u32_unchecked(upcoming32) };
3663 let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_value);
3664 // We need to fall off the fast path.
3665 composition.decomposition.pending = Some(upcoming_with_trie_value);
3666 let Some(consumed_so_far_slice) = pending_slice.get(..pending_slice.len() -
3667 // code_unit_iter.as_slice().len()
3668 // SAFETY: `ptr` and `end` have been derived from the same allocation
3669 // and `ptr` is never greater than `end`.
3670 unsafe { end.offset_from(ptr) as usize }
3671 - upcoming.len_utf16()) else {
3672 // If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
3673 debug_assert!(false);
3674 // Throw away the results of the fast path.
3675 break 'fastwrap;
3676 };
3677 let mut consumed_so_far = consumed_so_far_slice.chars_with_trie(composition.decomposition.delegate.trie());
3678 // Whether we could do something better than `next_back()` below is
3679 // https://github.com/unicode-org/icu4x/issues/7525
3680 let Some((c_from_back, trie_val_from_back)) = consumed_so_far.next_back() else {
3681 // If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
3682 debug_assert!(false);
3683 // Throw away the results of the fast path.
3684 break 'fastwrap;
3685 };
3686 // TODO: If the previous character was below the passthrough bound,
3687 // we really need to read from the trie. Otherwise, we could maintain
3688 // the most-recent trie value. Need to measure what's more expensive:
3689 // Remembering the trie value on each iteration or re-reading the
3690 // last one after the fast-track run.
3691 undecomposed_starter = CharacterAndTrieValue::new(c_from_back, trie_val_from_back);
3692 sink.write_slice(consumed_so_far.as_slice())?;
3693 break 'fast;
3694 }
3695 break;
3696 }
3697 // End of stream
3698 sink.write_slice(pending_slice)?;
3699 return Ok(());
3700 }
3701 // Sync the main iterator
3702 // composition.decomposition.delegate = code_unit_iter.as_slice().chars();
3703 // SAFETY: `ptr` and `end` have been derive from the same allocation
3704 // and `ptr` is never greater than `end`.
3705 composition.decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars_with_trie(composition.decomposition.delegate.trie());
3706 break 'fastwrap;
3707 }
3708 },
3709 text,
3710 sink,
3711 composition,
3712 undecomposed_starter,
3713 pending_slice,
3714 len_utf16,
3715 self,
3716 chars_with_trie,
3717 );
3718}
3719
3720/// A normalizer for performing composing normalization.
3721#[derive(Debug)]
3722pub struct ComposingNormalizer {
3723 decomposing_normalizer: DecomposingNormalizer,
3724 canonical_compositions: CanonicalCompositionsPayload,
3725}
3726
3727impl ComposingNormalizer {
3728 /// Constructs a borrowed version of this type for more efficient querying.
3729 pub fn as_borrowed(&self) -> ComposingNormalizerBorrowed<'_> {
3730 ComposingNormalizerBorrowed {
3731 decomposing_normalizer: self.decomposing_normalizer.as_borrowed(),
3732 canonical_compositions: self.canonical_compositions.as_borrowed(),
3733 }
3734 }
3735
3736 /// NFC constructor using compiled data.
3737 ///
3738 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
3739 ///
3740 /// [📚 Help choosing a constructor](icu_provider::constructors)
3741 #[cfg(feature = "compiled_data")]
3742 pub const fn new_nfc() -> ComposingNormalizerBorrowed<'static> {
3743 ComposingNormalizerBorrowed::new_nfc()
3744 }
3745
3746 icu_provider::gen_buffer_data_constructors!(
3747 () -> error: DataError,
3748 functions: [
3749 new_nfc: skip,
3750 try_new_nfc_with_buffer_provider,
3751 try_new_nfc_unstable,
3752 Self,
3753 ]
3754 );
3755
3756 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfc)]
3757 pub fn try_new_nfc_unstable<D>(provider: &D) -> Result<Self, DataError>
3758 where
3759 D: DataProvider<NormalizerNfdDataV1>
3760 + DataProvider<NormalizerNfdTablesV1>
3761 + DataProvider<NormalizerNfcV2>
3762 + ?Sized,
3763 {
3764 let decomposing_normalizer = DecomposingNormalizer::try_new_nfd_unstable(provider)?;
3765
3766 let canonical_compositions: DataPayload<NormalizerNfcV2> =
3767 provider.load(Default::default())?.payload;
3768
3769 Ok(ComposingNormalizer {
3770 decomposing_normalizer,
3771 canonical_compositions: CanonicalCompositionsPayload::Current(canonical_compositions),
3772 })
3773 }
3774
3775 /// NFKC constructor using compiled data.
3776 ///
3777 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
3778 ///
3779 /// [📚 Help choosing a constructor](icu_provider::constructors)
3780 #[cfg(feature = "compiled_data")]
3781 pub const fn new_nfkc() -> ComposingNormalizerBorrowed<'static> {
3782 ComposingNormalizerBorrowed::new_nfkc()
3783 }
3784
3785 icu_provider::gen_buffer_data_constructors!(
3786 () -> error: DataError,
3787 functions: [
3788 new_nfkc: skip,
3789 try_new_nfkc_with_buffer_provider,
3790 try_new_nfkc_unstable,
3791 Self,
3792 ]
3793 );
3794
3795 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfkc)]
3796 pub fn try_new_nfkc_unstable<D>(provider: &D) -> Result<Self, DataError>
3797 where
3798 D: DataProvider<NormalizerNfkdDataV1>
3799 + DataProvider<NormalizerNfdTablesV1>
3800 + DataProvider<NormalizerNfkdTablesV1>
3801 + DataProvider<NormalizerNfcV2>
3802 + ?Sized,
3803 {
3804 let decomposing_normalizer = DecomposingNormalizer::try_new_nfkd_unstable(provider)?;
3805
3806 let canonical_compositions: DataPayload<NormalizerNfcV2> =
3807 provider.load(Default::default())?.payload;
3808
3809 Ok(ComposingNormalizer {
3810 decomposing_normalizer,
3811 canonical_compositions: CanonicalCompositionsPayload::Current(canonical_compositions),
3812 })
3813 }
3814
3815 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_uts46)]
3816 pub(crate) fn try_new_uts46_unstable<D>(provider: &D) -> Result<Self, DataError>
3817 where
3818 D: DataProvider<NormalizerUts46DataV1>
3819 + DataProvider<NormalizerNfdTablesV1>
3820 + DataProvider<NormalizerNfkdTablesV1>
3821 // UTS 46 tables merged into CompatibilityDecompositionTablesV1
3822 + DataProvider<NormalizerNfcV2>
3823 + ?Sized,
3824 {
3825 let decomposing_normalizer =
3826 DecomposingNormalizer::try_new_uts46_decomposed_unstable(provider)?;
3827
3828 let canonical_compositions: DataPayload<NormalizerNfcV2> =
3829 provider.load(Default::default())?.payload;
3830
3831 Ok(ComposingNormalizer {
3832 decomposing_normalizer,
3833 canonical_compositions: CanonicalCompositionsPayload::Current(canonical_compositions),
3834 })
3835 }
3836}
3837
3838#[cfg(feature = "utf16_iter")]
3839struct IsNormalizedSinkUtf16<'a> {
3840 expect: &'a [u16],
3841}
3842
3843#[cfg(feature = "utf16_iter")]
3844impl<'a> IsNormalizedSinkUtf16<'a> {
3845 pub fn new(slice: &'a [u16]) -> Self {
3846 IsNormalizedSinkUtf16 { expect: slice }
3847 }
3848 pub fn remaining_len(&self) -> usize {
3849 self.expect.len()
3850 }
3851}
3852
3853#[cfg(feature = "utf16_iter")]
3854impl write16::Write16 for IsNormalizedSinkUtf16<'_> {
3855 fn write_slice(&mut self, s: &[u16]) -> core::fmt::Result {
3856 // We know that if we get a slice, it's a pass-through,
3857 // so we can compare addresses. Indexing is OK, because
3858 // an indexing failure would be a code bug rather than
3859 // an input or data issue.
3860 #[expect(clippy::indexing_slicing)]
3861 if core::ptr::eq(s.as_ptr(), self.expect.as_ptr()) {
3862 self.expect = &self.expect[s.len()..];
3863 Ok(())
3864 } else {
3865 Err(core::fmt::Error {})
3866 }
3867 }
3868
3869 fn write_char(&mut self, c: char) -> core::fmt::Result {
3870 let mut iter = utf16_iter::ErrorReportingUtf16Chars::new(self.expect);
3871 if iter.next() == Some(Ok(c)) {
3872 self.expect = iter.as_slice();
3873 Ok(())
3874 } else {
3875 Err(core::fmt::Error {})
3876 }
3877 }
3878}
3879
3880#[cfg(feature = "utf8_iter")]
3881struct IsNormalizedSinkUtf8<'a> {
3882 expect: &'a [u8],
3883}
3884
3885#[cfg(feature = "utf8_iter")]
3886impl<'a> IsNormalizedSinkUtf8<'a> {
3887 pub fn new(slice: &'a [u8]) -> Self {
3888 IsNormalizedSinkUtf8 { expect: slice }
3889 }
3890 pub fn remaining_len(&self) -> usize {
3891 self.expect.len()
3892 }
3893}
3894
3895#[cfg(feature = "utf8_iter")]
3896impl core::fmt::Write for IsNormalizedSinkUtf8<'_> {
3897 fn write_str(&mut self, s: &str) -> core::fmt::Result {
3898 // We know that if we get a slice, it's a pass-through,
3899 // so we can compare addresses. Indexing is OK, because
3900 // an indexing failure would be a code bug rather than
3901 // an input or data issue.
3902 #[expect(clippy::indexing_slicing)]
3903 if core::ptr::eq(s.as_ptr(), self.expect.as_ptr()) {
3904 self.expect = &self.expect[s.len()..];
3905 Ok(())
3906 } else {
3907 Err(core::fmt::Error {})
3908 }
3909 }
3910
3911 fn write_char(&mut self, c: char) -> core::fmt::Result {
3912 let mut iter = utf8_iter::ErrorReportingUtf8Chars::new(self.expect);
3913 if iter.next() == Some(Ok(c)) {
3914 self.expect = iter.as_slice();
3915 Ok(())
3916 } else {
3917 Err(core::fmt::Error {})
3918 }
3919 }
3920}
3921
3922struct IsNormalizedSinkStr<'a> {
3923 expect: &'a str,
3924}
3925
3926impl<'a> IsNormalizedSinkStr<'a> {
3927 pub fn new(slice: &'a str) -> Self {
3928 IsNormalizedSinkStr { expect: slice }
3929 }
3930 pub fn remaining_len(&self) -> usize {
3931 self.expect.len()
3932 }
3933}
3934
3935impl core::fmt::Write for IsNormalizedSinkStr<'_> {
3936 fn write_str(&mut self, s: &str) -> core::fmt::Result {
3937 // We know that if we get a slice, it's a pass-through,
3938 // so we can compare addresses. Indexing is OK, because
3939 // an indexing failure would be a code bug rather than
3940 // an input or data issue.
3941 if core::ptr::eq(s.as_ptr(), self.expect.as_ptr()) {
3942 self.expect = &self.expect[s.len()..];
3943 Ok(())
3944 } else {
3945 Err(core::fmt::Error {})
3946 }
3947 }
3948
3949 fn write_char(&mut self, c: char) -> core::fmt::Result {
3950 let mut iter = self.expect.chars();
3951 if iter.next() == Some(c) {
3952 self.expect = iter.as_str();
3953 Ok(())
3954 } else {
3955 Err(core::fmt::Error {})
3956 }
3957 }
3958}