Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)하여 불필요한 반복문을 피합니다.
34 changes: 21 additions & 13 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S

val files_to_exclude = mutableSetOf<String>()

// ⚡ 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() 추가 확인
Expand All @@ -204,16 +207,18 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = 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
}
}
}
}
}
Expand All @@ -226,7 +231,8 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = 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)
}
Expand Down Expand Up @@ -350,8 +356,10 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = 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) { "&#128193;" } else { "&#128196;" }
Expand Down
10 changes: 10 additions & 0 deletions src/test/kotlin/html4tree/CoverageTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Loading