diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java index 66ae3593..952d651e 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -75,7 +75,7 @@ public class ImportGlobNode extends AbstractImportNode { CompilerDirectives.transferToInterpreterAndInvalidate(); var context = VmContext.get(this); try { - var moduleKey = context.getModuleResolver().resolve(importUri); + var moduleKey = context.getModuleResolver().resolve(importUri, this); if (!moduleKey.isGlobbable()) { throw exceptionBuilder() .evalError("cannotGlobUri", importUri, importUri.getScheme()) diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java index f0ca0896..85b61f90 100644 --- a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java @@ -173,7 +173,7 @@ public final class ModuleKeyFactories { private static class File implements ModuleKeyFactory { @Override - public Optional create(URI uri) { + public Optional create(URI uri) throws URISyntaxException { // skip loading providers if the scheme is `file`. if (uri.getScheme().equalsIgnoreCase("file")) { return Optional.of(ModuleKeys.file(uri)); diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java index f5127e1f..1741ffb9 100644 --- a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java @@ -25,8 +25,10 @@ import java.net.URISyntaxException; import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.List; import java.util.Map; +import org.pkl.core.PklBugException; import org.pkl.core.SecurityManager; import org.pkl.core.SecurityManagerException; import org.pkl.core.externalreader.ExternalModuleResolver; @@ -90,10 +92,20 @@ public final class ModuleKeys { } /** Creates a module key for a {@code file:} module. */ - public static ModuleKey file(URI uri) { + public static ModuleKey file(URI uri) throws URISyntaxException { return new File(uri); } + /** Creates a module key for a {@code file:} module. */ + public static ModuleKey file(Path path) { + try { + return new File(path.toAbsolutePath().toUri()); + } catch (URISyntaxException e) { + // impossible, we started with a path to begin with. + throw PklBugException.unreachableCode(); + } + } + /** * Creates a module key for a {@code modulepath:} module to be resolved with the given resolver. */ @@ -299,7 +311,8 @@ public final class ModuleKeys { private static class File implements ModuleKey { final URI uri; - File(URI uri) { + File(URI uri) throws URISyntaxException { + IoUtils.validateFileUri(uri); this.uri = uri; } diff --git a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java index ef7fe04e..fa49ae72 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java +++ b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -443,7 +443,7 @@ public final class ProjectPackager { private @Nullable List getImportsAndReads(Path pklModulePath) { try { - var moduleKey = ModuleKeys.file(pklModulePath.toUri()); + var moduleKey = ModuleKeys.file(pklModulePath); var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath); return ImportsAndReadsParser.parse(moduleKey, resolvedModuleKey); } catch (IOException e) { diff --git a/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java index 20c1c741..21260e91 100644 --- a/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java +++ b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java @@ -254,6 +254,12 @@ public final class ResourceReaders { private static final class FileResource extends UrlResource { static final ResourceReader INSTANCE = new FileResource(); + @Override + public Optional read(URI uri) throws IOException, URISyntaxException { + IoUtils.validateFileUri(uri); + return super.read(uri); + } + @Override public String getUriScheme() { return "file"; @@ -324,7 +330,7 @@ public final class ResourceReaders { private abstract static class UrlResource implements ResourceReader { @Override - public Optional read(URI uri) throws IOException { + public Optional read(URI uri) throws IOException, URISyntaxException { if (HttpUtils.isHttpUrl(uri)) { var httpClient = VmContext.get(null).getHttpClient(); var request = HttpRequest.newBuilder(uri).build(); diff --git a/pkl-core/src/main/java/org/pkl/core/util/GlobResolver.java b/pkl-core/src/main/java/org/pkl/core/util/GlobResolver.java index eedcb559..690283e8 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/GlobResolver.java +++ b/pkl-core/src/main/java/org/pkl/core/util/GlobResolver.java @@ -436,7 +436,8 @@ public final class GlobResolver { /** Split a glob pattern into the base, non-wildcard parts, and the wildcard parts. */ private static Pair splitGlobPatternIntoBaseAndWildcards( - ReaderBase reader, String globPattern, boolean hasAbsoluteGlob) { + ReaderBase reader, String globPattern, boolean hasAbsoluteGlob) + throws InvalidGlobPatternException { var effectiveGlobPattern = globPattern; var basePathSb = new StringBuilder(); if (hasAbsoluteGlob) { @@ -446,6 +447,10 @@ public final class GlobResolver { basePathSb.append(IoUtils.stripFragment(globUri)).append('#'); } else { effectiveGlobPattern = globUri.getPath(); + if (effectiveGlobPattern == null) { + throw new InvalidGlobPatternException( + ErrorMessages.create("invalidGlobNonHierarchicalUri", globUri.getScheme())); + } basePathSb.append(globUri.getScheme()).append(':'); } } diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index d1065454..87ba8f3e 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -835,4 +835,10 @@ public final class IoUtils { } return index; } + + public static void validateFileUri(URI uri) throws URISyntaxException { + if (!uri.getSchemeSpecificPart().startsWith("/")) { + throw new URISyntaxException(uri.toString(), ErrorMessages.create("invalidOpaqueFileUri")); + } + } } diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index 82b175ad..8ef038e2 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -661,6 +661,9 @@ Invalid escape character `\\{0}`. invalidGlobTooComplex=\ The glob pattern is too complex. +invalidGlobNonHierarchicalUri=\ +Scheme `{0}` requires a hierarchical path (there must be a `/` after the first colon). + # used as {1} in invalidModuleUri and invalidResourceUri invalidTripleDotSyntax=\ expected `...` or `.../path/to/my_module.pkl` @@ -1057,3 +1060,7 @@ External {0} reader does not support scheme `{1}`. externalReaderAlreadyTerminated=\ External reader process has already terminated. + +invalidOpaqueFileUri=\ +File URIs must have a path that starts with `/` (e.g. file:/path/to/my_module.pkl).\n\ +To resolve relative paths, remove the scheme prefix (remove "file:"). diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri1.pkl new file mode 100644 index 00000000..c3a56307 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri1.pkl @@ -0,0 +1 @@ +res = read("file:path/to/foo.txt") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri2.pkl new file mode 100644 index 00000000..65885818 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri2.pkl @@ -0,0 +1 @@ +res = read*("file:path/to/foo.txt") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri3.pkl new file mode 100644 index 00000000..4dd28b9e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri3.pkl @@ -0,0 +1 @@ +res = import("file:path/to/foo.pkl") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri4.pkl new file mode 100644 index 00000000..2e45cdf8 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidFileUri4.pkl @@ -0,0 +1,9 @@ +// Ideally, this should have the same error message as `invalidFileUri2`; the error is that the glob +// pattern is invalid. +// +// But, this is somewhat challenging to fix, because the `URISyntaxException` gets thrown before +// `ImportGlobNode` is initialized. +// +// Regardless, this error is good enough (given this error message, users know what to do), and we +// can afford to be pragmatic here. +res = import*("file:path/to/foo.pkl") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri1.err new file mode 100644 index 00000000..2f580280 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri1.err @@ -0,0 +1,13 @@ +–– Pkl Error –– +Resource URI `file:path/to/foo.txt` has invalid syntax. + +x | res = read("file:path/to/foo.txt") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at invalidFileUri1#res (file:///$snippetsDir/input/errors/invalidFileUri1.pkl) + +File URIs must have a path that starts with `/` (e.g. file:/path/to/my_module.pkl). +To resolve relative paths, remove the scheme prefix (remove "file:"). + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri2.err new file mode 100644 index 00000000..7d8dd52d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri2.err @@ -0,0 +1,12 @@ +–– Pkl Error –– +Invalid glob pattern `file:path/to/foo.txt`. + +x | res = read*("file:path/to/foo.txt") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at invalidFileUri2#res (file:///$snippetsDir/input/errors/invalidFileUri2.pkl) + +Scheme `file` requires a hierarchical path (there must be a `/` after the first colon). + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri3.err new file mode 100644 index 00000000..3c8643b1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri3.err @@ -0,0 +1,13 @@ +–– Pkl Error –– +Module URI `file:path/to/foo.pkl` has invalid syntax. + +x | res = import("file:path/to/foo.pkl") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at invalidFileUri3#res (file:///$snippetsDir/input/errors/invalidFileUri3.pkl) + +File URIs must have a path that starts with `/` (e.g. file:/path/to/my_module.pkl). +To resolve relative paths, remove the scheme prefix (remove "file:"). + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri4.err new file mode 100644 index 00000000..09615442 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidFileUri4.err @@ -0,0 +1,13 @@ +–– Pkl Error –– +Module URI `file:path/to/foo.pkl` has invalid syntax. + +x | res = import*("file:path/to/foo.pkl") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at invalidFileUri4#res (file:///$snippetsDir/input/errors/invalidFileUri4.pkl) + +File URIs must have a path that starts with `/` (e.g. file:/path/to/my_module.pkl). +To resolve relative paths, remove the scheme prefix (remove "file:"). + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base)