diff --git a/pkl-core/src/main/java/org/pkl/core/SecurityManager.java b/pkl-core/src/main/java/org/pkl/core/SecurityManager.java index 2f505141e..e3b96ab81 100644 --- a/pkl-core/src/main/java/org/pkl/core/SecurityManager.java +++ b/pkl-core/src/main/java/org/pkl/core/SecurityManager.java @@ -35,8 +35,8 @@ public interface SecurityManager { /** Checks if the given importing module may import the given imported module. */ void checkImportModule(URI importingModule, URI importedModule) throws SecurityManagerException; - /** Checks if the given resource may be read. */ - void checkReadResource(URI resource) throws SecurityManagerException; + /** Checks if the given reading module may read the given resource . */ + void checkReadResource(URI readingModule, URI resource) throws SecurityManagerException; /** * Checks if the given resource may be resolved. This check is required before any attempt is made diff --git a/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java b/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java index 3f1fbe95d..93162e51d 100644 --- a/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java +++ b/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java @@ -155,19 +155,29 @@ public void checkResolveResource(URI resource) throws SecurityManagerException { } @Override - public void checkReadResource(URI uri) throws SecurityManagerException { + public void checkReadResource(URI readingModule, URI uri) throws SecurityManagerException { + var importingTrustLevel = trustLevels.apply(readingModule); + var importedTrustLevel = trustLevels.apply(uri); + + if (importingTrustLevel < importedTrustLevel) { + var message = + ErrorMessages.create( + "insufficientTrustLevel", uri, readingModule, "read resource", "reading"); + throw new SecurityManagerException(message); + } + checkRead(uri, allowedResources, true); } @Override - public void checkImportModule(URI importingModule, URI importedModule) - throws SecurityManagerException { + public void checkImportModule(URI importingModule, URI uri) throws SecurityManagerException { var importingTrustLevel = trustLevels.apply(importingModule); - var importedTrustLevel = trustLevels.apply(importedModule); + var importedTrustLevel = trustLevels.apply(uri); if (importingTrustLevel < importedTrustLevel) { var message = - ErrorMessages.create("insufficientModuleTrustLevel", importedModule, importingModule); + ErrorMessages.create( + "insufficientTrustLevel", uri, importingModule, "import module", "importing"); throw new SecurityManagerException(message); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java index 22836aaa9..dbeb8d348 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java @@ -1957,16 +1957,15 @@ public UnresolvedPropertyNode visitClassProperty(ClassProperty entry) { scope.getName(), scope.getConstLevel() == ConstLevel.ALL)); } else { // no value given - if (isLocal) { - assert typeAnnotation != null; - throw missingLocalPropertyValue(typeAnnotation); - } if (VmModifier.isExternal(modifiers)) { bodyNode = externalMemberRegistry.getPropertyBody(scope.getQualifiedName(), headerSection); if (bodyNode instanceof LanguageAwareNode languageAwareNode) { languageAwareNode.initLanguage(language); } + } else if (isLocal) { + assert typeAnnotation != null; + throw missingLocalPropertyValue(typeAnnotation); } else { bodyNode = null; // will be given a default by UnresolvedPropertyNode } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java index 9aa2f9e75..c1ce83be0 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java @@ -53,7 +53,10 @@ protected final URI parseUri(String resourceUri) { @TruffleBoundary protected final @Nullable Object doRead(String resourceUri, VmContext context, Node readNode) { var resolvedUri = resolveResource(currentModule, resourceUri); - return context.getResourceManager().read(resolvedUri, readNode).orElse(null); + return context + .getResourceManager() + .read(currentModule.getUri(), resolvedUri, readNode) + .orElse(null); } private URI resolveResource(ModuleKey moduleKey, String resourceUri) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobMemberBodyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobMemberBodyNode.java index 478bb23b4..e0855aab8 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobMemberBodyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobMemberBodyNode.java @@ -20,6 +20,7 @@ import com.oracle.truffle.api.source.SourceSection; import java.util.Map; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.module.ModuleKey; import org.pkl.core.runtime.VmContext; import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmUtils; @@ -27,8 +28,11 @@ /** Used by {@link ReadGlobNode}. */ public class ReadGlobMemberBodyNode extends ExpressionNode { - public ReadGlobMemberBodyNode(SourceSection sourceSection) { + private final ModuleKey currentModule; + + public ReadGlobMemberBodyNode(SourceSection sourceSection, ModuleKey currentModule) { super(sourceSection); + this.currentModule = currentModule; } @Override @@ -44,7 +48,11 @@ private Object readResource(VmObjectLike mapping, String path) { var globElement = VmUtils.getMapValue(globElements, path); assert globElement != null; var resourceUri = globElement.uri(); - var resource = VmContext.get(this).getResourceManager().read(resourceUri, this).orElse(null); + var resource = + VmContext.get(this) + .getResourceManager() + .read(currentModule.getUri(), resourceUri, this) + .orElse(null); if (resource == null) { CompilerDirectives.transferToInterpreter(); throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build(); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java index 645f73fec..461a1923c 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java @@ -58,7 +58,7 @@ private SharedMemberNode getMemberNode() { "", language, new FrameDescriptor(), - new ReadGlobMemberBodyNode(sourceSection)); + new ReadGlobMemberBodyNode(sourceSection, currentModule)); } return memberNode; } diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java index 850c0934a..329f4d1bc 100644 --- a/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java @@ -198,13 +198,13 @@ protected InputStream openExternalUri(URI uri) throws SecurityManagerException { } // treat package assets as resources instead of modules - securityManager.checkReadResource(uri); + securityManager.checkResolveResource(uri); var request = HttpRequest.newBuilder(uri).build(); HttpResponse response; try { response = httpClient.send( - request, BodyHandlers.ofInputStream(), securityManager::checkReadResource); + request, BodyHandlers.ofInputStream(), securityManager::checkResolveResource); } catch (IOException e) { throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage()); } 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 ce73865db..df9bed7f0 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 @@ -357,7 +357,7 @@ public Optional read(URI uri) var request = HttpRequest.newBuilder(uri).build(); var response = httpClient.send( - request, BodyHandlers.ofByteArray(), securityManager::checkReadResource); + request, BodyHandlers.ofByteArray(), securityManager::checkResolveResource); if (response.statusCode() == 404) return Optional.empty(); HttpUtils.checkHasStatusCode200(response); return Optional.of(new Resource(uri, response.body())); @@ -550,7 +550,7 @@ public Optional read(URI uri) if (local != null) { var resourceManager = VmContext.get(null).getResourceManager(); var securityManager = VmContext.get(null).getSecurityManager(); - securityManager.checkReadResource(local); + securityManager.checkResolveResource(local); var reader = resourceManager.getResourceReader(local); if (reader == null) { throw new VmExceptionBuilder() diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/CommandModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/CommandModule.java index 5f6491b4c..4fd1b7a32 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/CommandModule.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/CommandModule.java @@ -59,6 +59,10 @@ public static VmClass getImportClass() { return ImportClass.instance; } + public static VmClass getReadClass() { + return ReadClass.instance; + } + private static final class CommandInfoClass { static final VmClass instance = loadClass("CommandInfo"); } @@ -87,6 +91,10 @@ private static final class ImportClass { static final VmClass instance = loadClass("Import"); } + private static final class ReadClass { + static final VmClass instance = loadClass("Read"); + } + @TruffleBoundary private static VmClass loadClass(String className) { var theModule = getModule(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java index 720dc8944..90e19e375 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java @@ -60,6 +60,7 @@ import org.pkl.core.ast.type.TypeNode; import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.externalreader.ExternalReaderProcessException; +import org.pkl.core.http.HttpClientException; import org.pkl.core.module.ModuleKeys; import org.pkl.core.module.ResolvedModuleKey; import org.pkl.core.util.EconomicMaps; @@ -487,14 +488,17 @@ public OptionBehavior(@Nullable VmTyped annotation, boolean hasMetavar) { ? null : VmUtils.readMember(annotation, Identifier.CONVERT) instanceof VmFunction func ? (rawValue, workingDirUri) -> - handleBadValue(() -> handleImports(func.apply(rawValue), workingDirUri)) + handleBadValue( + () -> handleImportsAndReads(func.apply(rawValue), workingDirUri)) : null, annotation == null ? null : VmUtils.readMember(annotation, Identifier.TRANSFORM_ALL) instanceof VmFunction func ? (values, workingDirUri) -> handleBadValue( - () -> handleImports(func.apply(VmList.create(values)), workingDirUri)) + () -> + handleImportsAndReads( + func.apply(VmList.create(values)), workingDirUri)) : null, annotation == null ? null @@ -1061,15 +1065,12 @@ private CommandSpec.Result evaluateResult(VmTyped module, SubcommandState parent } // endregion - // region dynamic import handling - - private static boolean isImport(VmTyped value) { - return value.getVmClass() == CommandModule.getImportClass(); - } + // region dynamic import/read handling - private static boolean isImport(Object value) { + private static boolean isImportOrRead(Object value) { return value instanceof VmTyped vmTyped - && vmTyped.getVmClass() == CommandModule.getImportClass(); + && (vmTyped.getVmClass() == CommandModule.getImportClass() + || vmTyped.getVmClass() == CommandModule.getReadClass()); } // handle errors from convert/transformAll and correctly format them for the CLI @@ -1101,79 +1102,83 @@ private T handleErrors(Supplier f) { } } + private Object handleImportOrRead(Object val, URI workingDirUri) { + if (!(val instanceof VmTyped vmTyped)) return val; + if (vmTyped.getVmClass() == CommandModule.getImportClass()) { + return handleImport(vmTyped, workingDirUri); + } + if (vmTyped.getVmClass() == CommandModule.getReadClass()) { + return handleRead(vmTyped, workingDirUri); + } + return val; + } + // for convert, handle imports by replacing Command.Import values // with imported module or Mapping values // Command.Import instances in returned Pair, List, Set, or Map values are replaced as well // other types or nested instances of the above are not affected - private Object handleImports(Object result, URI workingDirUri) { - if (result instanceof VmTyped vmTyped && isImport(vmTyped)) { - return handleImport(vmTyped, workingDirUri); - } else if (result instanceof VmPair vmPair) { - if (!isImport(vmPair.getFirst()) && !isImport(vmPair.getSecond())) { + private Object handleImportsAndReads(Object result, URI workingDirUri) { + if (result instanceof VmPair vmPair) { + if (!isImportOrRead(vmPair.getFirst()) && !isImportOrRead(vmPair.getSecond())) { return vmPair; } return new VmPair( - isImport(vmPair.getFirst()) - ? handleImport((VmTyped) vmPair.getFirst(), workingDirUri) - : vmPair.getFirst(), - isImport(vmPair.getSecond()) - ? handleImport((VmTyped) vmPair.getSecond(), workingDirUri) - : vmPair.getSecond()); + handleImportOrRead(vmPair.getFirst(), workingDirUri), + handleImportOrRead(vmPair.getSecond(), workingDirUri)); } else if (result instanceof VmCollection vmCollection) { for (var elem : vmCollection) { - if (isImport(elem)) { + if (isImportOrRead(elem)) { var builder = vmCollection.builder(); - vmCollection.forEach( - it -> builder.add(isImport(it) ? handleImport((VmTyped) it, workingDirUri) : it)); + vmCollection.forEach(it -> builder.add(handleImportOrRead(it, workingDirUri))); return builder.build(); } } return vmCollection; } else if (result instanceof VmMap vmMap) { for (var entry : vmMap) { - if (isImport(entry.getKey()) || isImport(entry.getValue())) { + if (isImportOrRead(entry.getKey()) || isImportOrRead(entry.getValue())) { var builder = VmMap.builder(); vmMap.forEach( it -> builder.add( - isImport(it.getKey()) - ? handleImport((VmTyped) it.getKey(), workingDirUri) - : it.getKey(), - isImport(it.getValue()) - ? handleImport((VmTyped) it.getValue(), workingDirUri) - : it.getValue())); + handleImportOrRead(it.getKey(), workingDirUri), + handleImportOrRead(it.getValue(), workingDirUri))); return builder.build(); } } } - return result; + return handleImportOrRead(result, workingDirUri); } - private Object handleImport(VmTyped mport, URI workingDirUri) { - var moduleName = (String) VmUtils.readMember(mport, Identifier.URI); - String uriString; + private URI getModuleUriString( + VmTyped directive, URI workingDirUri, String errorKey, boolean checkTripleDot) { + var name = (String) VmUtils.readMember(directive, Identifier.URI); + + if (checkTripleDot && name.startsWith("...")) { + throw exceptionBuilder().evalError("cannotGlobTripleDots").build(); + } + // Ported from org.pkl.cli.commons.cli.commands.BaseOptions: try { // Can't just use URI constructor, because URI(null, null, "C:/foo/bar", null) turns // into `URI("C", null, "/foo/bar", null)`. @SuppressWarnings("DuplicateExpressions") var uri = - IoUtils.isUriLike(moduleName) - ? new URI(moduleName) - : IoUtils.isWindows() && IoUtils.isWindowsAbsolutePath(moduleName) - ? Path.of(moduleName).toUri() - : new URI(null, null, IoUtils.toNormalizedPathString(Path.of(moduleName)), null); - uriString = - uri.isAbsolute() ? uri.toString() : IoUtils.resolve(workingDirUri, uri).toString(); + IoUtils.isUriLike(name) + ? new URI(name) + : IoUtils.isWindows() && IoUtils.isWindowsAbsolutePath(name) + ? Path.of(name).toUri() + : new URI(null, null, IoUtils.toNormalizedPathString(Path.of(name)), null); + return uri.isAbsolute() ? uri : IoUtils.resolve(workingDirUri, uri); } catch (URISyntaxException e) { - throw exceptionBuilder() - .evalError("invalidModuleUri", moduleName) - .withHint(e.getReason()) - .build(); + throw exceptionBuilder().evalError(errorKey, name).withHint(e.getReason()).build(); } + } + private Object handleImport(VmTyped mport, URI workingDirUri) { + var importUri = getModuleUriString(mport, workingDirUri, "invalidModuleUri", false); + var uriString = importUri.toString(); var isGlob = (Boolean) VmUtils.readMember(mport, Identifier.GLOB); - var importUri = URI.create(uriString); var language = VmLanguage.get(null); // non-glob @@ -1214,6 +1219,51 @@ private Object handleImport(VmTyped mport, URI workingDirUri) { } } + private Object handleRead(VmTyped read, URI workingDirUri) { + var type = (String) VmUtils.readMember(read, Identifier.TYPE); + var isGlob = "glob".equals(type); + var uri = getModuleUriString(read, workingDirUri, "invalidResourceUri", isGlob); + var uriString = uri.toString(); + var context = VmContext.get(null); + + // non-glob + if (!isGlob) { + var resource = context.getResourceManager().read(REPL_TEXT_URI, uri, null); + if ("nullable".equals(type)) return resource.orElse(VmNull.withoutDefault()); + if (resource.isEmpty()) { + throw exceptionBuilder().evalError("cannotFindResource", uriString).build(); + } + return resource.get(); + } + + // glob + try { + var reader = context.getResourceManager().getReader(uri, null); + if (!reader.isGlobbable()) { + throw exceptionBuilder().evalError("cannotGlobUri", uri, uri.getScheme()).build(); + } + var resolvedElements = + GlobResolver.resolveGlob(context.getSecurityManager(), reader, null, null, uriString); + + var builder = new VmObjectBuilder(resolvedElements.size()); + for (var entry : resolvedElements.entrySet()) { + builder.addEntry(entry.getKey(), reader.read(entry.getValue().uri())); + } + return builder.toMapping(resolvedElements); + } catch (IOException e) { + throw exceptionBuilder().evalError("ioErrorResolvingGlob", uriString).withCause(e).build(); + } catch (SecurityManagerException | HttpClientException | URISyntaxException e) { + throw exceptionBuilder().withCause(e).build(); + } catch (InvalidGlobPatternException e) { + throw exceptionBuilder() + .evalError("invalidGlobPattern", uriString) + .withHint(e.getMessage()) + .build(); + } catch (ExternalReaderProcessException e) { + throw exceptionBuilder().evalError("externalReaderFailure").withCause(e).build(); + } + } + // endregion // region utilities diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java index 9009ccf17..f9ce37652 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java @@ -161,6 +161,7 @@ public final class Identifier implements Comparable { public static final Identifier CONVERT = get("convert"); public static final Identifier TRANSFORM_ALL = get("transformAll"); public static final Identifier GLOB = get("glob"); + public static final Identifier TYPE = get("type"); public static final Identifier COMPLETION_CANDIDATES = get("completionCandidates"); public static final Identifier ILLEGAL = get("`"); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java index aa0ac34aa..67604825f 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java @@ -58,11 +58,11 @@ public ResourceManager(SecurityManager securityManager, Collection doRead(ResourceReader reader, URI uri, @Nullable Node re } @TruffleBoundary - public Optional read(URI resourceUri, @Nullable Node readNode) { + public Optional read(URI readingModule, URI resourceUri, @Nullable Node readNode) { return resources.computeIfAbsent( resourceUri.normalize(), (uri) -> { @@ -105,7 +105,7 @@ public Optional read(URI resourceUri, @Nullable Node readNode) { if (!(reader instanceof ResourceReaders.HttpResource) && !(reader instanceof ResourceReaders.HttpsResource)) { try { - securityManager.checkReadResource(uri); + securityManager.checkReadResource(readingModule, uri); } catch (SecurityManagerException e) { throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ModuleClassNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ModuleClassNodes.java index d4998bda5..aa2110771 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ModuleClassNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ModuleClassNodes.java @@ -21,6 +21,7 @@ import java.net.URI; import org.pkl.core.runtime.*; import org.pkl.core.stdlib.ExternalMethod1Node; +import org.pkl.core.stdlib.ExternalPropertyNode; import org.pkl.core.stdlib.PklName; @PklName("Module") @@ -59,4 +60,14 @@ protected VmList eval(VmObjectLike self, VmObjectLike other) { .build(); } } + + /** Bypass resource trust level check when determining the output format */ + public abstract static class outputFormat extends ExternalPropertyNode { + @Specialization + protected Object eval(VmObjectLike self) { + var context = VmContext.get(this); + var outputFormat = context.getExternalProperties().get("pkl.outputFormat"); + return outputFormat != null ? outputFormat : VmNull.withoutDefault(); + } + } } 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 b1e52a977..c598a6fd3 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -727,8 +727,8 @@ Refusing to read resource `{0}` because it is not within the root directory (`-- modulePastRootDir=\ Refusing to load module `{0}` because it is not within the root directory (`--root-dir`). -insufficientModuleTrustLevel=\ -Refusing to import module `{0}` because importing module `{1}` has an insufficient trust level. +insufficientTrustLevel=\ +Refusing to {2} `{0}` because {3} module `{1}` has an insufficient trust level. invalidRegexSyntax=\ Syntax error in regex `{0}`: {1} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/SecurityManagersTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/SecurityManagersTest.kt index 3869b76fd..19925ae6b 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/SecurityManagersTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/SecurityManagersTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -63,30 +63,32 @@ class SecurityManagersTest { } @Test - fun `checkReadResource() - complete match`() { - val e = catchThrowable { manager.checkReadResource(URI("env:FOO_BAR")) } + fun `checkResolveResource - complete match`() { + val e = catchThrowable { manager.checkResolveResource(URI("env:FOO_BAR")) } assertThat(e).doesNotThrowAnyException() } @Test - fun `checkReadResource() - partial match from start`() { - val e = catchThrowable { manager.checkReadResource(URI("env:FOO_BAR_BAZ")) } + fun `checkResolveResource - partial match from start`() { + val e = catchThrowable { manager.checkResolveResource(URI("env:FOO_BAR_BAZ")) } assertThat(e).doesNotThrowAnyException() } @Test - fun `checkReadResource() - partial match not from start`() { - assertThrows { manager.checkReadResource(URI("other:env:FOO_BAR")) } + fun `checkResolveResource - partial match not from start`() { + assertThrows { + manager.checkResolveResource(URI("other:env:FOO_BAR")) + } } @Test - fun `checkReadResource() - no match`() { - assertThrows { manager.checkReadResource(URI("other:uri")) } + fun `checkResolveResource - no match`() { + assertThrows { manager.checkResolveResource(URI("other:uri")) } } @Test - fun `checkReadResource() - no match #2`() { - assertThrows { manager.checkReadResource(URI("env:FOO_BAZ")) } + fun `checkResolveResource - no match #2`() { + assertThrows { manager.checkResolveResource(URI("env:FOO_BAZ")) } } @Test @@ -148,10 +150,10 @@ class SecurityManagersTest { val path = rootDir.resolve("baz.pkl") Files.createFile(path) manager.checkResolveModule(path.toUri()) - manager.checkReadResource(path.toUri()) + manager.checkResolveResource(path.toUri()) manager.checkResolveModule(rootDir.toUri().resolve("qux/../baz.pkl")) - manager.checkReadResource(rootDir.toUri().resolve("qux/../baz.pkl")) + manager.checkResolveResource(rootDir.toUri().resolve("qux/../baz.pkl")) } @Test @@ -167,10 +169,10 @@ class SecurityManagersTest { ) manager.checkResolveModule(Path.of("/foo/bar/baz.pkl").toUri()) - manager.checkReadResource(Path.of("/foo/bar/baz.pkl").toUri()) + manager.checkResolveResource(Path.of("/foo/bar/baz.pkl").toUri()) manager.checkResolveModule(Path.of("/foo/bar/qux/../baz.pkl").toUri()) - manager.checkReadResource(Path.of("/foo/bar/qux/../baz.pkl").toUri()) + manager.checkResolveResource(Path.of("/foo/bar/qux/../baz.pkl").toUri()) } @Test @@ -191,14 +193,14 @@ class SecurityManagersTest { val path = rootDir.resolve("../baz.pkl") Files.createFile(path) assertThrows { manager.checkResolveModule(path.toUri()) } - assertThrows { manager.checkReadResource(path.toUri()) } + assertThrows { manager.checkResolveResource(path.toUri()) } val symlink = rootDir.resolve("qux") Files.createSymbolicLink(symlink, tempDir) val path2 = symlink.resolve("baz2.pkl") Files.createFile(path2) assertThrows { manager.checkResolveModule(path2.toUri()) } - assertThrows { manager.checkReadResource(path2.toUri()) } + assertThrows { manager.checkResolveResource(path2.toUri()) } } @Test @@ -217,14 +219,14 @@ class SecurityManagersTest { manager.checkResolveModule(Path.of("/foo/baz.pkl").toUri()) } assertThrows { - manager.checkReadResource(Path.of("/foo/baz.pkl").toUri()) + manager.checkResolveResource(Path.of("/foo/baz.pkl").toUri()) } assertThrows { manager.checkResolveModule(Path.of("/foo/bar/../baz.pkl").toUri()) } assertThrows { - manager.checkReadResource(Path.of("/foo/bar/../baz.pkl").toUri()) + manager.checkResolveResource(Path.of("/foo/bar/../baz.pkl").toUri()) } } } diff --git a/pkl-core/src/test/kotlin/org/pkl/core/util/IoUtilsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/util/IoUtilsTest.kt index 8a7076c71..bf24bb1f6 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/util/IoUtilsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/util/IoUtilsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -38,7 +38,7 @@ class IoUtilsTest { override fun checkImportModule(importingModule: URI, importedModule: URI) {} - override fun checkReadResource(resource: URI) {} + override fun checkReadResource(readingModule: URI, resource: URI) {} override fun checkResolveResource(resource: URI) {} } diff --git a/stdlib/Command.pkl b/stdlib/Command.pkl index fd1630625..a6fae847b 100644 --- a/stdlib/Command.pkl +++ b/stdlib/Command.pkl @@ -259,6 +259,9 @@ class Argument extends Annotation { /// A value used in [Flag.convert], [Flag.transformAll], [Argument.convert], and /// [Argument.transformAll] functions to trigger a dynamic module import. +/// +/// Imports are evaluated as if running in the Pkl REPL, so typical restrictions on remote modules +/// importing local modules do not apply. class Import { /// The URI of the module to import. /// @@ -272,6 +275,25 @@ class Import { glob: Boolean = false } +/// A value used in [Flag.convert], [Flag.transformAll], [Argument.convert], and +/// [Argument.transformAll] functions to trigger a dynamic resource read. +/// +/// Reads are evaluated as if running in the Pkl REPL, so typical restrictions on remote modules +/// reading local resources do not apply. +class Read { + /// The URI of the resource to read. + /// + /// Relative paths are resolved relative to the current working directory. + uri: String + + /// Resource read behavior. + /// + /// When `null`, the replacement is a [Resource] or [String]. Replacement fails if the resource does not exist. + /// When `"nullable"`, the replacement is a [Resource] or [String] if the resource exists and `null` if it does not. + /// When `"glob"`, the replacement value is a [Mapping] from [String] keys to matched [Resource] or [String] values. + type: ("glob" | "nullable")? +} + local const quantityRegex = Regex(#"([0-9]+(?:\.[0-9]+)?)\.?([A-Za-z]+)"#) local const function parseQuantity(value: String, typeName: String): Pair = diff --git a/stdlib/base.pkl b/stdlib/base.pkl index ee3e94ef2..d8761aa9e 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -95,6 +95,8 @@ abstract external class Module { /// path = rootModule.relativePathTo(module) /// ``` external function relativePathTo(other: Module): List + + external local outputFormat: String? /// The output of this module. /// @@ -103,7 +105,7 @@ abstract external class Module { hidden output: ModuleOutput = new { value = outer renderer = - let (format = read?("prop:pkl.outputFormat") ?? "pcf") + let (format = outputFormat ?? "pcf") if (format == "json") new JsonRenderer {} else if (format == "jsonnet")