[2.x] fix: Avoid following symlinks during scripted test cleanup (#8985)

**Problem**
When a scripted test creates a symbolic link within the test directory
pointing to a file or directory outside of it, the batch cleanup code
follows the symlink and deletes the external target. This is because
`view.list(base / **, !keep)` recursively traverses symlinked
directories, and the resulting paths resolve to the external location.

**Solution**
Replace the flat recursive glob listing with manual recursion that
checks `FileAttributes.isSymbolicLink` before descending into
directories, matching the pattern already used in `Clean.scala`.
Symlinks are now deleted as leaf nodes without traversing their targets.

Also hoists the cleanup helpers out of the per-test loop since they
only depend on the (constant) temp directory.

Fixes sbt/sbt#7331
This commit is contained in:
Dream 2026-03-28 13:10:06 -04:00 committed by GitHub
parent f81a1524bb
commit f624998c91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 22 additions and 13 deletions

View File

@ -225,6 +225,27 @@ final class ScriptedTests(
val seqHandlers = handlers.values.toList
runner.initStates(states, seqHandlers)
// Delete test contents between runs, but preserve global dirs.
// Uses manual recursion to avoid following symlinks (sbt/sbt#7331).
val view = sbt.nio.file.FileTreeView.default
val base = tempTestDir.getCanonicalFile.toGlob
val global = base / "global"
val globalLogging = base / ** / "global-logging"
def recursiveFilter(glob: Glob): PathFilter = (glob: PathFilter) || glob / **
val keep: PathFilter = recursiveFilter(global) || recursiveFilter(globalLogging)
def deleteContents(dir: java.nio.file.Path): Unit =
view
.list(Glob(dir, AnyPath), !keep)
.foreach:
case (p, attrs) if attrs.isDirectory && !attrs.isSymbolicLink =>
deleteContents(p)
try Files.deleteIfExists(p)
catch { case _: IOException => }
case (p, _) =>
try Files.deleteIfExists(p)
catch { case _: IOException => }
val tempTestPath = tempTestDir.getCanonicalFile.toPath
def runBatchTests = {
groupedTests.map { case ((group, name), originalDir) =>
val label = s"$group/$name"
@ -261,19 +282,7 @@ final class ScriptedTests(
// Run the test and delete files (except global that holds local scala jars)
val result = runOrHandleDisabled(label, tempTestDir, runTest, buffer)
if (!keepTempDirectory) {
val view = sbt.nio.file.FileTreeView.default
val base = tempTestDir.getCanonicalFile.toGlob
val global = base / "global"
val globalLogging = base / ** / "global-logging"
def recursiveFilter(glob: Glob): PathFilter = (glob: PathFilter) || glob / **
val keep: PathFilter = recursiveFilter(global) || recursiveFilter(globalLogging)
val toDelete = view.list(base / **, !keep).map(_._1).sorted.reverse
toDelete.foreach { p =>
try Files.deleteIfExists(p)
catch { case _: IOException => }
}
}
if (!keepTempDirectory) deleteContents(tempTestPath)
result
}
}