Harden DSL parsing against escapes and malformed tags
Fixed escape-aware headword parsing for \{, \}, \[ and trailing backslash.
Fixed O(n^2) tag scan and nested bracket handling in article rendering.
This commit is contained in:
@@ -24,10 +24,11 @@ object DslHeadwordParser {
|
||||
}
|
||||
|
||||
if (trimmed.indexOf('{') == -1 && trimmed.indexOf('[') == -1) {
|
||||
val unescaped = unescape(trimmed)
|
||||
return ParsedHeadword(
|
||||
simplified = trimmed,
|
||||
displayText = trimmed,
|
||||
searchableText = trimmed.lowercase()
|
||||
simplified = unescaped,
|
||||
displayText = unescaped,
|
||||
searchableText = unescaped.lowercase()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,9 +61,9 @@ object DslHeadwordParser {
|
||||
val searchableText = createSearchableText(trimmed)
|
||||
|
||||
return ParsedHeadword(
|
||||
simplified = simplified,
|
||||
displayText = displayText,
|
||||
searchableText = searchableText
|
||||
simplified = unescape(simplified),
|
||||
displayText = unescape(displayText),
|
||||
searchableText = unescape(searchableText)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -129,8 +130,17 @@ object DslHeadwordParser {
|
||||
if (value.indexOf('{') == -1 && value.indexOf('}') == -1) return value
|
||||
|
||||
return buildString(value.length) {
|
||||
value.forEach { char ->
|
||||
var index = 0
|
||||
while (index < value.length) {
|
||||
val char = value[index]
|
||||
if (char == '\\' && index + 1 < value.length) {
|
||||
append(char)
|
||||
append(value[index + 1])
|
||||
index += 2
|
||||
} else {
|
||||
if (char != '{' && char != '}') append(char)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +148,10 @@ object DslHeadwordParser {
|
||||
private fun firstFormattingTagIndex(value: String): Int {
|
||||
var index = 0
|
||||
while (index < value.length) {
|
||||
if (value[index] == '\\' && index + 1 < value.length) {
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
if (value[index] == '[' && formattingTagEnd(value, index) > index) {
|
||||
return index
|
||||
}
|
||||
@@ -149,8 +163,12 @@ object DslHeadwordParser {
|
||||
private fun firstCurlyContent(value: String): String? {
|
||||
var index = 0
|
||||
while (index < value.length) {
|
||||
if (value[index] == '\\' && index + 1 < value.length) {
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
if (value[index] == '{') {
|
||||
val end = value.indexOf('}', startIndex = index + 1)
|
||||
val end = matchingCurlyEnd(value, index + 1)
|
||||
if (end > index + 1) {
|
||||
return value.substring(index + 1, end)
|
||||
}
|
||||
@@ -161,7 +179,7 @@ object DslHeadwordParser {
|
||||
}
|
||||
|
||||
private fun substringBeforeFirstBracket(value: String): String {
|
||||
val bracketIndex = value.indexOf('[')
|
||||
val bracketIndex = indexOfUnescaped(value, '[')
|
||||
return if (bracketIndex >= 0) value.substring(0, bracketIndex) else value
|
||||
}
|
||||
|
||||
@@ -171,8 +189,12 @@ object DslHeadwordParser {
|
||||
var index = 0
|
||||
|
||||
while (index < value.length) {
|
||||
if (value[index] == '\\' && index + 1 < value.length) {
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
if (value[index] == '{') {
|
||||
val end = value.indexOf('}', startIndex = index + 1)
|
||||
val end = matchingCurlyEnd(value, index + 1)
|
||||
if (end >= 0 && (removeEmpty || end > index + 1)) {
|
||||
if (builder == null) {
|
||||
builder = StringBuilder(value.length)
|
||||
@@ -197,6 +219,10 @@ object DslHeadwordParser {
|
||||
var index = 0
|
||||
|
||||
while (index < value.length) {
|
||||
if (value[index] == '\\' && index + 1 < value.length) {
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
if (value[index] == '[') {
|
||||
val tagEnd = formattingTagEnd(value, index)
|
||||
if (tagEnd > index) {
|
||||
@@ -217,6 +243,56 @@ object DslHeadwordParser {
|
||||
}?.toString() ?: value
|
||||
}
|
||||
|
||||
private fun matchingCurlyEnd(value: String, startIndex: Int): Int {
|
||||
var index = startIndex
|
||||
while (index < value.length) {
|
||||
when {
|
||||
value[index] == '\\' && index + 1 < value.length -> index += 2
|
||||
value[index] == '{' -> return -1
|
||||
value[index] == '}' -> return index
|
||||
else -> index++
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun indexOfUnescaped(
|
||||
value: String,
|
||||
target: Char,
|
||||
startIndex: Int = 0,
|
||||
): Int {
|
||||
var index = startIndex.coerceAtLeast(0)
|
||||
while (index < value.length) {
|
||||
if (value[index] == '\\' && index + 1 < value.length) {
|
||||
index += 2
|
||||
} else if (value[index] == target) {
|
||||
return index
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun unescape(value: String): String {
|
||||
val firstEscape = value.indexOf('\\')
|
||||
if (firstEscape < 0) return value
|
||||
|
||||
return buildString(value.length) {
|
||||
append(value, 0, firstEscape)
|
||||
var index = firstEscape
|
||||
while (index < value.length) {
|
||||
if (value[index] == '\\' && index + 1 < value.length) {
|
||||
append(value[index + 1])
|
||||
index += 2
|
||||
} else {
|
||||
append(value[index])
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formattingTagEnd(value: String, openBracketIndex: Int): Int {
|
||||
val tokenStart = openBracketIndex + 1
|
||||
if (tokenStart >= value.length) return -1
|
||||
|
||||
@@ -119,7 +119,7 @@ object DslAnnotatedParser {
|
||||
i++
|
||||
}
|
||||
'[' -> {
|
||||
val end = dsl.indexOf(']', i + 1)
|
||||
val end = tagCloseIndex(dsl, i + 1)
|
||||
if (end != -1) {
|
||||
val tagStart = i + 1
|
||||
var tagEnd = end
|
||||
@@ -228,6 +228,19 @@ object DslAnnotatedParser {
|
||||
splitOversizedBlocks(result)
|
||||
}
|
||||
|
||||
private fun tagCloseIndex(value: String, startIndex: Int): Int {
|
||||
var index = startIndex
|
||||
while (index < value.length) {
|
||||
when {
|
||||
value[index] == '\\' && index + 1 < value.length -> index += 2
|
||||
value[index] == '[' -> return -1
|
||||
value[index] == ']' -> return index
|
||||
else -> index++
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun splitOversizedBlocks(blocks: List<DslBlock>): List<DslBlock> {
|
||||
if (blocks.none { it.text.length > MAX_BLOCK_TEXT_LENGTH }) return blocks
|
||||
return blocks.flatMap { block ->
|
||||
|
||||
Reference in New Issue
Block a user