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 @@ -43,3 +43,6 @@
## 2025-01-24 - 단일 readAttributes 호출로 파일 속성 조회 최적화
**학습:** `isDirectory`, `!it.isDirectory()`, `isSymbolicLink` 3개의 개별적인 파일 시스템 I/O 호출을 수행하면 성능 저하가 큽니다. 이를 단일 `Files.readAttributes` 호출로 변경하여 메타데이터를 한 번에 조회함으로써 I/O 오버헤드를 대폭 줄일 수 있음을 확인했습니다.
**조치:** 디렉토리 순회 시 파일의 여러 속성을 확인할 때는 개별적인 stat 호출보다 `Files.readAttributes`를 사용하여 필요한 모든 속성을 한 번에 가져오는 방식을 우선적으로 고려해야 합니다.
## 2026-08-05 - 루프 내 정적 객체 반복 할당 최적화
**Learning:** 디렉토리 순회 루프(예: `process_dir`, `process_ignore_file`) 내부에 큰 상수 문자열이나 고정된 설정 리스트(예: `defaultSensitiveFiles`)를 지역 변수로 선언하면, 매 호출마다 객체가 반복적으로 할당되어 메모리 낭비와 GC 압박을 유발합니다. 더불어 Kotlin에서 정적 속성을 추출할 때 암시적 getter로 인한 커버리지 하락을 막으려면 `private object` 내부에 `const val` 혹은 `@JvmField`를 활용해야 합니다.
**Action:** 함수 호출마다 생성될 필요가 없는 고정 데이터(큰 문자열, 설정 리스트, 정해진 해시 연산 등)는 `private object`로 분리하여 애플리케이션 수명 주기 동안 한 번만 할당되도록 합니다.
45 changes: 24 additions & 21 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.int

private val CSS_CONTENT = """
body {
private object Constants {
const val CSS_CONTENT = """body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
padding: 1rem;
Expand Down Expand Up @@ -79,10 +79,24 @@ a:hover, a:focus-visible {
padding: 0.5rem;
opacity: 0.7;
font-style: italic;
}
""".trimIndent()
}"""

@JvmField
val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))

@JvmField
val DEFAULT_SENSITIVE_FILES = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json")

private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))
const val IGNORE_FILENAME = ".html4ignore"

const val INDEX_BOTTOM = """
</ul>
</nav>
</main>
</body>
</html>
"""
}

class Html4tree : CliktCommand() {
val maxLevel:Int by option(help="Number of levels deep for which to generate an index.html file", hidden = false).int().default(-1)
Expand Down Expand Up @@ -241,9 +255,7 @@ fun String.urlEncodePath(): String {

fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): Set<String> {

val ignore_filename = ".html4ignore"

val ignore_file_path = curr_dir.getAbsolutePath()+"/"+ignore_filename
val ignore_file_path = curr_dir.getAbsolutePath()+"/"+Constants.IGNORE_FILENAME

val ignore_file = File(ignore_file_path)

Expand Down Expand Up @@ -287,8 +299,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
files_to_exclude.add("index.html")

// 보안 향상: 민감한 시스템, 설정, 시크릿 파일을 디렉토리 목록에서 기본적으로 제외하여 정보 노출(Information Exposure) 방지
val defaultSensitiveFiles = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json")
files_to_exclude.addAll(defaultSensitiveFiles)
files_to_exclude.addAll(Constants.DEFAULT_SENSITIVE_FILES)

// 보안 향상: .env, .git 등 민감한 정보가 포함될 수 있는 숨김 파일(.으로 시작하는 모든 항목)을 기본적으로 노출하지 않도록 제외 (정보 노출 방지)
(dirFilesNames ?: curr_dir.list())?.forEach {
Expand Down Expand Up @@ -322,11 +333,11 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<!-- 보안 향상: 인라인 스크립트 실행 방지 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src '${STYLE_HASH}'; base-uri 'none'; form-action 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src '${Constants.STYLE_HASH}'; base-uri 'none'; form-action 'none';">
<!-- 보안 향상: 리퍼러를 통한 디렉토리 경로 노출 방지 -->
<meta name="referrer" content="no-referrer">
<title>${curr_dir.getName().escapeHtml()}</title>
<style>${CSS_CONTENT}</style>
<style>${Constants.CSS_CONTENT}</style>
</head>
<body>
<main>
Expand Down Expand Up @@ -375,16 +386,8 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
return l.toString();
}

val index_bottom="""
</ul>
</nav>
</main>
</body>
</html>
"""

try {
write_index_file(curr_dir, index_top+index_middle()+index_bottom)
write_index_file(curr_dir, index_top+index_middle()+Constants.INDEX_BOTTOM)
} catch (e: Exception) {
// 보안 향상: 디렉토리에 쓰기 권한이 없거나 파일 시스템 오류가 발생했을 때
// 전체 크롤링(프로세스)이 중단되는 DoS를 방지합니다. (Fail Securely)
Expand Down
Loading