diff --git a/.jules/bolt.md b/.jules/bolt.md index 39f32f6e..83cc604a 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2024-06-21 - Regex Compilation in Loops **Learning:** In Kotlin, compiling regular expressions (`.toRegex()`) inside a loop over files is a significant O(N * M) performance bottleneck when processing ignore files (N files * M rules). **Action:** Always map string rules to compiled `Regex` objects outside of the file iteration loop (O(M) compilation) to avoid unnecessary regex re-compilations. + +## 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. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 5dca48bb..036239ea 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -19,7 +19,8 @@ class Html4tree : CliktCommand() { fun main(args: Array) = Html4tree().main(args) fun go(topDir: String, maxLevel: Int) { - val top_dir = File(topDir) + require(topDir.isNotBlank()) + val top_dir = File(topDir).canonicalFile require(top_dir.exists() && top_dir.isDirectory()) val ll = LinkedList() @@ -48,13 +49,31 @@ fun String.escapeHtml(): String { .replace(">", ">") .replace("\"", """) .replace("'", "'") + .replace("`", "`") } fun String.urlEncodePath(): String { - return java.net.URLEncoder.encode(this, "UTF-8").replace("+", "%20") + val encoded = StringBuilder() + this.toByteArray(Charsets.UTF_8).forEach { + val byte = it.toInt() and 0xff + val isUnreserved = (byte in 'A'.toInt()..'Z'.toInt()) || + (byte in 'a'.toInt()..'z'.toInt()) || + (byte in '0'.toInt()..'9'.toInt()) || + byte == '-'.toInt() || + byte == '.'.toInt() || + byte == '_'.toInt() || + byte == '~'.toInt() + if (isUnreserved) { + encoded.append(byte.toChar()) + } else { + encoded.append('%') + encoded.append(byte.toString(16).padStart(2, '0').toUpperCase()) + } + } + return encoded.toString() } -fun process_ignore_file(curr_dir: File): List { +fun process_ignore_file(curr_dir: File): Set { val ignore_filename = ".html4ignore" @@ -62,12 +81,20 @@ fun process_ignore_file(curr_dir: File): List { val ignore_file = File(ignore_file_path) - val files_to_exclude = mutableListOf() + val files_to_exclude = mutableSetOf() if(ignore_file.exists()){ val ignored_regexes = mutableListOf() - ignore_file.forEachLine { ignored_regexes.add(("^"+it+"$").toRegex()) } + ignore_file.forEachLine { + val pattern = it.trim() + if (pattern.isNotEmpty()) { + try { + ignored_regexes.add(("^"+pattern+"$").toRegex()) + } catch (_: IllegalArgumentException) { + } + } + } curr_dir.list().sorted().forEach { val current = it @@ -87,7 +114,7 @@ fun process_ignore_file(curr_dir: File): List { fun process_dir(curr_dir: File){ - val exclude: List = process_ignore_file(curr_dir) + val exclude: Set = process_ignore_file(curr_dir) val css = """