Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down Expand Up @@ -107,4 +107,4 @@ sonarqube {

tasks.named("sonar").configure {
dependsOn(":koverXmlReport")
}
}
39 changes: 39 additions & 0 deletions src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Nodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}