nre: fix replacement string parser OOB access, numeric refs, and unterminated named refs (#25560)

1. A trailing `$` at the end of a replacement string could read out of
bounds via `how[i + 1]`; this now raises `ValueError` instead.

2. Numeric capture parsing used `id += (id * 10) + digit` instead of `id
= (id * 10) + digit`, so multi-digit refs were parsed incorrectly (e.g.
`$12` resolved as capture 13 instead of 12).

4. Unterminated named replacement syntax (e.g. `${foo)` is now rejected
with ValueError instead of being accepted and parsed inconsistently.

Found and fixed by GPT 5.3 Codex.

(cherry picked from commit 9b2b286baf)
This commit is contained in:
Raka Hourianto
2026-02-28 09:39:16 +03:00
committed by narimiran
parent 7b8ef1a901
commit 5eb96d40ee
2 changed files with 12 additions and 1 deletions

View File

@@ -15,6 +15,9 @@ template formatStr*(howExpr, namegetter, idgetter): untyped =
val.add(how[i])
i += 1
else:
if i + 1 >= how.len:
raise newException(ValueError, "Syntax error in format string at " & $i)
if how[i + 1] == '$':
val.add('$')
i += 2
@@ -27,7 +30,7 @@ template formatStr*(howExpr, namegetter, idgetter): untyped =
i += 1
var id {.inject.} = 0
while i < how.len and how[i] in {'0'..'9'}:
id += (id * 10) + (ord(how[i]) - ord('0'))
id = (id * 10) + (ord(how[i]) - ord('0'))
i += 1
val.add(idgetter)
lastNum = id + 1
@@ -44,6 +47,8 @@ template formatStr*(howExpr, namegetter, idgetter): untyped =
while i < how.len and how[i] != '}':
name.add(how[i])
i += 1
if i >= how.len or how[i] != '}':
raise newException(ValueError, "Syntax error in format string at " & $i)
i += 1
val.add(namegetter)
else:

View File

@@ -14,9 +14,15 @@ block: # replace
check("123".replace(re"(\d)(\d)", "$#$#") == "123")
check("123".replace(re"(?<foo>\d)(\d)", "$foo$#$#") == "1123")
check("123".replace(re"(?<foo>\d)(\d)", "${foo}$#$#") == "1123")
check("abcdefghijklm".replace(re"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)(m)", "$12") == "l")
block: # replacing missing captures should throw instead of segfaulting
expect IndexDefect: discard "ab".replace(re"(a)|(b)", "$1$2")
expect IndexDefect: discard "b".replace(re"(a)?(b)", "$1$2")
expect KeyError: discard "b".replace(re"(a)?", "${foo}")
expect KeyError: discard "b".replace(re"(?<foo>a)?", "${foo}")
block: # malformed replacement syntax should throw instead of OOB crash
expect ValueError: discard "a".replace(re"a", "$")
expect ValueError: discard "a".replace(re"a", "x$")
expect ValueError: discard "a".replace(re"a", "${foo")