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: 0 additions & 3 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,3 @@
## 2025-01-24 - 단일 readAttributes 호좜둜 파일 속성 쑰회 μ΅œμ ν™”
**ν•™μŠ΅:** `isDirectory`, `!it.isDirectory()`, `isSymbolicLink` 3개의 κ°œλ³„μ μΈ 파일 μ‹œμŠ€ν…œ I/O ν˜ΈμΆœμ„ μˆ˜ν–‰ν•˜λ©΄ μ„±λŠ₯ μ €ν•˜κ°€ ν½λ‹ˆλ‹€. 이λ₯Ό 단일 `Files.readAttributes` 호좜둜 λ³€κ²½ν•˜μ—¬ 메타데이터λ₯Ό ν•œ λ²ˆμ— μ‘°νšŒν•¨μœΌλ‘œμ¨ I/O μ˜€λ²„ν—€λ“œλ₯Ό λŒ€ν­ 쀄일 수 μžˆμŒμ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.
**쑰치:** 디렉토리 순회 μ‹œ 파일의 μ—¬λŸ¬ 속성을 확인할 λ•ŒλŠ” κ°œλ³„μ μΈ stat ν˜ΈμΆœλ³΄λ‹€ `Files.readAttributes`λ₯Ό μ‚¬μš©ν•˜μ—¬ ν•„μš”ν•œ λͺ¨λ“  속성을 ν•œ λ²ˆμ— κ°€μ Έμ˜€λŠ” 방식을 μš°μ„ μ μœΌλ‘œ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€.
## 2025-01-24 - 단일 readAttributes 호좜둜 파일 속성 쑰회 μ΅œμ ν™” (순회 루프)
**ν•™μŠ΅:** 디렉토리 순회 루프 λ‚΄μ—μ„œ isDirectory 및 isSymbolicLink 두 번의 stat을 각각 ν˜ΈμΆœν•˜λ©΄ 파일 μ‹œμŠ€ν…œ I/O μ˜€λ²„ν—€λ“œκ°€ λ°°κ°€λ©λ‹ˆλ‹€. λ©”λͺ¨λ¦¬ λ‚΄ μ œμ™Έ κ·œμΉ™ 확인 ν›„ ν•œ 번의 readAttributes둜 속성을 ν•œ λ²ˆμ— κ°€μ Έμ˜€λŠ” 것이 훨씬 λΉ λ¦…λ‹ˆλ‹€.
**쑰치:** Files.isDirectory 및 Files.isSymbolicLinkλ₯Ό 단일 Files.readAttributes 호좜둜 κ΅μ²΄ν•˜μ—¬ O(N) I/O 톡신을 μ΅œμ ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@
## 2024-07-13 - 빈 디렉토리 μƒνƒœμ˜ μ ‘κ·Όμ„±(Accessibility) κ°œμ„ 
**Learning:** 정적 파일 μ„œλ²„μ˜ 빈 디렉토리 μƒνƒœλŠ” 슀크린 리더 μ‚¬μš©μžμ—κ²Œ 컨텐츠 λˆ„λ½μœΌλ‘œ μ˜€ν•΄λ°›μ„ 수 있으며, μ‹œκ°μ μœΌλ‘œλ„ 일반 리슀트 μ•„μ΄ν…œκ³Ό 정렬이 λ§žμ§€ μ•ŠλŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
**Action:** 빈 μƒνƒœλ₯Ό λ‚˜νƒ€λ‚΄λŠ” μš”μ†Œμ— `role="status"`λ₯Ό μΆ”κ°€ν•˜μ—¬ 슀크린 리더가 λͺ…ν™•ν•˜κ²Œ 인지할 수 μžˆλ„λ‘ ν•˜κ³ , μ•„μ΄μ½˜κ³Ό flex λ ˆμ΄μ•„μ›ƒμ„ 톡해 λ‹€λ₯Έ 리슀트 μ•„μ΄ν…œκ³Ό μΌκ΄€λœ μ‹œκ°μ  흐름을 μ œκ³΅ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

## 2026-08-09 - κΈ΄ ν…μŠ€νŠΈλ‘œ μΈν•œ λ ˆμ΄μ•„μ›ƒ 깨짐 λ°©μ§€
**Learning:** 맀우 κΈ΄ νŒŒμΌμ΄λ‚˜ 디렉토리 이름(곡백 μ—†λŠ” λ¬Έμžμ—΄)은 λͺ¨λ°”일 ν™˜κ²½ λ“± μ œν•œλœ λ„ˆλΉ„μ—μ„œ ν…μŠ€νŠΈ μ€„λ°”κΏˆμ΄ μΌμ–΄λ‚˜μ§€ μ•Šμ•„ λ ˆμ΄μ•„μ›ƒμ„ 깨뜨리고 κ°€λ‘œ μŠ€ν¬λ‘€μ„ μœ λ°œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
**Action:** κΈ΄ ν…μŠ€νŠΈκ°€ λ Œλ”λ§λ  수 μžˆλŠ” 제λͺ© μš”μ†Œ(예: `h1`)에 항상 `overflow-wrap: anywhere;` 속성을 μ μš©ν•˜μ—¬ ν…μŠ€νŠΈκ°€ μ •μƒμ μœΌλ‘œ μ€„λ°”κΏˆλ˜λ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€.
39 changes: 13 additions & 26 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ main {
max-width: 800px;
margin: 0 auto;
}
h1 {
overflow-wrap: anywhere;
}
ul {
list-style-type: none;
padding-left: 0;
Expand Down Expand Up @@ -144,20 +147,14 @@ internal fun crawl_directories(
processDirectory: (File, Set<String>, Array<File>?) -> Unit = { file, exclude, files -> process_dir(file, exclude, files) },
processIgnoreFile: (File, Array<String>?) -> Set<String> = { file, names -> process_ignore_file(file, names) },
listFiles: (File) -> Array<File>? = { it.listFiles() },
readAttributes: (File) -> BasicFileAttributes? = {
try {
Files.readAttributes(it.toPath(), BasicFileAttributes::class.java, LinkOption.NOFOLLOW_LINKS)
} catch (e: Exception) {
null
}
},
isDirectory: (File) -> Boolean = { Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) },
isSymbolicLink: (File) -> Boolean = { Files.isSymbolicLink(it.toPath()) },
Comment on lines +150 to +151

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

심볼릭 링크 νŒλ³„ 경둜λ₯Ό μ •κ·œν™”ν•˜μ„Έμš”.

NOFOLLOW_LINKS μ‚¬μš©μ€ μ˜¬λ°”λ¦…λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ 두 콜백이 it.toPath()λ₯Ό 직접 μ‚¬μš©ν•©λ‹ˆλ‹€. μƒλŒ€ κ²½λ‘œμ™€ ..κ°€ ν¬ν•¨λœ κ²½λ‘œμ—μ„œλ„ λ™μΌν•œ νŒλ³„ κ·œμΉ™μ„ μ μš©ν•˜λ €λ©΄ it.absoluteFile.toPath().normalize()λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

μˆ˜μ • μ˜ˆμ‹œ
-    isDirectory: (File) -> Boolean = { Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) },
-    isSymbolicLink: (File) -> Boolean = { Files.isSymbolicLink(it.toPath()) },
+    isDirectory: (File) -> Boolean = {
+        Files.isDirectory(it.absoluteFile.toPath().normalize(), LinkOption.NOFOLLOW_LINKS)
+    },
+    isSymbolicLink: (File) -> Boolean = {
+        Files.isSymbolicLink(it.absoluteFile.toPath().normalize())
+    },

As per coding guidelines: 심볼릭 링크 μƒνƒœκ°€ μ€‘μš”ν•  λ•Œ absoluteFile.toPath().normalize()λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
isDirectory: (File) -> Boolean = { Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) },
isSymbolicLink: (File) -> Boolean = { Files.isSymbolicLink(it.toPath()) },
isDirectory: (File) -> Boolean = {
Files.isDirectory(it.absoluteFile.toPath().normalize(), LinkOption.NOFOLLOW_LINKS)
},
isSymbolicLink: (File) -> Boolean = {
Files.isSymbolicLink(it.absoluteFile.toPath().normalize())
},
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/html4tree/main.kt` around lines 150 - 151, Update the
isDirectory and isSymbolicLink callbacks to derive the path from
it.absoluteFile.toPath().normalize() before performing filesystem checks, while
preserving NOFOLLOW_LINKS for directory detection and the existing symbolic-link
check.

Source: Coding guidelines

readIdentity: (File) -> FileIdentity = ::read_file_identity
) {
var lle: LinkedListEntry? = ll.pull()

while(lle != null){
val attrs = readAttributes(lle.file)
if (attrs == null || !attrs.isDirectory) {
if (!isDirectory(lle.file)) {
lle = ll.pull()
continue
}
Expand All @@ -180,28 +177,18 @@ internal fun crawl_directories(

if(maxLevel == -1 || currentLevel < maxLevel) {
dirFiles?.forEach {
// ⚑ Bolt Performance Optimization: Short-circuit OS stat calls
// ⚑ Bolt Performance Optimization: Short-circuit OS stat calls (isDirectory/isSymbolicLink)
// by checking cheap in-memory string exclusion rules first
if(!it.name.isHiddenFile() && it.name !in exclude) {
val childAttrs = readAttributes(it)
if(childAttrs != null && childAttrs.isDirectory && !childAttrs.isSymbolicLink) {
val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key)
ll.push(childEntry)
}
if(!it.name.startsWith(".") && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it)) {
val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key)
ll.push(childEntry)
Comment on lines +180 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'isSymbolicLink\s*=\s*\{|crawl_directories' src/test/kotlin

Repository: ContextualWisdomLab/html4tree

Length of output: 6710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'src/main/kotlin/html4tree/main.kt|src/test/kotlin/html4tree/MainTest.kt' . | sed 's#^\./##'

echo "== main.kt relevant section =="
sed -n '150,205p' src/main/kotlin/html4tree/main.kt | cat -n

echo "== MainTest crawl_directories occurrences with context =="
sed -n '130,210p;225,250p;680,710p' src/test/kotlin/html4tree/MainTest.kt | cat -n

echo "== all isSymbolicLink occurrences =="
rg -n -C 3 'isSymbolicLink' src/test/kotlin src/main/kotlin

echo "== static branch coverage probe for crawl_directories condition ==
python3 - <<'PY'
from pathlib import Path
test = Path('src/test/kotlin/html4tree/MainTest.kt').read_text()
main = Path('src/main/kotlin/html4tree/main.kt').read_text()
print('contains isSymbolicLink == false assignments:', 'isSymbolicLink = { false }' in test)
print('contains isSymbolicLink == true assignment:', 'isSymbolicLink = { true }' in test)
print('crawl_directories call count around isSymbolicLink:', test.count('crawl_directories'))
print('branch expression in main:', 'if!' in main and 'isDirectory(it) && !isSymbolicLink(it)' in main)
PY

Repository: ContextualWisdomLab/html4tree

Length of output: 16195


심볼릭 링크 차단 λΆ„κΈ°λ₯Ό ν…ŒμŠ€νŠΈν•˜μ„Έμš”.

crawl_directoriesλŠ” isDirectory == true && !isSymbolicLink만 μžμ‹μœΌλ‘œ μΆ”κ°€ν•©λ‹ˆλ‹€. ν˜„μž¬ ν…ŒμŠ€νŠΈλŠ” isSymbolicLink = { false }만 μ£Όμž…ν•˜κ³  μžˆμ–΄ isDirectory == true && isSymbolicLink == true κ²½λ‘œκ°€ JaCoCo 100% 컀버리지λ₯Ό μΆ©μ‘±ν•˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή 콜백 쑰합을 μ£Όμž…ν•˜κ³  큐에 μΆ”κ°€λ˜μ§€ μ•ŠλŠ”μ§€ κ²€μ¦ν•˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/html4tree/main.kt` around lines 180 - 184, Update the tests
for crawl_directories to inject isDirectory = true and isSymbolicLink = true,
then verify the symbolic-link entry is not added to the traversal queue.
Preserve the existing non-symbolic directory coverage while adding this
branch-specific assertion.

Source: Coding guidelines

}
}
}
lle = ll.pull()
}
}

fun String.isHiddenFile(): Boolean {
return when (firstOrNull()) {
'.', '\u3002', '\uFF0E', '\uFF61' -> true
else -> false
}
}

// ⚑ Bolt Performance Optimization: Single-pass loop with lazy StringBuilder
// Chained `.replace()` calls allocate multiple intermediate strings.
// A single pass over the string lazily allocating a StringBuilder is much faster.
Expand Down Expand Up @@ -317,9 +304,9 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
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)

// λ³΄μ•ˆ ν–₯상: dot-like prefixes are treated as hidden to prevent visually-confusable sensitive entries from reaching generated indexes.
// λ³΄μ•ˆ ν–₯상: .env, .git λ“± λ―Όκ°ν•œ 정보가 포함될 수 μžˆλŠ” μˆ¨κΉ€ 파일(.으둜 μ‹œμž‘ν•˜λŠ” λͺ¨λ“  ν•­λͺ©)을 기본적으둜 λ…ΈμΆœν•˜μ§€ μ•Šλ„λ‘ μ œμ™Έ (정보 λ…ΈμΆœ λ°©μ§€)
(dirFilesNames ?: curr_dir.list())?.forEach {
if (it.isHiddenFile()) {
if (it.startsWith(".")) {
files_to_exclude.add(it)
}
}
Expand Down Expand Up @@ -373,7 +360,7 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
val fileName = it.getName()
// ⚑ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls
// πŸ›‘οΈ Sentinel: Ignore hidden files/directories to prevent sensitive data exposure
if (!fileName.isHiddenFile() && fileName !in exclude) {
if (!fileName.startsWith(".") && fileName !in exclude) {
var isLinkedDirectory = false
var isSymbolicLink = false
try {
Expand Down
27 changes: 0 additions & 27 deletions src/test/kotlin/html4tree/CoverageTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,4 @@ class CoverageTest {
readOnlyDir.setWritable(true, false)
}
}

@Test
fun testCrawlDirectoriesReadAttributesExceptionFallback() {
val tempDir = java.nio.file.Files.createTempDirectory("test").toFile()
val readOnlyDir = File(tempDir, "readonly")
readOnlyDir.mkdir()
val ll = LinkedList()
ll.push(LinkedListEntry(readOnlyDir, 0, null))
crawl_directories(ll, -1, readAttributes = { null })
assertTrue(true)
}

@Test
fun testReadAttributesDefaultException() {
// Create a file that fails to be read, e.g. path too long or invalid path, but easiest is mock or pass a non-existent file?
// Wait, NOFOLLOW_LINKS on a broken symlink still returns attributes.
// What about passing a File that throws when toPath() is called?
// We can just call the default parameter using reflection, or test it directly.
// But how to cover the default parameter? The memory says: "To cover default fallback lambdas, write tests that omit the parameter and intentionally fail the primary operation to force execution of the default fallback logic."
// If we omit readAttributes, it will use `Files.readAttributes`. To make it throw an exception, we can pass a file that has been deleted right before, or just a file that doesn't exist!
val tempDir = java.nio.file.Files.createTempDirectory("test").toFile()
val missingDir = File(tempDir, "missing")
val ll = LinkedList()
ll.push(LinkedListEntry(missingDir, 0, null)) // missingDir doesn't exist, readAttributes throws NoSuchFileException
crawl_directories(ll, -1)
assertTrue(true)
}
}
36 changes: 0 additions & 36 deletions src/test/kotlin/html4tree/HiddenFileSecurityTest.kt

This file was deleted.

32 changes: 11 additions & 21 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.nio.file.Files
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
Expand All @@ -19,20 +17,6 @@ import kotlin.test.assertTrue
class MainTest {
private lateinit var tempDir: File

private fun createMockAttributes(isDir: Boolean, isSymlink: Boolean): BasicFileAttributes {
return object : BasicFileAttributes {
override fun lastModifiedTime(): FileTime = FileTime.fromMillis(0)
override fun lastAccessTime(): FileTime = FileTime.fromMillis(0)
override fun creationTime(): FileTime = FileTime.fromMillis(0)
override fun isRegularFile(): Boolean = !isDir && !isSymlink
override fun isDirectory(): Boolean = isDir
override fun isSymbolicLink(): Boolean = isSymlink
override fun isOther(): Boolean = false
override fun size(): Long = 0L
override fun fileKey(): Any? = null
}
}

@Before
fun setup() {
tempDir = Files.createTempDirectory("html4tree-test-").toFile()
Expand Down Expand Up @@ -170,7 +154,8 @@ class MainTest {
processDirectory = { file, _, _ -> processed.add(file) },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = { emptyArray() },
readAttributes = { file -> createMockAttributes(isDir = true, isSymlink = false) },
isDirectory = { true },
isSymbolicLink = { false },
readIdentity = { FileIdentity("after-swap", true) }
)

Expand All @@ -191,7 +176,8 @@ class MainTest {
processDirectory = { file, _, _ -> processed.add(file) },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = { emptyArray() },
readAttributes = { file -> createMockAttributes(isDir = true, isSymlink = false) },
isDirectory = { true },
isSymbolicLink = { false },
readIdentity = { FileIdentity(null, false) }
)

Expand All @@ -214,7 +200,8 @@ class MainTest {
processDirectory = { file, _, _ -> processed.add(file) },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = { file -> if (file == root) arrayOf(child) else emptyArray() },
readAttributes = { file -> createMockAttributes(isDir = true, isSymlink = false) },
isDirectory = { true },
isSymbolicLink = { false },
readIdentity = { file ->
val key = file.absolutePath
val callCount = callsByPath.getOrDefault(key, 0)
Expand Down Expand Up @@ -252,7 +239,8 @@ class MainTest {
processDirectory = { file, _, _ -> processed.add(file) },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = { emptyArray() },
readAttributes = { file -> createMockAttributes(isDir = file == directoryEntry, isSymlink = false) },
isDirectory = { it == directoryEntry },
isSymbolicLink = { false },
readIdentity = { FileIdentity("directory-key", true) }
)

Expand Down Expand Up @@ -356,6 +344,7 @@ class MainTest {
assertTrue(htmlContent.contains("prefers-reduced-motion"))
assertTrue(htmlContent.contains("max-width: 800px;"))
assertTrue(htmlContent.contains("margin: 0 auto;"))
assertTrue(htmlContent.contains("overflow-wrap: anywhere;"))
}

@Test
Expand Down Expand Up @@ -710,7 +699,8 @@ class MainTest {
listed = true
emptyArray()
},
readAttributes = { file -> createMockAttributes(isDir = true, isSymlink = false) },
isDirectory = { true },
isSymbolicLink = { false },
readIdentity = { FileIdentity("current-key", true) }
)

Expand Down
Loading