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 ad81fc5..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 @@ -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 \-. + */ + internal 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 b2db5a8..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.* @@ -139,7 +140,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 +302,15 @@ internal class Calculator( return eval(node, "String", Typed::toStringTypeCoercion) } + private fun evalToRawString(node: Node<*>): String? { + return when (node.getType()) { + NodeType.STRING -> Utf8StringNode.decodeToRegex(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 +}