From 7bcf23fb979a23b84daae2475438862f4cfd2f33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 13 Jul 2026 02:30:11 +0900 Subject: [PATCH 1/4] perf: reuse directory snapshots during traversal --- .jules/bolt.md | 4 ++++ src/main/kotlin/html4tree/main.kt | 31 +++++++++++++++++++-------- src/test/kotlin/html4tree/MainTest.kt | 28 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 3b1d19e..96fefb5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,3 +26,7 @@ ## 2024-07-28 - 디렉토리 목록 불필요한 정렬 오버헤드 **학습:** 디렉토리 목록(`list()` 또는 `listFiles()`)을 단순히 필터링하여 `Set`에 추가하는 경우처럼 특정 순서가 필요하지 않은 작업에서 `.sorted()`를 호출하면 불필요한 O(N log N) 오버헤드가 발생합니다. **조치:** `Set`과 같은 순서에 무관한 자료구조에 요소를 추가하기 위한 필터링 작업에서는 디렉토리 목록에서 `.sorted()` 호출을 제거하여 성능을 최적화합니다. + +## 2026-07-13 - 디렉토리 스냅샷 재사용 +**학습:** 동일 디렉토리를 렌더링, ignore 매칭, 하위 탐색 단계마다 다시 열거하면 파일 시스템 호출이 중복되고 각 단계가 서로 다른 시점의 목록을 관찰할 수 있습니다. +**조치:** 방문한 디렉토리마다 불변 `List` 스냅샷과 ignore 결과를 한 번 만들고 세 단계에서 재사용하되, 직접 호출하는 기존 API에는 안전한 기본 인자를 유지합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index c14010f..ceccdad 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -51,12 +51,16 @@ fun go(topDir: String, maxLevel: Int) { while(lle != null && Files.isDirectory(lle.file.toPath(), LinkOption.NOFOLLOW_LINKS)){ val currentLevel: Int = lle.level + // Capture one immutable directory snapshot for rendering, ignore matching, + // and traversal. This avoids repeated filesystem enumeration and keeps all + // decisions for a visited directory internally consistent. + val dirFiles = directory_snapshot(lle.file) + val exclude = process_ignore_file(lle.file, dirFiles) if(maxLevel == -1 || currentLevel <= maxLevel) - process_dir(lle.file) + process_dir(lle.file, dirFiles, exclude) if(maxLevel == -1 || currentLevel < maxLevel) { - val exclude = process_ignore_file(lle.file) - lle.file.listFiles()?.forEach { + dirFiles.forEach { if(Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(it.toPath()) && it.name !in exclude) { ll.push( LinkedListEntry(it, currentLevel+1)) } @@ -121,7 +125,13 @@ fun String.urlEncodePath(): String { return encoded.toString() } -fun process_ignore_file(curr_dir: File): Set { +fun directory_snapshot(curr_dir: File): List = + curr_dir.listFiles()?.toList() ?: emptyList() + +fun process_ignore_file( + curr_dir: File, + dirFiles: List = directory_snapshot(curr_dir) +): Set { val ignore_filename = ".html4ignore" @@ -152,8 +162,8 @@ fun process_ignore_file(curr_dir: File): Set { } // ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다. - curr_dir.list()?.forEach { - val current = it + dirFiles.forEach { + val current = it.name val pathCurrent = java.nio.file.Paths.get(current) for (matcher in ignored_matchers) { if (matcher.matches(pathCurrent)) { @@ -185,9 +195,12 @@ fun write_index_file(curr_dir: File, content: String) { } } -fun process_dir(curr_dir: File){ +fun process_dir( + curr_dir: File, + dirFiles: List = directory_snapshot(curr_dir), + exclude: Set = process_ignore_file(curr_dir, dirFiles) +){ - val exclude: Set = process_ignore_file(curr_dir) val styleNonce = generate_csp_nonce() val css = """ @@ -265,7 +278,7 @@ fun process_dir(curr_dir: File){ val index_middle = fun():String{ val l = StringBuilder() - val dir_files: MutableList = curr_dir.listFiles()?.toMutableList() ?: mutableListOf() + val dir_files: MutableList = dirFiles.toMutableList() dir_files.sortWith(compareBy ({it.name}) ) dir_files.forEach { val fileName = it.getName() diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index eaa855d..f11ac0c 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -498,4 +498,32 @@ class MainTest { // Line 1001 should be ignored due to line limit assertFalse(excluded.contains("test.txt1001")) } + + @Test + fun testProcessIgnoreFileUsesProvidedDirectorySnapshot() { + File(tempDir, ".html4ignore").writeText("*.txt") + val visibleAtSnapshot = File(tempDir, "visible.txt") + visibleAtSnapshot.createNewFile() + val snapshot = tempDir.listFiles()?.toList() ?: emptyList() + File(tempDir, "created-later.txt").createNewFile() + + val excluded = process_ignore_file(tempDir, snapshot) + + assertTrue(excluded.contains("visible.txt")) + assertFalse(excluded.contains("created-later.txt")) + } + + @Test + fun testProcessDirUsesProvidedDirectorySnapshot() { + val visibleAtSnapshot = File(tempDir, "visible.txt") + visibleAtSnapshot.createNewFile() + val snapshot = tempDir.listFiles()?.toList() ?: emptyList() + File(tempDir, "created-later.txt").createNewFile() + + process_dir(tempDir, snapshot) + + val html = File(tempDir, "index.html").readText() + assertTrue(html.contains("visible.txt")) + assertFalse(html.contains("created-later.txt")) + } } From 4dfb9ed22a7add98dffcef9a150669fcd44937e9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:32:14 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20I/O=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 50 +++----- .jules/palette.md | 7 -- .jules/sentinel.md | 16 +-- src/main/kotlin/html4tree/main.kt | 108 ++++++------------ src/test/kotlin/html4tree/CoverageTest.kt | 22 ---- src/test/kotlin/html4tree/MainTest.kt | 133 ++++++---------------- 6 files changed, 90 insertions(+), 246 deletions(-) delete mode 100644 src/test/kotlin/html4tree/CoverageTest.kt diff --git a/.jules/bolt.md b/.jules/bolt.md index 96fefb5..5cf5ec7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,32 +1,18 @@ -## 2024-06-21 - 루프 내 정규식 컴파일 -**학습:** Kotlin에서 무시 파일을 처리할 때 파일 반복 루프 내에서 정규식(`.toRegex()`)을 컴파일하는 것은 O(N * M)의 심각한 성능 병목을 일으킵니다 (N 파일 * M 규칙). -**조치:** 불필요한 정규식 재컴파일을 피하기 위해 항상 파일 반복 루프 외부에서 문자열 규칙을 컴파일된 `Regex` 객체로 매핑합니다 (O(M) 컴파일). - -## 2024-05-24 - 루프 내 할당 핫 패스 -**학습:** 디렉토리 항목을 렌더링할 때 반복적인 문자열 연결과 리스트 기반 제외 조회를 사용하면 대규모 디렉토리에서 불필요한 할당 및 조회 비용이 발생합니다. -**조치:** 항목 렌더링에 `StringBuilder`를 사용하고 제외된 파일 이름에 대해 `Set`을 사용합니다. - -## 2024-07-26 - 중간 문자열 할당 -**학습:** Kotlin에서 문자열에 연결된 `.replace()` 호출 (예: HTML 이스케이프)은 각 단계에서 중간 문자열을 할당하여 요소가 많은 핫 패스에서 성능 및 가비지 컬렉션에 큰 영향을 미칩니다. -**조치:** 연결된 `.replace()` 호출을 문자를 한 번만 반복하는 단일 패스 루프로 바꾸고, 변환된 출력을 추가하기 위해 `StringBuilder`를 지연 초기화합니다. - -## 2024-07-08 - URL 인코딩 문자열 할당 병목 -**학습:** 핫 패스 루프 내에서 예약된 바이트당 최대 3개의 문자열을 할당하는 `byte.toString(16).padStart(2, '0').toUpperCase()`는 상당한 GC 압력을 유발합니다. 이는 디렉토리 크롤러에서 대규모 문자열이나 수많은 파일을 처리할 때 Kotlin에서 흔히 볼 수 있는 위험한 안티 패턴입니다. -**조치:** 중간 문자열 생성을 완전히 피하기 위해 포맷된 16진수 출력을 작성할 때 연결된 문자열 연산을 직접 문자 매핑 및 비트 연산(`ushr`, `and`)으로 바꿉니다. 10 미만 및 9 초과의 16진수 값을 모두 포괄하는 테스트 입력을 통해 100% 브랜치 커버리지를 보장합니다. - -## 2026-07-10 - 저렴한 메모리 내 검사 전 비싼 OS stat 호출 -**학습:** Kotlin/Java에서 `java.nio.file.Files`를 통해 파일 속성 (예: `isDirectory` 또는 `isSymbolicLink`)을 확인하려면 `Path` 객체를 할당해야 하며 비싼 네이티브 OS stat 호출을 수행합니다. 파일 목록을 처리할 때 파일 시스템을 건드리는 메서드를 호출하기 전에 제외 목록 (저렴한 메모리 내 문자열 연산 사용)과 비교하여 파일 시스템 검사를 단축합니다. -**조치:** `Files.isDirectory` 및 `Files.isSymbolicLink`를 호출하기 전에 `exclude` 세트를 확인하도록 조건문을 재배열했습니다. - - -## 2026-07-12 - 이중 루프 내 패턴 매칭 조기 종료 (Short-Circuit) -**학습:** 무시할 파일(ignore patterns)을 확인할 때, 각 파일에 대해 모든 패턴을 순회(`forEach`)하는 것은 비효율적입니다. 파일이 하나의 패턴에 매칭되어 제외 목록에 추가되면 나머지 패턴을 확인할 필요가 없습니다. 이를 조기 종료(Short-circuit)하지 않으면 불필요한 O(N * M) 정규식/패턴 매칭 평가가 발생합니다. -**조치:** 무시 목록 평가 등 조건을 만족할 때 더 이상 확인이 필요 없는 경우에는 `forEach` 대신 일반 `for` 루프와 `break`를 사용하거나 `any`를 활용하여 연산을 단축합니다. - -## 2024-07-28 - 디렉토리 목록 불필요한 정렬 오버헤드 -**학습:** 디렉토리 목록(`list()` 또는 `listFiles()`)을 단순히 필터링하여 `Set`에 추가하는 경우처럼 특정 순서가 필요하지 않은 작업에서 `.sorted()`를 호출하면 불필요한 O(N log N) 오버헤드가 발생합니다. -**조치:** `Set`과 같은 순서에 무관한 자료구조에 요소를 추가하기 위한 필터링 작업에서는 디렉토리 목록에서 `.sorted()` 호출을 제거하여 성능을 최적화합니다. - -## 2026-07-13 - 디렉토리 스냅샷 재사용 -**학습:** 동일 디렉토리를 렌더링, ignore 매칭, 하위 탐색 단계마다 다시 열거하면 파일 시스템 호출이 중복되고 각 단계가 서로 다른 시점의 목록을 관찰할 수 있습니다. -**조치:** 방문한 디렉토리마다 불변 `List` 스냅샷과 ignore 결과를 한 번 만들고 세 단계에서 재사용하되, 직접 호출하는 기존 API에는 안전한 기본 인자를 유지합니다. +## 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. + +## 2024-07-26 - Intermediate String Allocations +**Learning:** Chained `.replace()` calls on strings in Kotlin (e.g. for HTML escaping) allocate an intermediate String at each step, significantly impacting performance and garbage collection on hot paths with many elements. +**Action:** Replace chained `.replace()` calls with a single-pass loop that iterates over characters once, lazily initializing a `StringBuilder` to append the transformed output. +## 2024-07-08 - URL Encoding String Allocation Bottleneck +**Learning:** `byte.toString(16).padStart(2, '0').toUpperCase()` inside a loop allocating up to 3 strings per reserved byte in a hot path causes significant GC pressure. This is a common but dangerous anti-pattern in Kotlin when processing large strings or numerous files in directory crawlers. +**Action:** Replace chained string operations with direct character mapping and bitwise operations (`ushr`, `and`) when building formatted hex output, which avoids intermediate string creation entirely. Ensure 100% branch coverage with test inputs spanning both < 10 and > 9 hex values. +## 2026-07-10 - Expensive OS stat calls before cheap in-memory checks\n**Learning:** In Kotlin/Java, checking file properties (like `isDirectory` or `isSymbolicLink`) via `java.nio.file.Files` requires allocating `Path` objects and performs expensive native OS stat calls. When processing file listings, always short-circuit filesystem checks by testing against exclusion lists (using cheap in-memory string operations) before calling methods that touch the filesystem.\n**Action:** Re-ordered conditionals to check `exclude` sets before invoking `Files.isDirectory` and `Files.isSymbolicLink`. +## 2024-07-11 - [Eliminating Redundant Directory Listings] +**Learning:** In nested file processing functions like `process_dir` and `process_ignore_file`, executing `curr_dir.listFiles()` repeatedly for the same directory is highly inefficient and creates expensive duplicate OS syscalls. +**Action:** Always hoist I/O intensive listing operations like `listFiles()` to the highest appropriate scope and pass the result as a parameter to child functions rather than re-evaluating it. diff --git a/.jules/palette.md b/.jules/palette.md index 0db5d64..4ee7e42 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -17,10 +17,3 @@ ## 2026-07-10 - ARIA Label Language Matching **Learning:** Screen readers rely on the document's language attribute (`lang="ko"`) to select the appropriate pronunciation engine. If semantic descriptors like `aria-label` are provided in a language different from the document language (e.g., English text in a Korean document), they may be mispronounced or skipped entirely, breaking accessibility. **Action:** Always ensure that `aria-label`, `alt` text, and other hidden semantic text strings match the language specified in the document's `` tag for proper localization and screen reader compatibility. -## 2026-07-11 - Safari VoiceOver List Semantics Fix -**Learning:** Using `list-style: none` or `list-style-type: none` in CSS causes Safari (VoiceOver) to remove list semantics entirely, which harms accessibility for screen reader users trying to navigate directory listings. -**Action:** Always explicitly add `role="list"` to `
    ` or `
      ` elements when removing their default list styling to preserve accessibility semantics across all browsers. - -## 2026-07-12 - Inline Colors vs Dark Mode -**Learning:** Hardcoded inline colors (like `color: #666`) fail to adapt in dark mode because they bypass CSS media queries and cannot be easily overridden without `!important`, causing text legibility issues on dark backgrounds. -**Action:** Use `opacity: 0.7` instead of hardcoded hex values for muted text to dynamically inherit theme colors and ensure legibility across all color schemes. diff --git a/.jules/sentinel.md b/.jules/sentinel.md index aba452c..a21dad7 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -18,10 +18,10 @@ **Learning:** `File.listFiles()` returns null (not empty) on unreadable directories. Directory crawlers must reject symlink roots, skip symlink children, and avoid enqueueing paths deeper than the configured traversal limit. **Prevention:** Use `java.nio.file.Files.isSymbolicLink(file.toPath())` for root and child directory checks, gracefully handle `null` arrays from `listFiles()` and `list()`, and only enqueue child directories when the current level is still below `maxLevel`. -## 2024-06-28 - [html4tree] 정적 HTML 생성 보안 -**Vulnerability:** 심층 방어 누락 (CSP 누락) -**Learning:** 입력값이 적절히 이스케이프 되더라도, 파일/디렉토리 구조를 표시하는 정적 생성 HTML은 잠재적인 XSS 우회 공격에 대비하여 추가적인 방어 계층을 제공하는 콘텐츠 보안 정책(CSP)을 반드시 구현해야 합니다. -**Prevention:** 외부 스크립트나 리소스가 필요하지 않은 경우 자동 생성되는 HTML 헤더에 엄격한 CSP 메타 태그(예: `default-src 'none'; style-src 'nonce-...'; base-uri 'none'; form-action 'none';`)를 포함하고, 스타일 블록에는 일회성 nonce를 부여하십시오. +## 2024-06-28 - [html4tree] Static HTML Generation Security +**Vulnerability:** Defense in Depth (CSP Missing) +**Learning:** Even when inputs are properly escaped, statically generated HTML that displays file/directory structures should implement a Content Security Policy (CSP) to provide an extra layer of defense against potential XSS bypasses. +**Prevention:** Include a strict CSP meta tag (e.g., `default-src 'none'; style-src 'unsafe-inline';`) in auto-generated HTML headers when external scripts or resources are not required. ## 2024-05-31 - [CRITICAL] 심볼릭 링크 검사 우회 취약점 (canonicalFile) **Vulnerability:** `File(path).canonicalFile`를 사용하여 심볼릭 링크 여부를 검사하면, `canonicalFile` 함수 내부에서 심볼릭 링크를 이미 대상(target) 파일의 실제 경로로 해석(resolve)해 버리기 때문에, 이후에 진행되는 `Files.isDirectory(..., LinkOption.NOFOLLOW_LINKS)` 등의 심볼릭 링크 제한 검사가 완전히 무력화되는 취약점이 발견되었습니다. @@ -45,11 +45,3 @@ **Vulnerability:** The `.html4ignore` parser still allowed excessively large files and root-directory crawls could generate unbounded filesystem output. **Learning:** Local CLI configuration inputs and traversal roots need explicit resource ceilings, not only syntactic validation. **Prevention:** Limit `.html4ignore` file size, parsed line count, compiled pattern count, and regex length; reject filesystem root traversal using `File.parentFile != null`. -## 2024-07-11 - [.html4ignore 파일의 ReDoS 취약점 수정 (Glob 패턴 적용)] -**Vulnerability:** [사용자가 제공한 `.html4ignore` 패턴을 직접 정규표현식으로 컴파일하여 발생하는 ReDoS(정규표현식 서비스 거부) 취약점 발견.] -**Learning:** [필터링 패턴으로 정규표현식을 직접 노출하면 악의적으로 조작된 긴 문자열이나 복잡한 패턴을 통해 애플리케이션의 리소스를 고갈시킬 수 있음.] -**Prevention:** [사용자 입력 패턴은 정규표현식으로 변환하기 전 `java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")`와 같은 안전한 Glob 매칭 방식을 사용해야 함.] -## 2024-07-12 - [html4tree] Unhandled File/Directory Permissions causing DoS -**Vulnerability:** When encountering an unreadable `.html4ignore` file or a directory without write permissions, an unhandled `java.io.FileNotFoundException` or `java.nio.file.AccessDeniedException` was thrown, respectively. This causes the entire crawler to crash (DoS) when encountering files/directories with restricted permissions. -**Learning:** Application processes that recursively scan filesystem directories must gracefully handle permission denied exceptions to ensure that one inaccessible node does not halt the entire scanning/processing operation. -**Prevention:** Check `.canRead()` before attempting to parse configuration/ignore files, and wrap file writing operations in `try-catch` blocks to securely handle `AccessDeniedException` or other `IOException`s, failing gracefully (Fail Securely) rather than crashing the application. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index ceccdad..3141b09 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -1,11 +1,9 @@ package html4tree import java.io.File -import java.security.SecureRandom import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.StandardCopyOption -import java.util.Base64 import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.default @@ -23,17 +21,8 @@ class Html4tree : CliktCommand() { fun main(args: Array) = Html4tree().main(args) -private val nonceRandom = SecureRandom() - -fun generate_csp_nonce(): String { - val nonceBytes = ByteArray(16) - nonceRandom.nextBytes(nonceBytes) - return Base64.getEncoder().encodeToString(nonceBytes) -} - fun go(topDir: String, maxLevel: Int) { require(topDir.isNotBlank()) - require(!topDir.contains("..")) { "Path traversal sequences are not allowed." } // 보안 수정: symlink 검사를 우회하는 canonicalFile 대신 absoluteFile을 사용 // canonicalFile은 symlink를 대상 경로로 해석하여 이어지는 NOFOLLOW_LINKS 검사를 무력화합니다. val top_dir = File(topDir).absoluteFile.toPath().normalize().toFile() @@ -51,17 +40,17 @@ fun go(topDir: String, maxLevel: Int) { while(lle != null && Files.isDirectory(lle.file.toPath(), LinkOption.NOFOLLOW_LINKS)){ val currentLevel: Int = lle.level - // Capture one immutable directory snapshot for rendering, ignore matching, - // and traversal. This avoids repeated filesystem enumeration and keeps all - // decisions for a visited directory internally consistent. - val dirFiles = directory_snapshot(lle.file) - val exclude = process_ignore_file(lle.file, dirFiles) + // ⚡ Bolt Performance Optimization: Hoist listFiles() out of nested functions + // By fetching directory contents once, we eliminate redundant OS syscalls + // in process_dir and process_ignore_file (saves 2x I/O calls per directory). + val dir_files = lle.file.listFiles()?.toList() ?: emptyList() + if(maxLevel == -1 || currentLevel <= maxLevel) - process_dir(lle.file, dirFiles, exclude) + process_dir(lle.file, dir_files) if(maxLevel == -1 || currentLevel < maxLevel) { - dirFiles.forEach { - if(Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(it.toPath()) && it.name !in exclude) { + dir_files.forEach { + if(Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS)){ ll.push( LinkedListEntry(it, currentLevel+1)) } } @@ -125,13 +114,7 @@ fun String.urlEncodePath(): String { return encoded.toString() } -fun directory_snapshot(curr_dir: File): List = - curr_dir.listFiles()?.toList() ?: emptyList() - -fun process_ignore_file( - curr_dir: File, - dirFiles: List = directory_snapshot(curr_dir) -): Set { +fun process_ignore_file(curr_dir: File, dir_files: List): Set { val ignore_filename = ".html4ignore" @@ -143,9 +126,8 @@ fun process_ignore_file( // 보안 향상: .html4ignore 파일이 일반 파일인지 확인하고, 심볼릭 링크인 경우 무시하여 DoS 및 경로 조작을 방지합니다. // 보안 향상: 파일 크기(1MB 제한) 및 줄 수(1000줄), 정규식 길이(100자)를 제한하여 ReDoS 및 메모리 고갈(OOM) 방지 - // 보안 향상: 권한이 없는 파일 접근 시 발생하는 예외(DoS)를 방지하기 위해 canRead() 추가 확인 - if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.canRead() && ignore_file.length() <= 1048576){ - val ignored_matchers = mutableListOf() + if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.length() <= 1048576){ + val ignored_regexes = mutableListOf() ignore_file.useLines { lines -> for ((lineIndex, it) in lines.withIndex()) { @@ -154,23 +136,20 @@ fun process_ignore_file( val pattern = it.trim() if (pattern.isNotEmpty() && pattern.length <= 100) { try { - ignored_matchers.add(java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")) - } catch (_: java.util.regex.PatternSyntaxException) { + ignored_regexes.add(("^"+pattern+"$").toRegex()) + } catch (_: IllegalArgumentException) { } } } } - // ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다. - dirFiles.forEach { - val current = it.name - val pathCurrent = java.nio.file.Paths.get(current) - for (matcher in ignored_matchers) { - if (matcher.matches(pathCurrent)) { + dir_files.map { it.name }.sorted().forEach { + val current = it + ignored_regexes.forEach { regex -> + if(regex.matches(current)){ files_to_exclude.add(current) - break } - } + } } } @@ -195,40 +174,27 @@ fun write_index_file(curr_dir: File, content: String) { } } -fun process_dir( - curr_dir: File, - dirFiles: List = directory_snapshot(curr_dir), - exclude: Set = process_ignore_file(curr_dir, dirFiles) -){ +fun process_dir(curr_dir: File, dir_files: List){ - val styleNonce = generate_csp_nonce() + val exclude: Set = process_ignore_file(curr_dir, dir_files) val css = """ - """ @@ -263,7 +224,7 @@ fun process_dir( - + ${curr_dir.getName().escapeHtml()} ${css} @@ -271,16 +232,16 @@ fun process_dir(

      ${curr_dir.getName().escapeHtml()}