Skip to content

Commit 86c5772

Browse files
Follow symbolic links during glob expansion
FileResolver.listElements() unconditionally skipped all symlinks to prevent cyclical globs from directory symlinks. As a side effect, import*() and read*() silently dropped symlinks to regular files from their results, returning partial output with no warning, and skipped symlinks to directories even when no cycle existed. Stop filtering symlinks. Switch the directory check from Files.isDirectory(), which silently returns false on errors, to Files.readAttributes(), which lets I/O errors propagate. This way the existing OS-level symlink-resolution limit surfaces a clear "Too many levels of symbolic links" error on cycles instead of a silent truncation. Broken symlinks are still surfaced as non-directory entries via a NoSuchFileException catch.
1 parent 2e49a31 commit 86c5772

11 files changed

Lines changed: 164 additions & 5 deletions

File tree

docs/modules/release-notes/pages/changelog.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ include::ROOT:partial$component-attributes.adoc[]
44
[[release-0.32.0]]
55
== 0.32.0 (UNRELEASED)
66

7+
=== Fixes
8+
9+
* Follow symbolic links during `import*` and `read*` glob expansion; symlink cycles now surface as a clear I/O error
10+
(pr:https://github.com/apple/pkl/pull/XXXX[]).
11+
712
[[release-0.31.1]]
813
== 0.31.1 (2026-03-26)
914

pkl-core/src/main/java/org/pkl/core/module/FileResolver.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import java.nio.file.NoSuchFileException;
2222
import java.nio.file.NotDirectoryException;
2323
import java.nio.file.Path;
24+
import java.nio.file.attribute.BasicFileAttributes;
2425
import java.util.ArrayList;
2526
import java.util.Collections;
2627
import java.util.List;
@@ -37,11 +38,16 @@ public static List<PathElement> listElements(Path path) throws IOException {
3738
try (var stream = Files.newDirectoryStream(path)) {
3839
var ret = new ArrayList<PathElement>();
3940
for (var entry : stream) {
40-
// skip symlinks to prevent cyclical globs
41-
if (Files.isSymbolicLink(entry)) {
42-
continue;
41+
// Use readAttributes (rather than Files.isDirectory) so that errors like
42+
// "Too many levels of symbolic links" surface as IOException instead of being
43+
// silently swallowed. Broken symlinks still go through as non-directory entries.
44+
boolean isDirectory;
45+
try {
46+
isDirectory = Files.readAttributes(entry, BasicFileAttributes.class).isDirectory();
47+
} catch (NoSuchFileException ignored) {
48+
isDirectory = false;
4349
}
44-
ret.add(new PathElement(entry.getFileName().toString(), Files.isDirectory(entry)));
50+
ret.add(new PathElement(entry.getFileName().toString(), isDirectory));
4551
}
4652
return ret;
4753
} catch (NotDirectoryException | NoSuchFileException ignored) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
subdir
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
real-target.pkl
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "real-target"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "inner"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
amends ".../snippetTest.pkl"
2+
3+
examples {
4+
["symlink to file is included"] {
5+
import*("../../input-helper/globtest-symlink/*.pkl").keys.toListing()
6+
}
7+
8+
["symlink target content is read through symlink"] {
9+
import*("../../input-helper/globtest-symlink/*.pkl")
10+
.toMap()
11+
.values
12+
.map((it) -> it.name)
13+
.toListing()
14+
}
15+
16+
["directory symlink is followed"] {
17+
import*("../../input-helper/globtest-symlink/**.pkl").keys.toListing()
18+
}
19+
}

pkl-core/src/test/files/LanguageSnippetTests/output/basic/importGlob.pcf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ examples {
1919
"../../input-helper/globtest/moduleA.pkl"
2020
"../../input-helper/globtest/moduleB.pkl"
2121
"../../input-helper/globtest/child/moduleC.pkl"
22+
"../../input-helper/globtest/childlinked/moduleC.pkl"
2223
}
2324
}
2425
["amended"] {
@@ -34,13 +35,19 @@ examples {
3435
---
3536
name: child/moduleC
3637

38+
---
39+
name: child/moduleC
40+
3741
"""
3842
}
3943
["globstar then up one level"] {
4044
new {
4145
"../../input-helper/globtest/child/../module with [weird] ~!characters.pkl"
4246
"../../input-helper/globtest/child/../moduleA.pkl"
4347
"../../input-helper/globtest/child/../moduleB.pkl"
48+
"../../input-helper/globtest/childlinked/../module with [weird] ~!characters.pkl"
49+
"../../input-helper/globtest/childlinked/../moduleA.pkl"
50+
"../../input-helper/globtest/childlinked/../moduleB.pkl"
4451
}
4552
}
4653
["empty glob matches current directory"] {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
examples {
2+
["symlink to file is included"] {
3+
new {
4+
"../../input-helper/globtest-symlink/linked.pkl"
5+
"../../input-helper/globtest-symlink/real-target.pkl"
6+
}
7+
}
8+
["symlink target content is read through symlink"] {
9+
new {
10+
"real-target"
11+
"real-target"
12+
}
13+
}
14+
["directory symlink is followed"] {
15+
new {
16+
"../../input-helper/globtest-symlink/linked.pkl"
17+
"../../input-helper/globtest-symlink/real-target.pkl"
18+
"../../input-helper/globtest-symlink/linked-dir/inner.pkl"
19+
"../../input-helper/globtest-symlink/subdir/inner.pkl"
20+
}
21+
}
22+
}

pkl-core/src/test/files/LanguageSnippetTests/output/basic/readGlob.pcf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ examples {
2727
text = """
2828
name = "child/moduleC"
2929

30+
"""
31+
base64 = "bmFtZSA9ICJjaGlsZC9tb2R1bGVDIgo="
32+
}
33+
["../../input-helper/globtest/childlinked/moduleC.pkl"] {
34+
uri = "file:///$snippetsDir/input-helper/globtest/childlinked/moduleC.pkl"
35+
text = """
36+
name = "child/moduleC"
37+
3038
"""
3139
base64 = "bmFtZSA9ICJjaGlsZC9tb2R1bGVDIgo="
3240
}
@@ -72,6 +80,11 @@ examples {
7280
text = "hi"
7381
base64 = "bmFtZSA9ICJjaGlsZC9tb2R1bGVDIgo="
7482
}
83+
["../../input-helper/globtest/childlinked/moduleC.pkl"] {
84+
uri = "file:///$snippetsDir/input-helper/globtest/childlinked/moduleC.pkl"
85+
text = "hi"
86+
base64 = "bmFtZSA9ICJjaGlsZC9tb2R1bGVDIgo="
87+
}
7588
}
7689
}
7790
["env:"] {

0 commit comments

Comments
 (0)