Skip to content
Merged
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
10 changes: 10 additions & 0 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ internal fun crawl_directories(

// ⚡ Bolt Performance Optimization: 디렉토리 목록을 캐싱하여 중복된 I/O 시스템 호출을 줄임
val dirFiles = listFiles(lle.file)

// The path can be replaced between the initial identity check and
// directory enumeration. Do not process or enqueue children from a
// snapshot whose post-listing identity is unreadable or different.
val postListingIdentity = readIdentity(lle.file)
if (!postListingIdentity.readable || currentIdentity.key != postListingIdentity.key) {
lle = ll.pull()
continue
}

val dirFilesNames = dirFiles?.map { it.name }?.toTypedArray()
val exclude = processIgnoreFile(lle.file, dirFilesNames)

Expand Down
71 changes: 71 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,77 @@ class MainTest {
assertFalse(listed, "fileKey mismatch should skip child listing")
}

@Test
fun testDirectoryReplacementAfterListingIsRejected() {
val subdir = File(tempDir, "post_listing_swap")
subdir.mkdir()
val ll = LinkedList()
val entry = LinkedListEntry(subdir, 0)
entry.fileKey = "stable-key"
ll.push(entry)

var processed = false
var listed = false
var identityCalls = 0

crawl_directories(
ll,
-1,
processDirectory = { _, _, _ -> processed = true },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = {
listed = true
emptyArray()
},
readAttributes = { _ -> createMockAttributes(isDir = true, isSymlink = false) },
readIdentity = {
identityCalls++
if (identityCalls == 1) {
FileIdentity("stable-key", true)
} else {
FileIdentity("replacement-key", true)
}
}
)

assertTrue(listed, "the initial identity should allow child listing")
assertEquals(2, identityCalls, "identity must be re-read after child listing")
assertFalse(processed, "a replaced directory must not be processed")
}

@Test
fun testDirectoryBecomingUnreadableAfterListingIsRejected() {
val subdir = File(tempDir, "post_listing_unreadable")
subdir.mkdir()
val ll = LinkedList()
val entry = LinkedListEntry(subdir, 0)
entry.fileKey = "stable-key"
ll.push(entry)

var processed = false
var identityCalls = 0

crawl_directories(
ll,
-1,
processDirectory = { _, _, _ -> processed = true },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = { emptyArray() },
readAttributes = { _ -> createMockAttributes(isDir = true, isSymlink = false) },
readIdentity = {
identityCalls++
if (identityCalls == 1) {
FileIdentity("stable-key", true)
} else {
FileIdentity(null, false)
}
}
)

assertEquals(2, identityCalls, "identity must be re-read after child listing")
assertFalse(processed, "a directory that becomes unreadable must not be processed")
}

@Test
fun testProcessIgnoreFileWithIllegalArgumentPattern() {
val tempDir = java.nio.file.Files.createTempDirectory("test").toFile()
Expand Down
Loading