Skip to content
Closed
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
5 changes: 5 additions & 0 deletions docs/modules/release-notes/pages/changelog.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ include::ROOT:partial$component-attributes.adoc[]
[[release-0.32.0]]
== 0.32.0 (UNRELEASED)

=== Fixes

* Fix `import*` and `read*` silently skipping symlinked files during glob expansion
(pr:https://github.com/apple/pkl/pull/1522[]).

[[release-0.31.1]]
== 0.31.1 (2026-03-26)

Expand Down
6 changes: 3 additions & 3 deletions pkl-core/src/main/java/org/pkl/core/module/FileResolver.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -37,8 +37,8 @@ public static List<PathElement> listElements(Path path) throws IOException {
try (var stream = Files.newDirectoryStream(path)) {
var ret = new ArrayList<PathElement>();
for (var entry : stream) {
// skip symlinks to prevent cyclical globs
if (Files.isSymbolicLink(entry)) {
// Skip symlinks to directories to prevent cyclical globs.
if (Files.isSymbolicLink(entry) && Files.isDirectory(entry)) {
continue;
}
ret.add(new PathElement(entry.getFileName().toString(), Files.isDirectory(entry)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name = "real-target"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
amends ".../snippetTest.pkl"

examples {
["symlink to file is included"] {
import*("../../input-helper/globtest-symlink/*.pkl").keys.toListing()
}

["symlink target content is read through symlink"] {
import*("../../input-helper/globtest-symlink/*.pkl")
.toMap()
.values
.map((it) -> it.name)
.toListing()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
examples {
["symlink to file is included"] {
new {
"../../input-helper/globtest-symlink/linked.pkl"
"../../input-helper/globtest-symlink/real-target.pkl"
}
}
["symlink target content is read through symlink"] {
new {
"real-target"
"real-target"
}
}
}
69 changes: 69 additions & 0 deletions pkl-core/src/test/kotlin/org/pkl/core/module/FileResolverTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.module

import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.createDirectory
import kotlin.io.path.createFile
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.writeString

class FileResolverTest {
@Test
fun `includes symlink whose target is a regular file outside the listed directory`(
@TempDir tempDir: Path
) {
val listedDir = tempDir.resolve("listed").createDirectory()
val otherDir = tempDir.resolve("other").createDirectory()
listedDir.resolve("real.pkl").createFile().writeString("""name = "real"""")
val target = otherDir.resolve("target.pkl").createFile()
target.writeString("""name = "target"""")
Files.createSymbolicLink(listedDir.resolve("linked.pkl"), target)

val elements = FileResolver.listElements(listedDir)

assertThat(elements)
.containsExactlyInAnyOrder(PathElement("real.pkl", false), PathElement("linked.pkl", false))
}

@Test
fun `skips symlink whose target is a directory to prevent cyclical globs`(
@TempDir tempDir: Path
) {
val listedDir = tempDir.resolve("listed").createDirectory()
val otherDir = tempDir.resolve("other").createDirectory()
listedDir.resolve("real.pkl").createFile()
Files.createSymbolicLink(listedDir.resolve("linked-dir"), otherDir)

val elements = FileResolver.listElements(listedDir)

assertThat(elements).containsExactly(PathElement("real.pkl", false))
}

@Test
fun `includes broken symlink as non-directory entry`(@TempDir tempDir: Path) {
tempDir.resolve("real.pkl").createFile()
Files.createSymbolicLink(tempDir.resolve("broken.pkl"), tempDir.resolve("missing.pkl"))

val elements = FileResolver.listElements(tempDir)

assertThat(elements)
.containsExactlyInAnyOrder(PathElement("real.pkl", false), PathElement("broken.pkl", false))
}
}
Loading