diff --git a/.jules/bolt.md b/.jules/bolt.md index 83cc604..64677a3 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2024-05-24 - Loop Allocation Hot Paths **Learning:** Rendering directory entries with repeated string concatenation and list-based exclusion lookups creates avoidable allocation and lookup cost in large directories. **Action:** Use `StringBuilder` for entry rendering and a `Set` for excluded file names. + +## 2024-07-03 - Inefficient String Escaping +**Learning:** Chaining `replace` calls on a string in Kotlin allocates multiple intermediate strings and causes O(N) allocation overhead for a simple escaping process. +**Action:** Replace multiple `replace` calls with a single-pass `StringBuilder` loop that checks if escaping is needed first, drastically reducing allocation and execution time. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 2e2809f..5a0a573 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -23,12 +23,13 @@ fun main(args: Array) = Html4tree().main(args) fun go(topDir: String, maxLevel: Int) { require(topDir.isNotBlank()) - val top_dir = File(topDir).canonicalFile + val top_dir = File(topDir) require(Files.isDirectory(top_dir.toPath(), LinkOption.NOFOLLOW_LINKS)) { "Top directory must be an existing non-symlink directory" } + val canonical_top_dir = top_dir.canonicalFile val ll = LinkedList() - ll.push(LinkedListEntry(top_dir,0)) + ll.push(LinkedListEntry(canonical_top_dir,0)) var lle: LinkedListEntry? = ll.pull() @@ -49,12 +50,30 @@ fun go(topDir: String, maxLevel: Int) { } fun String.escapeHtml(): String { - return this.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - .replace("`", "`") + var hasSpecial = false + for (i in 0 until length) { + val c = this[i] + if (c == '&' || c == '<' || c == '>' || c == '"' || c == '\'' || c == '`') { + hasSpecial = true + break + } + } + if (!hasSpecial) return this + + val sb = StringBuilder(length + 16) + for (i in 0 until length) { + val c = this[i] + when (c) { + '&' -> sb.append("&") + '<' -> sb.append("<") + '>' -> sb.append(">") + '"' -> sb.append(""") + '\'' -> sb.append("'") + '`' -> sb.append("`") + else -> sb.append(c) + } + } + return sb.toString() } fun String.urlEncodePath(): String { diff --git a/src/test/kotlin/html4tree/EscapeHtmlTest.kt b/src/test/kotlin/html4tree/EscapeHtmlTest.kt new file mode 100644 index 0000000..8ce8925 --- /dev/null +++ b/src/test/kotlin/html4tree/EscapeHtmlTest.kt @@ -0,0 +1,24 @@ +package html4tree + +import org.junit.Test +import kotlin.test.assertEquals + +class EscapeHtmlTest { + @Test + fun testEscapeHtmlNoSpecial() { + val s = "no special chars" + assertEquals("no special chars", s.escapeHtml()) + } + + @Test + fun testEscapeHtmlAllSpecial() { + val s = "&<>\"'`" + assertEquals("&<>"'`", s.escapeHtml()) + } + + @Test + fun testEscapeHtmlMixed() { + val s = "a&bd\"e'f`g" + assertEquals("a&b<c>d"e'f`g", s.escapeHtml()) + } +}