From 5cbe7210cdb64356550a27196c3e3d5810853617 Mon Sep 17 00:00:00 2001 From: Enrico Date: Thu, 23 Apr 2026 14:34:04 +0200 Subject: [PATCH 1/3] fix: Do not escape regex in d2:validatePattern [DHIS2-21359] --- .../org/hisp/dhis/lib/expression/eval/Calculator.kt | 11 ++++++++++- .../lib/expression/function/ValidatePatternTest.kt | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt index b2db5a8..90ea53d 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt @@ -139,7 +139,7 @@ internal class Calculator( evalToInteger(fn.child(2))) NamedFunction.d2_validatePattern -> functions.d2_validatePattern( evalToString(fn.child(0)), - evalToString(fn.child(1))) + evalToRawString(fn.child(1))) NamedFunction.d2_weeksBetween -> functions.d2_weeksBetween( evalToDate(fn.child(0)), evalToDate(fn.child(1))) @@ -301,6 +301,15 @@ internal class Calculator( return eval(node, "String", Typed::toStringTypeCoercion) } + private fun evalToRawString(node: Node<*>): String? { + return when (node.getType()) { + NodeType.STRING -> node.getRawValue() + NodeType.ARGUMENT -> evalToRawString(node.child(0)) + NodeType.PAR -> evalToRawString(node.child(0)) + else -> evalToString(node) + } + } + fun evalToBoolean(node: Node<*>): Boolean? { return eval(node, "Boolean", Typed::toBooleanTypeCoercion) } diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ValidatePatternTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ValidatePatternTest.kt index 278129b..817d4a3 100644 --- a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ValidatePatternTest.kt +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ValidatePatternTest.kt @@ -29,15 +29,17 @@ internal class ValidatePatternTest { fun testValidatePattern_Match() { assertTrue(evaluate("d2:validatePattern(\"124\", \"[0-9]+\")")) assertTrue(evaluate("d2:validatePattern(\"12x4\", \"[0-9x]+\")")) + assertTrue(evaluate("d2:validatePattern(\"John\",(\"[a-zA-Z0-9À-ȕ\\'\\-\\‘\\`\\’\\ ]+\"))")) } @Test fun testValidatePattern_NoMatch() { assertFalse(evaluate("d2:validatePattern(\"12x4\", \"[0-9]+\")")) assertFalse(evaluate("d2:validatePattern(\"ab0\", \"[0-9x]+\")")) + assertFalse(evaluate("d2:validatePattern(\"Иван\",(\"[a-zA-Z0-9À-ȕ\\'\\-\\‘\\`\\’\\ ]+\"))")) } private fun evaluate(expression: String): Boolean { return Expression(expression, ExpressionMode.RULE_ENGINE_ACTION).evaluate() as Boolean } -} \ No newline at end of file +} From 7f851082f8e98eef70c81a43e121ec81ad9f5f91 Mon Sep 17 00:00:00 2001 From: Enrico Date: Thu, 23 Apr 2026 15:05:57 +0200 Subject: [PATCH 2/3] Add decodeToRegex function to work on all platforms --- .../org/hisp/dhis/lib/expression/ast/Nodes.kt | 39 +++++++++++++++++++ .../dhis/lib/expression/eval/Calculator.kt | 3 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt index ad81fc5..5c3fa41 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt @@ -356,6 +356,45 @@ object Nodes { } return str.toString() } + + /** + * Processes a raw expression-string value for use as a regex pattern on all platforms. + * + * Strips backslashes from expression-level escapes (e.g. \' -> ', \ -> ' ') + * but preserves standard regex escapes (\d, \w, \s, \uXXXX, etc.) so the + * regex engine receives them intact. In particular, \- is kept as \- so that + * a hyphen inside a character class is treated as a literal and cannot form an + * unintended range. Both Java and JS unicode-mode regex accept \-. + */ + fun decodeToRegex(rawValue: String): String { + if (rawValue.indexOf('\\') < 0) return rawValue + val str = StringBuilder() + val chars = rawValue.toCharArray() + var i = 0 + while (i < chars.size) { + val c = chars[i++] + if (c != '\\' || i >= chars.size) { + str.append(c) + continue + } + val next = chars[i++] + when (next) { + 't', 'n', 'r', 'f', 'b', + '\\', '^', '$', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', + '-', 'd', 'D', 'w', 'W', 's', 'S', 'B', 'p', 'P' -> str.append('\\').append(next) + 'u' -> { + str.append('\\').append('u') + repeat(4) { if (i < chars.size) str.append(chars[i++]) } + } + 'x' -> { + str.append('\\').append('x') + repeat(2) { if (i < chars.size) str.append(chars[i++]) } + } + else -> str.append(next) + } + } + return str.toString() + } } } diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt index 90ea53d..abfc503 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt @@ -2,6 +2,7 @@ package org.hisp.dhis.lib.expression.eval import kotlinx.datetime.LocalDate import org.hisp.dhis.lib.expression.ast.* +import org.hisp.dhis.lib.expression.ast.Nodes.Utf8StringNode import org.hisp.dhis.lib.expression.ast.UnaryOperator.Companion.negate import org.hisp.dhis.lib.expression.spi.* @@ -303,7 +304,7 @@ internal class Calculator( private fun evalToRawString(node: Node<*>): String? { return when (node.getType()) { - NodeType.STRING -> node.getRawValue() + NodeType.STRING -> Utf8StringNode.decodeToRegex(node.getRawValue()) NodeType.ARGUMENT -> evalToRawString(node.child(0)) NodeType.PAR -> evalToRawString(node.child(0)) else -> evalToString(node) From 379b3f4aff7bfa56e6c7f228d07538aa93130bbe Mon Sep 17 00:00:00 2001 From: Enrico Date: Thu, 23 Apr 2026 15:11:27 +0200 Subject: [PATCH 3/3] Bump version --- build.gradle.kts | 4 ++-- .../kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index aba53a1..d24ca67 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ repositories { mavenCentral() } -version = "1.4.1-SNAPSHOT" +version = "1.4.2-SNAPSHOT" group = "org.hisp.dhis.lib.expression" if (project.hasProperty("removeSnapshotSuffix")) { @@ -107,4 +107,4 @@ sonarqube { tasks.named("sonar").configure { dependsOn(":koverXmlReport") -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt index 5c3fa41..1fbc90a 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt @@ -366,7 +366,7 @@ object Nodes { * a hyphen inside a character class is treated as a literal and cannot form an * unintended range. Both Java and JS unicode-mode regex accept \-. */ - fun decodeToRegex(rawValue: String): String { + internal fun decodeToRegex(rawValue: String): String { if (rawValue.indexOf('\\') < 0) return rawValue val str = StringBuilder() val chars = rawValue.toCharArray()