diff --git a/.jules/bolt.md b/.jules/bolt.md index 67f16f5..30e5e58 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -34,3 +34,6 @@ ## 2024-08-01 - URL 인코딩 빌더 지연 생성 **학습:** URL 인코딩이 필요 없는 안전한 경로 문자열에서도 항상 `StringBuilder`를 생성하면 hot path에서 불필요한 할당이 발생합니다. **조치:** 예약 바이트를 처음 만났을 때만 `StringBuilder`를 만들고, 그 전까지는 원본 문자열을 그대로 반환하는 지연 생성 패턴을 사용합니다. +## 2024-07-13 - [디렉토리 순회 및 I/O 호출 성능 최적화] +**Learning:** 디렉토리 순회 시 `File.list()`나 `File.listFiles()`를 반복 호출하면 불필요한 시스템 I/O 오버헤드가 크게 발생합니다. 또한 `.toPath()` 같은 객체를 반복문 내부에서 조건 검사 시 매번 생성하는 것은 GC 오버헤드를 유발합니다. +**Action:** `dirFilesNames ?: curr_dir.list()` 호출 결과를 변수에 캐싱하여 재사용하고, 반환된 배열이나 콜렉션을 순회할 때는 `Path` 객체 등의 인스턴스를 루프 밖이나 루프 최상단에서 한 번만 할당(`val path = it.toPath()`)하도록 최적화해야 합니다. 특히 패턴 매칭이나 필터링 작업이 비어있을 때는 조기에 종료(Short-circuit)하여 불필요한 반복문을 피합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index c606e58..25fc9df 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -184,6 +184,9 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S val files_to_exclude = mutableSetOf() + // ⚡ Bolt Performance Optimization: Cache dirFilesNames to avoid redundant I/O calls + val cachedDirFilesNames = dirFilesNames ?: curr_dir.list() + // 보안 향상: .html4ignore 파일이 일반 파일인지 확인하고, 심볼릭 링크인 경우 무시하여 DoS 및 경로 조작을 방지합니다. // 보안 향상: 파일 크기(1MB 제한) 및 줄 수(1000줄), 정규식 길이(100자)를 제한하여 ReDoS 및 메모리 고갈(OOM) 방지 // 보안 향상: 권한이 없는 파일 접근 시 발생하는 예외(DoS)를 방지하기 위해 canRead() 추가 확인 @@ -204,16 +207,18 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S } } - // ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다. - val list = dirFilesNames ?: curr_dir.list() - list?.forEach { - val current = it - val pathCurrent = java.nio.file.Paths.get(current) - for (matcher in ignored_matchers) { - if (matcher.matches(pathCurrent)) { - files_to_exclude.add(current) - break - } + // ⚡ Bolt Performance Optimization: Avoid iteration if there are no valid patterns + if (ignored_matchers.isNotEmpty()) { + // ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다. + cachedDirFilesNames?.forEach { + val current = it + val pathCurrent = java.nio.file.Paths.get(current) + for (matcher in ignored_matchers) { + if (matcher.matches(pathCurrent)) { + files_to_exclude.add(current) + break + } + } } } } @@ -226,7 +231,8 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S files_to_exclude.addAll(defaultSensitiveFiles) // 보안 향상: .env, .git 등 민감한 정보가 포함될 수 있는 숨김 파일(.으로 시작하는 모든 항목)을 기본적으로 노출하지 않도록 제외 (정보 노출 방지) - (dirFilesNames ?: curr_dir.list())?.forEach { + // ⚡ Bolt Performance Optimization: Iterate over cached directory files instead of querying I/O + cachedDirFilesNames?.forEach { if (it.startsWith(".")) { files_to_exclude.add(it) } @@ -350,8 +356,10 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array // ⚡ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls // 🛡️ Sentinel: Ignore hidden files/directories to prevent sensitive data exposure if (!fileName.startsWith(".") && fileName !in exclude) { - val isLinkedDirectory = Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) - if ((isLinkedDirectory || !it.isDirectory()) && !Files.isSymbolicLink(it.toPath())) { + // ⚡ Bolt Performance Optimization: Cache Path object to avoid multiple instantiations + val path = it.toPath() + val isLinkedDirectory = Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) + if ((isLinkedDirectory || !it.isDirectory()) && !Files.isSymbolicLink(path)) { val encodedHref = if (isLinkedDirectory) { "./${fileName.urlEncodePath()}/" } else { "./${fileName.urlEncodePath()}" } val ariaLabel = "${fileName} ${if (isLinkedDirectory) { "디렉토리" } else { "파일" }}".escapeHtml() val icon = if (isLinkedDirectory) { "📁" } else { "📄" } diff --git a/src/test/kotlin/html4tree/CoverageTest.kt b/src/test/kotlin/html4tree/CoverageTest.kt index dccf304..250597a 100644 --- a/src/test/kotlin/html4tree/CoverageTest.kt +++ b/src/test/kotlin/html4tree/CoverageTest.kt @@ -19,4 +19,14 @@ class CoverageTest { readOnlyDir.setWritable(true, false) } } + + @Test + fun testProcessIgnoreFileEmptyMatchers() { + val tempDir = java.nio.file.Files.createTempDirectory("test_empty_matchers").toFile() + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("\n \n") // Empty or blank lines to trigger empty matchers + val excluded = process_ignore_file(tempDir, null) + assertTrue(excluded.contains("index.html")) + tempDir.deleteRecursively() + } }