diff --git a/src/validity.cpp b/src/validity.cpp index 52ebd0c..4d3703e 100644 --- a/src/validity.cpp +++ b/src/validity.cpp @@ -189,7 +189,7 @@ bool is_label_valid(const std::u32string_view label) { if (c == 0x200c) { if (i > 0) { if (std::ranges::binary_search(virama, label[i - 1])) { - return true; + continue; } } if ((i == 0) || (i + 1 >= label.size())) { @@ -206,14 +206,16 @@ bool is_label_valid(const std::u32string_view label) { }; std::u32string_view before = label.substr(0, i); std::u32string_view after = label.substr(i + 1); - return (std::find_if(before.begin(), before.end(), is_l_or_d) != - before.end()) && - (std::find_if(after.begin(), after.end(), is_r_or_d) != - after.end()); + if ((std::find_if(before.begin(), before.end(), is_l_or_d) == + before.end()) || + (std::find_if(after.begin(), after.end(), is_r_or_d) == + after.end())) { + return false; + } } else if (c == 0x200d) { if (i > 0) { if (std::ranges::binary_search(virama, label[i - 1])) { - return true; + continue; } } return false; diff --git a/tests/to_ascii_tests.cpp b/tests/to_ascii_tests.cpp index 0f55b40..51d85ac 100644 --- a/tests/to_ascii_tests.cpp +++ b/tests/to_ascii_tests.cpp @@ -191,6 +191,21 @@ TEST(to_ascii_tests, bidi_regression) { << "multi-label should fail"; } +TEST(to_ascii_tests, contextj_joiner_does_not_bypass_bidi) { + // A ZWNJ preceded by a virama satisfies the ContextJ rule, but that must not + // short-circuit the Bidi validity check for the rest of the label. Here the + // label starts with L (U+0915) so it is an LTR label, and the trailing AL + // (U+0627) violates Rule 5. It must be rejected. + EXPECT_TRUE(ada::idna::to_ascii("\u0915\u094D\u200C\u0627").empty()) + << "valid ZWNJ must not bypass Bidi rule 5"; + // Same with ZWJ (U+200D). + EXPECT_TRUE(ada::idna::to_ascii("\u0915\u094D\u200D\u0627").empty()) + << "valid ZWJ must not bypass Bidi rule 5"; + // A contextually valid joiner in an otherwise valid label still converts. + EXPECT_EQ(ada::idna::to_ascii("\u0915\u094D\u200C\u0916"), "xn--11bc8nw90g") + << "valid ZWNJ label must still convert"; +} + // Helper: domain-to-ASCII as URL Standard callers use it (to_ascii + forbidden // domain code point filter). Empty string means failure. static std::string domain_to_ascii(std::string_view input) {