]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib: idna - Implement Bidi rule checks
authorStephan Bosch <stephan.bosch@open-xchange.com>
Fri, 4 Apr 2025 02:20:38 +0000 (04:20 +0200)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Mon, 13 Jul 2026 18:22:50 +0000 (18:22 +0000)
src/lib/idna.c
src/lib/idna.h
src/lib/test-idna.c

index 1f44a2e083c380b02e6d21c350e0f78bc81e1615..492e2ab29789bce2ce03366e2a8e832d9838ba3c 100644 (file)
@@ -9,6 +9,172 @@
 #include "idna.h"
 #include "idna-punycode.h"
 
+/*
+ * Bidi Checking
+ */
+
+void idna_bidi_checker_init(struct idna_bidi_checker *ibc_r,
+                           struct idna_bidi_check_context *ctx)
+{
+       i_zero(ibc_r);
+       ibc_r->ctx = ctx;
+}
+
+void idna_bidi_checker_reset(struct idna_bidi_checker *ibc)
+{
+       struct idna_bidi_check_context *ctx = ibc->ctx;
+
+       idna_bidi_checker_init(ibc, ctx);
+}
+
+void idna_bidi_checker_input(struct idna_bidi_checker *ibc,  uint32_t cp,
+                            const struct unicode_code_point_data **cp_data)
+{
+       struct idna_bidi_check_context *ctx = ibc->ctx;
+
+       if (*cp_data == NULL)
+               *cp_data = unicode_code_point_get_data(cp);
+
+       /* RFC 5893, Section 2: The Bidi Rule
+
+          The following rule, consisting of six conditions, applies to labels
+          in Bidi domain names. The requirements that this rule satisfies are
+          described in Section 3. All of the conditions must be satisfied for
+          the rule to be satisfied.
+
+          1.  The first character must be a character with Bidi property L, R,
+              or AL.  If it has the R or AL property, it is an RTL label; if it
+              has the L property, it is an LTR label.
+
+          2.  In an RTL label, only characters with the Bidi properties R, AL,
+              AN, EN, ES, CS, ET, ON, BN, or NSM are allowed.
+
+          3.  In an RTL label, the end of the label must be a character with
+              Bidi property R, AL, EN, or AN, followed by zero or more
+              characters with Bidi property NSM.
+
+          4.  In an RTL label, if an EN is present, no AN may be present, and
+              vice versa.
+
+          5.  In an LTR label, only characters with the Bidi properties L, EN,
+              ES, CS, ET, ON, BN, or NSM are allowed.
+
+          6.  In an LTR label, the end of the label must be a character with
+              Bidi property L or EN, followed by zero or more characters with
+              Bidi property NSM.
+        */
+
+       switch (ibc->state) {
+       case IDNA_BIDI_CHECK_STATE_START:
+               switch ((*cp_data)->bidi_class) {
+               case UNICODE_BIDI_CLASS_R:
+               case UNICODE_BIDI_CLASS_AL:
+                       ctx->rtl_label = TRUE;
+                       ibc->label_can_end = TRUE;
+                       ibc->state = IDNA_BIDI_CHECK_STATE_RTL;
+                       break;
+               case UNICODE_BIDI_CLASS_AN:
+                       ctx->rtl_label = TRUE;
+                       ctx->valid = FALSE;
+                       break;
+               case UNICODE_BIDI_CLASS_L:
+                       ibc->label_can_end = TRUE;
+                       ibc->state = IDNA_BIDI_CHECK_STATE_LTR;
+                       break;
+               default:
+                       ctx->valid = FALSE;
+                       break;
+               }
+               break;
+       case IDNA_BIDI_CHECK_STATE_RTL:
+               switch ((*cp_data)->bidi_class) {
+               case UNICODE_BIDI_CLASS_R:
+               case UNICODE_BIDI_CLASS_AL:
+                       ibc->label_can_end = TRUE;
+                       break;
+               case UNICODE_BIDI_CLASS_ES:
+               case UNICODE_BIDI_CLASS_CS:
+               case UNICODE_BIDI_CLASS_ET:
+               case UNICODE_BIDI_CLASS_ON:
+               case UNICODE_BIDI_CLASS_BN:
+                       ibc->label_can_end = FALSE;
+                       break;
+               case UNICODE_BIDI_CLASS_NSM:
+                       break;
+               case UNICODE_BIDI_CLASS_EN:
+                       if (ibc->an_present) {
+                               ctx->valid = FALSE;
+                               break;
+                       }
+                       ibc->en_present = TRUE;
+                       ibc->label_can_end = TRUE;;
+                       break;
+               case UNICODE_BIDI_CLASS_AN:
+                       if (ibc->en_present) {
+                               ctx->valid = FALSE;
+                               break;
+                       }
+                       ibc->an_present = TRUE;
+                       ibc->label_can_end = TRUE;;
+                       break;
+               default:
+                       ctx->valid = FALSE;
+                       break;
+               }
+               break;
+       case IDNA_BIDI_CHECK_STATE_LTR:
+               switch ((*cp_data)->bidi_class) {
+               case UNICODE_BIDI_CLASS_L:
+               case UNICODE_BIDI_CLASS_EN:
+                       ibc->label_can_end = TRUE;
+                       break;
+               case UNICODE_BIDI_CLASS_ES:
+               case UNICODE_BIDI_CLASS_CS:
+               case UNICODE_BIDI_CLASS_ET:
+               case UNICODE_BIDI_CLASS_ON:
+               case UNICODE_BIDI_CLASS_BN:
+                       ibc->label_can_end = FALSE;
+                       break;
+               case UNICODE_BIDI_CLASS_NSM:
+                       break;
+               case UNICODE_BIDI_CLASS_R:
+               case UNICODE_BIDI_CLASS_AL:
+               case UNICODE_BIDI_CLASS_AN:
+                       ctx->rtl_label = TRUE;
+                       ctx->valid = FALSE;
+                       break;
+               default:
+                       ctx->valid = FALSE;
+                       break;
+               }
+               break;
+       default:
+               i_unreached();
+       }
+}
+
+int idna_bidi_checker_finish(struct idna_bidi_checker *ibc)
+{
+       struct idna_bidi_check_context *ctx = ibc->ctx;
+
+       switch (ibc->state) {
+       case IDNA_BIDI_CHECK_STATE_START:
+               break;
+       case IDNA_BIDI_CHECK_STATE_RTL:
+               if (!ibc->label_can_end)
+                       ctx->valid = FALSE;
+               break;
+       case IDNA_BIDI_CHECK_STATE_LTR:
+               if (!ibc->label_can_end)
+                       ctx->valid = FALSE;
+               break;
+       }
+
+       if (!ctx->valid && ctx->rtl_label)
+               return -1;
+       return 0;
+}
+
 /*
  * IDNA Processing
  */
@@ -198,6 +364,7 @@ struct idna_validate {
        enum idna_process_flags flags;
 
        enum idna_validate_state state;
+       struct idna_bidi_checker bidicheck;
 
        uint32_t cp, last_cp;
        const struct unicode_code_point_data *cp_data;
@@ -224,6 +391,7 @@ static const struct unicode_transform_def idna_validate_def = {
 
 static void
 idna_validate_init(struct idna_validate *valdt_r,
+                  struct idna_bidi_check_context *bidictx,
                   enum idna_process_flags flags,
                   bool decoded_a_label)
 {
@@ -233,6 +401,7 @@ idna_validate_init(struct idna_validate *valdt_r,
        valdt_r->decoded_a_label = decoded_a_label;
        if (decoded_a_label)
                unicode_nf_checker_init(&valdt_r->nfccheck, UNICODE_NFC);
+       idna_bidi_checker_init(&valdt_r->bidicheck, bidictx);
 }
 
 static int
@@ -261,6 +430,18 @@ idna_validate_label_end(struct idna_validate *valdt, const char **error_r)
                return -1;
        }
 
+       /* 9. If CheckBidi, and if the domain name is a Bidi domain name, then
+          the label must satisfy all six of the numbered conditions in
+          RFC 5893, Section 2.
+        */
+       if (HAS_NO_BITS(valdt->flags, IDNA_PROCESS_FLAG_IGNORE_BIDI) &&
+           valdt->state != IDNA_VALIDATE_STATE_ALABEL &&
+           idna_bidi_checker_finish(&valdt->bidicheck) < 0) {
+               *error_r = "Invalid label in Bidi domain name";
+               return -1;
+       }
+       idna_bidi_checker_reset(&valdt->bidicheck);
+
        return 0;
 }
 
@@ -340,6 +521,7 @@ idna_validate_cp(struct idna_validate *valdt, uint32_t cp,
                /* Check for 'xn--' for A-label */
                i_assert(!valdt->decoded_a_label);
                if (cp == '-') {
+                       idna_bidi_checker_reset(&valdt->bidicheck);
                        valdt->state = IDNA_VALIDATE_STATE_ALABEL;
                        return 0;
                }
@@ -455,8 +637,8 @@ idna_validate_cp(struct idna_validate *valdt, uint32_t cp,
           the label must satisfy all six of the numbered conditions in
           RFC 5893, Section 2.
         */
-
-       /* - NOT IMPLEMENTED - */
+       if (HAS_NO_BITS(valdt->flags, IDNA_PROCESS_FLAG_IGNORE_BIDI))
+               idna_bidi_checker_input(&valdt->bidicheck, cp, cp_data);
 
        return 0;
 }
@@ -560,6 +742,7 @@ idna_validate_flush(struct unicode_transform *trans,
 
 struct idna_process_sink {
        struct unicode_transform transform;
+       struct idna_bidi_check_context *bidictx;
        enum idna_process_flags flags;
 
        uint32_t label_buf[LABEL_BUF_SIZE];
@@ -577,7 +760,7 @@ idna_check_a_label(struct idna_process_sink *sink,
 {
        struct idna_validate valdt;
 
-       idna_validate_init(&valdt, sink->flags, TRUE);
+       idna_validate_init(&valdt, sink->bidictx, sink->flags, TRUE);
 
        struct unicode_transform *trans = &valdt.transform;
        size_t pos;
@@ -769,12 +952,14 @@ struct unicode_transform_def idna_process_sink_def = {
 
 static void
 idna_process_sink_init(struct idna_process_sink *sink_r,
+                      struct idna_bidi_check_context *bidictx,
                       enum idna_process_flags flags,
                       buffer_t *unicode_buffer,
                       buffer_t *ascii_buffer)
 {
        i_zero(sink_r);
        unicode_transform_init(&sink_r->transform, &idna_process_sink_def);
+       sink_r->bidictx = bidictx;
        sink_r->flags = flags;
        sink_r->unicode_buffer = unicode_buffer;
        sink_r->ascii_buffer = ascii_buffer;
@@ -807,6 +992,10 @@ int idna_process_domain_name(const char *domain_name,
           end of this function.
         */
 
+       struct idna_bidi_check_context bidictx;
+
+       idna_bidi_checker_context_init(&bidictx);
+
        /* 1. Map: */
 
        struct idna_map map;
@@ -832,7 +1021,7 @@ int idna_process_domain_name(const char *domain_name,
 
        struct idna_validate valdt;
 
-       idna_validate_init(&valdt, flags, FALSE);
+       idna_validate_init(&valdt, &bidictx, flags, FALSE);
        unicode_transform_chain(&nfc.transform, &valdt.transform);
 
        /* Actually break the string into labels at U+002E ( . ) FULL STOP and
@@ -847,7 +1036,7 @@ int idna_process_domain_name(const char *domain_name,
        if (to_ascii_r != NULL)
                out_ascii = t_buffer_create(256);
 
-       idna_process_sink_init(&sink, flags, out_unicode, out_ascii);
+       idna_process_sink_init(&sink, &bidictx, flags, out_unicode, out_ascii);
        unicode_transform_chain(&valdt.transform, &sink.transform);
 
        /* Run the Unicode transform chain */
index 47bfe0ac6a890b20806adc8311b8dfed523f99f5..081fae53ef02a61103ada14288c1149c35920089 100644 (file)
@@ -4,6 +4,48 @@
 #define IDNA_DNS_MAX_LABEL_LENGTH 63
 #define IDNA_DNS_MAX_NAME_LENGTH 254
 
+struct unicode_code_point_data;
+
+/*
+ * Bidi Checking
+ */
+
+enum idna_bidi_check_state {
+       IDNA_BIDI_CHECK_STATE_START = 0,
+       IDNA_BIDI_CHECK_STATE_RTL,
+       IDNA_BIDI_CHECK_STATE_LTR,
+};
+
+struct idna_bidi_check_context {
+       bool rtl_label:1;
+       bool valid:1;
+};
+
+struct idna_bidi_checker {
+       enum idna_bidi_check_state state;
+       struct idna_bidi_check_context *ctx;
+
+       bool en_present:1;
+       bool an_present:1;
+       bool label_can_end:1;
+};
+
+static inline void
+idna_bidi_checker_context_init(struct idna_bidi_check_context *ctx_r)
+{
+       i_zero(ctx_r);
+       ctx_r->valid = TRUE;
+       ctx_r->rtl_label = FALSE;
+}
+
+void idna_bidi_checker_init(struct idna_bidi_checker *ibc_r,
+                           struct idna_bidi_check_context *ctx);
+void idna_bidi_checker_reset(struct idna_bidi_checker *ibc);
+
+void idna_bidi_checker_input(struct idna_bidi_checker *ibc, uint32_t cp,
+                            const struct unicode_code_point_data **cp_data);
+int idna_bidi_checker_finish(struct idna_bidi_checker *ibc);
+
 /* UnicodeĀ® Technical Standard #46, Section 4:
  *
  * Input:
@@ -13,8 +55,8 @@ enum idna_process_flags {
        IDNA_PROCESS_FLAG_IGNORE_STD3_ASCII_RULES = BIT(0),
        /* A boolean flag: CheckHyphens */
        IDNA_PROCESS_FLAG_CHECK_HYPHENS = BIT(1),
-       /* A boolean flag: CheckBidi - always FALSE
-          NOT IMPLEMENTED */
+       /* A boolean flag: CheckBidi - inverted */
+       IDNA_PROCESS_FLAG_IGNORE_BIDI = BIT(2),
        /* A boolean flag: CheckJoiners - always FALSE
           NOT IMPLEMENTED */
        /* A boolean flag: Transitional_Processing - always FALSE
index a4e30fda30e9a50aadfb8494b5bed8ca3340a019..46424fd1af484e19ef815a7d34a68bf416edb46e 100644 (file)
@@ -92,7 +92,8 @@ static const char *resolve_escapes(const char *in)
        return str_c(out);
 }
 
-static void test_success_status(const char *status)
+static void
+test_success_status(const char *status, enum idna_process_flags flags)
 {
        if (*status == '\0')
                return;
@@ -102,8 +103,9 @@ static void test_success_status(const char *status)
        while (*ps != NULL && !test_has_failed()) {
                const char *s = t_str_trim(*ps, " \t");
 
-               if (*s == 'B') {
-                       /* BiDi checks not implemented. */
+               if (HAS_ALL_BITS(flags, IDNA_PROCESS_FLAG_IGNORE_BIDI) &&
+                   *s == 'B') {
+                       /* BiDi checks disabled. */
                } else if (*s == 'C') {
                        /* ContextJ checks not implemented. */
                } else {
@@ -197,7 +199,7 @@ test_scenario(const char *source, enum idna_process_flags flags,
        test_assert_idx(*to_unicode_status != '\0' ||
                        *to_ascii_n_status != '\0'|| ret >= 0, line_num);
        if (ret >= 0)
-               test_success_status(to_unicode_status);
+               test_success_status(to_unicode_status, flags);
        else {
                test_u_failure_status(to_unicode_status, to_ascii_n_status,
                                      error, line_num);
@@ -269,6 +271,9 @@ static void test_line(const char *line, unsigned int line_num)
        test_scenario(source, 0,
                      to_unicode, to_unicode_status,
                      to_ascii_n, to_ascii_n_status, line_num);
+       test_scenario(source, IDNA_PROCESS_FLAG_IGNORE_BIDI,
+                     to_unicode, to_unicode_status,
+                     to_ascii_n, to_ascii_n_status, line_num);
 }
 
 static void test_idna_uts_46(void)