Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/normalization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,55 @@ static uint8_t trailing_ccc(char32_t c) noexcept {
return get_ccc(decomposition_data[base + length - 1]);
}

void compose(std::u32string& input);

// A character that has a canonical decomposition is in NFC form only if that
// decomposition composes back to exactly it. Singletons never do (e.g. U+212B
// ANGSTROM SIGN). Composition exclusions do not either: their decomposition
// composes to a different primary composite (e.g. U+1F71 -> U+03B1 U+0301 ->
// U+03AC) or does not compose at all (e.g. U+0958 DEVANAGARI LETTER QA ->
// U+0915 U+093C), and non-starter decompositions such as U+0344 -> U+0308
// U+0301 stay decomposed. All are NFC_Quick_Check=No and must be normalized.
static bool decomposition_is_not_nfc(char32_t c) noexcept {
if (c >= hangul_sbase && c < hangul_sbase + hangul_scount) {
return false;
}
if (c >= 0x110000) {
return false;
}
const uint16_t* const decomposition =
decomposition_block_row(decomposition_index[c >> 8]) + (c % 256);
const size_t length = (decomposition[1] >> 2) - (decomposition[0] >> 2);
if (length == 0 || (decomposition[0] & 1)) {
return false; // no canonical decomposition (or compatibility-only)
}
if (length == 1) {
return true; // singleton
}
const size_t base = decomposition[0] >> 2;
if (base + length > decomposition_data_size) {
return false;
}
// Recompose the canonical decomposition; if it does not collapse back to c,
// then c is an exclusion / non-starter decomposition and is not NFC.
std::u32string decomposed(decomposition_data + base,
decomposition_data + base + length);
compose(decomposed);
return !(decomposed.size() == 1 && decomposed[0] == c);
}

bool is_already_nfc(std::u32string_view input) noexcept {
if (input.empty()) {
return true;
}
if (!tables_are_ready() && !ensure_tables()) {
return false;
}
// 1) Singleton decompositions are never NFC (e.g. U+212B ANGSTROM SIGN).
// Multi-code-point decomps are primary composites that are NFC as-is.
// 1) A character whose canonical decomposition does not compose back to it is
// not NFC: singletons (e.g. U+212B), composition exclusions (e.g. U+0958)
// and non-starter decompositions (e.g. U+0344).
for (char32_t c : input) {
if (canonical_decomp_length(c) == 1) {
if (decomposition_is_not_nfc(c)) {
return false;
}
}
Expand Down
30 changes: 30 additions & 0 deletions tests/safety_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,36 @@ TEST(Safety, IsAlreadyNfc) {
EXPECT_FALSE(ada::idna::is_already_nfc(decomposed));
}

TEST(Safety, IsAlreadyNfcCompositionExclusions) {
ASSERT_TRUE(ada::idna::ensure_tables());
// A canonical decomposition longer than one code point does not make a
// character NFC: composition exclusions and non-starter decompositions are
// NFC_Quick_Check=No and must normalize.
// U+0958 DEVANAGARI LETTER QA -> U+0915 U+093C (excluded, no recompose)
std::u32string qa = {0x0958};
EXPECT_FALSE(ada::idna::is_already_nfc(qa));
ASSERT_TRUE(ada::idna::normalize(qa));
EXPECT_EQ(qa, std::u32string({0x0915, 0x093C}));

// U+1F71 GREEK SMALL LETTER ALPHA WITH OXIA -> U+03AC: its decomposition
// recomposes to a different primary composite, so U+1F71 is not NFC.
std::u32string oxia = {0x1F71};
EXPECT_FALSE(ada::idna::is_already_nfc(oxia));
ASSERT_TRUE(ada::idna::normalize(oxia));
EXPECT_EQ(oxia, std::u32string({0x03AC}));

// U+0344 COMBINING GREEK DIALYTIKA TONOS -> U+0308 U+0301 (non-starter
// decomposition, stays decomposed).
std::u32string dialytika = {0x0344};
EXPECT_FALSE(ada::idna::is_already_nfc(dialytika));
ASSERT_TRUE(ada::idna::normalize(dialytika));
EXPECT_EQ(dialytika, std::u32string({0x0308, 0x0301}));

// Genuine primary composites stay on the already-NFC fast path.
EXPECT_TRUE(ada::idna::is_already_nfc(std::u32string({0x00E9})));
EXPECT_TRUE(ada::idna::is_already_nfc(std::u32string({0x03AC})));
}

TEST(Safety, ComposesHangulLvPlusTrailingJamo) {
ASSERT_TRUE(ada::idna::ensure_tables());
// A precomposed LV syllable (SIndex % TCount == 0) followed by a trailing
Expand Down
Loading