diff --git a/packages/graalvm/api/graalvm.api b/packages/graalvm/api/graalvm.api index cef9c17665..344d81d9c5 100644 --- a/packages/graalvm/api/graalvm.api +++ b/packages/graalvm/api/graalvm.api @@ -3842,6 +3842,14 @@ public abstract interface class elide/runtime/intrinsics/js/node/WritableFilesys public abstract fun mkdirSync (Lorg/graalvm/polyglot/Value;)Ljava/lang/String; public abstract fun mkdirSync (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)Ljava/lang/String; public static synthetic fun mkdirSync$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemAPI;Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/MkdirOptions;ILjava/lang/Object;)Ljava/lang/String; + public abstract fun opendir (Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;Lkotlin/jvm/functions/Function2;)V + public abstract fun opendir (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)V + public static synthetic fun opendir$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemAPI;Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun opendir$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemAPI;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;ILjava/lang/Object;)V + public abstract fun opendirSync (Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;)Lelide/runtime/node/fs/Directory; + public abstract fun opendirSync (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)Lelide/runtime/node/fs/Directory; + public static synthetic fun opendirSync$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemAPI;Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;ILjava/lang/Object;)Lelide/runtime/node/fs/Directory; + public static synthetic fun opendirSync$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemAPI;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;ILjava/lang/Object;)Lelide/runtime/node/fs/Directory; public abstract fun writeFile (Lelide/runtime/intrinsics/js/node/path/Path;Ljava/lang/String;Lelide/runtime/intrinsics/js/node/fs/WriteFileOptions;Lkotlin/jvm/functions/Function1;)V public abstract fun writeFile (Lelide/runtime/intrinsics/js/node/path/Path;[BLelide/runtime/intrinsics/js/node/fs/WriteFileOptions;Lkotlin/jvm/functions/Function1;)V public abstract fun writeFile (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)V @@ -3865,6 +3873,10 @@ public abstract interface class elide/runtime/intrinsics/js/node/WritableFilesys public abstract fun mkdir (Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/JsPromise; public abstract fun mkdir (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/JsPromise; public static synthetic fun mkdir$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemPromiseAPI;Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/MkdirOptions;ILjava/lang/Object;)Lelide/runtime/intrinsics/js/JsPromise; + public abstract fun opendir (Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;)Lelide/runtime/intrinsics/js/JsPromise; + public abstract fun opendir (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/JsPromise; + public static synthetic fun opendir$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemPromiseAPI;Lelide/runtime/intrinsics/js/node/path/Path;Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;ILjava/lang/Object;)Lelide/runtime/intrinsics/js/JsPromise; + public static synthetic fun opendir$default (Lelide/runtime/intrinsics/js/node/WritableFilesystemPromiseAPI;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;ILjava/lang/Object;)Lelide/runtime/intrinsics/js/JsPromise; public abstract fun writeFile (Lelide/runtime/intrinsics/js/node/path/Path;Ljava/lang/Object;Lelide/runtime/intrinsics/js/node/fs/WriteFileOptions;)Lelide/runtime/intrinsics/js/JsPromise; public abstract fun writeFile (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/JsPromise; public abstract fun writeFile (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/JsPromise; @@ -4597,10 +4609,25 @@ public final class elide/runtime/intrinsics/js/node/events/RemoveEventListenerOp public final fun getDEFAULTS ()Lelide/runtime/intrinsics/js/node/events/RemoveEventListenerOptions; } -public abstract interface class elide/runtime/intrinsics/js/node/fs/Dir { +public abstract interface class elide/runtime/intrinsics/js/node/fs/Dir : elide/runtime/interop/ReadOnlyProxyObject, java/lang/AutoCloseable, org/graalvm/polyglot/proxy/ProxyIterable { + public abstract fun close (Lkotlin/jvm/functions/Function0;)V + public abstract fun close (Lorg/graalvm/polyglot/Value;)V + public abstract fun closeSync ()V + public abstract fun getPath ()Ljava/lang/String; + public abstract fun read (Lorg/graalvm/polyglot/Value;)V + public abstract fun readSync ()Lelide/runtime/intrinsics/js/node/fs/Dirent; } -public abstract interface class elide/runtime/intrinsics/js/node/fs/Dirent { +public abstract interface class elide/runtime/intrinsics/js/node/fs/Dirent : elide/runtime/interop/ReadOnlyProxyObject { + public abstract fun getName ()Ljava/lang/String; + public abstract fun getParentPath ()Ljava/lang/String; + public abstract fun isBlockDevice ()Z + public abstract fun isCharacterDevice ()Z + public abstract fun isDirectory ()Z + public abstract fun isFIFO ()Z + public abstract fun isFile ()Z + public abstract fun isSocket ()Z + public abstract fun isSymbolicLink ()Z } public abstract interface class elide/runtime/intrinsics/js/node/fs/FSWatcher { @@ -4631,6 +4658,30 @@ public final class elide/runtime/intrinsics/js/node/fs/MkdirOptions$Companion { public final fun getDEFAULTS ()Lelide/runtime/intrinsics/js/node/fs/MkdirOptions; } +public final class elide/runtime/intrinsics/js/node/fs/OpenDirOptions { + public static final field Companion Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions$Companion; + public fun ()V + public fun (Ljava/lang/Object;Ljava/lang/Integer;Z)V + public synthetic fun (Ljava/lang/Object;Ljava/lang/Integer;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Object; + public final fun component2 ()Ljava/lang/Integer; + public final fun component3 ()Z + public final fun copy (Ljava/lang/Object;Ljava/lang/Integer;Z)Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions; + public static synthetic fun copy$default (Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions;Ljava/lang/Object;Ljava/lang/Integer;ZILjava/lang/Object;)Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions; + public fun equals (Ljava/lang/Object;)Z + public static final fun from (Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions; + public final fun getBufferSize ()Ljava/lang/Integer; + public final fun getEncoding ()Ljava/lang/Object; + public final fun getRecursive ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class elide/runtime/intrinsics/js/node/fs/OpenDirOptions$Companion { + public final fun from (Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions; + public final fun getDEFAULTS ()Lelide/runtime/intrinsics/js/node/fs/OpenDirOptions; +} + public final class elide/runtime/intrinsics/js/node/fs/ReadFileOptions { public static final field Companion Lelide/runtime/intrinsics/js/node/fs/ReadFileOptions$Companion; public fun ()V @@ -8162,6 +8213,81 @@ public synthetic class elide/runtime/node/fs/$VfsInitializerListener$Definition public fun load ()Lio/micronaut/inject/BeanDefinition; } +public final class elide/runtime/node/fs/Directory : elide/runtime/intrinsics/js/node/fs/Dir { + public static final field Factory Lelide/runtime/node/fs/Directory$Factory; + public synthetic fun (Ljava/io/File;Ljava/lang/String;Ljava/nio/file/DirectoryStream;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V + public fun close (Lkotlin/jvm/functions/Function0;)V + public fun close (Lorg/graalvm/polyglot/Value;)V + public fun closeSync ()V + public synthetic fun getIterator ()Ljava/lang/Object; + public fun getIterator ()Lorg/graalvm/polyglot/proxy/ProxyIterator; + public fun getMember (Ljava/lang/String;)Ljava/lang/Object; + public synthetic fun getMemberKeys ()Ljava/lang/Object; + public fun getMemberKeys ()[Ljava/lang/String; + public fun getPath ()Ljava/lang/String; + public static final fun of (Ljava/io/File;Ljava/nio/file/Path;)Lelide/runtime/node/fs/Directory; + public fun read (Lorg/graalvm/polyglot/Value;)V + public fun readSync ()Lelide/runtime/intrinsics/js/node/fs/Dirent; + public static final fun wrap (Ljava/io/File;Ljava/nio/file/DirectoryStream;)Lelide/runtime/node/fs/Directory; +} + +public final class elide/runtime/node/fs/Directory$Factory : org/graalvm/polyglot/proxy/ProxyInstantiable { + public fun newInstance ([Lorg/graalvm/polyglot/Value;)Ljava/lang/Object; + public final fun of (Ljava/io/File;Ljava/nio/file/Path;)Lelide/runtime/node/fs/Directory; + public final fun wrap (Ljava/io/File;Ljava/nio/file/DirectoryStream;)Lelide/runtime/node/fs/Directory; +} + +public abstract interface class elide/runtime/node/fs/DirectoryEntry : elide/runtime/intrinsics/js/node/fs/Dirent { + public static final field Factory Lelide/runtime/node/fs/DirectoryEntry$Factory; + public static fun forFile (Ljava/io/File;)Lelide/runtime/node/fs/DirectoryEntry$FileEntry; + public static fun forPath (Ljava/nio/file/Path;)Lelide/runtime/node/fs/DirectoryEntry$PathEntry; + public fun getMember (Ljava/lang/String;)Ljava/lang/Object; + public synthetic fun getMemberKeys ()Ljava/lang/Object; + public fun getMemberKeys ()[Ljava/lang/String; +} + +public final class elide/runtime/node/fs/DirectoryEntry$Factory : org/graalvm/polyglot/proxy/ProxyInstantiable { + public final fun forFile (Ljava/io/File;)Lelide/runtime/node/fs/DirectoryEntry$FileEntry; + public final fun forPath (Ljava/nio/file/Path;)Lelide/runtime/node/fs/DirectoryEntry$PathEntry; + public fun newInstance ([Lorg/graalvm/polyglot/Value;)Ljava/lang/Object; +} + +public final class elide/runtime/node/fs/DirectoryEntry$FileEntry : elide/runtime/node/fs/DirectoryEntry { + public synthetic fun (Ljava/io/File;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getFile ()Ljava/io/File; + public fun getName ()Ljava/lang/String; + public fun getParentPath ()Ljava/lang/String; + public fun isBlockDevice ()Z + public fun isCharacterDevice ()Z + public fun isDirectory ()Z + public fun isFIFO ()Z + public fun isFile ()Z + public fun isSocket ()Z + public fun isSymbolicLink ()Z + public static final fun of (Ljava/io/File;)Lelide/runtime/node/fs/DirectoryEntry$FileEntry; +} + +public final class elide/runtime/node/fs/DirectoryEntry$PathEntry : elide/runtime/node/fs/DirectoryEntry { + public synthetic fun (Ljava/nio/file/Path;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getName ()Ljava/lang/String; + public fun getParentPath ()Ljava/lang/String; + public final fun getPath ()Ljava/nio/file/Path; + public fun isBlockDevice ()Z + public fun isCharacterDevice ()Z + public fun isDirectory ()Z + public fun isFIFO ()Z + public fun isFile ()Z + public fun isSocket ()Z + public fun isSymbolicLink ()Z + public static final fun of (Ljava/nio/file/Path;)Lelide/runtime/node/fs/DirectoryEntry$PathEntry; +} + +public final class elide/runtime/node/fs/DirectoryEntryKt { + public static final fun asDirectoryEntry (Ljava/io/File;)Lelide/runtime/node/fs/DirectoryEntry$FileEntry; + public static final fun asDirectoryEntry (Ljava/nio/file/Path;)Lelide/runtime/node/fs/DirectoryEntry$PathEntry; +} + public final class elide/runtime/node/fs/VfsInitializerListener : elide/runtime/plugins/vfs/VfsListener, java/util/function/Supplier { public fun ()V public fun get ()Lelide/runtime/vfs/GuestVFS; diff --git a/packages/graalvm/build.gradle.kts b/packages/graalvm/build.gradle.kts index 149c05cd42..1c242f35af 100644 --- a/packages/graalvm/build.gradle.kts +++ b/packages/graalvm/build.gradle.kts @@ -423,7 +423,6 @@ graalvmNative { classpath(tasks.compileJava, tasks.compileKotlin, configurations.nativeImageClasspath) buildArgs(sharedLibArgs.plus(listOf( - // "-H:LayerUse=${baseLayer.get().asFile.absolutePath}", "-H:LayerCreate=${layerOut.get().asFile.name}" ))) } @@ -432,6 +431,7 @@ graalvmNative { fallback = false sharedLibrary = false quickBuild = true + jvmArgs("-Dpolyglot.engine.WarnInterpreterOnly=false") buildArgs(sharedLibArgs.plus(testLibArgs).plus(listOf( "--features=org.graalvm.junit.platform.JUnitPlatformFeature", ))) diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemAPI.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemAPI.kt index e9548eb4cd..03a1229028 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemAPI.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemAPI.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Elide Technologies, Inc. + * Copyright (c) 2024-2025 Elide Technologies, Inc. * * Licensed under the MIT license (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at @@ -17,6 +17,7 @@ import java.nio.file.AccessMode import elide.annotations.API import elide.runtime.intrinsics.js.node.fs.* import elide.runtime.intrinsics.js.node.path.Path +import elide.runtime.node.fs.Directory import elide.vm.annotations.Polyglot /** @@ -512,6 +513,80 @@ import elide.vm.annotations.Polyglot * @param dest The destination path to copy to. */ @Polyglot public fun copyFileSync(src: Value, dest: Value) + + /** + * ## Method: `fs.opendir` + * + * Asynchronously open a directory at the provided [path], employing the provided [options]; a `Dir` instance is + * passed to the provided [callback] for directory operations. + * + * From the Node.js docs: + * "Asynchronously open a directory. See the POSIX `opendir(3)` documentation for more details. Creates an `fs.Dir`, + * which contains all further functions for reading from and cleaning up the directory. The `encoding` option sets the + * encoding for the path while opening the directory and subsequent read operations." + * + * This method variant is meant for implementation and host-side dispatch. + * + * @param path Path to the directory to open. + * @param options Options to apply to the operation. + * @param callback Callback function to invoke with the directory iterator. + */ + @Polyglot public fun opendir(path: Path, options: OpenDirOptions = OpenDirOptions.DEFAULTS, callback: OpenDirCallback) + + /** + * ## Method: `fs.opendir` + * + * Asynchronously open a directory at the provided [path], employing the provided [options]; a `Dir` instance is + * passed to the provided [callback] for directory operations. + * + * From the Node.js docs: + * "Asynchronously open a directory. See the POSIX `opendir(3)` documentation for more details. Creates an `fs.Dir`, + * which contains all further functions for reading from and cleaning up the directory. The `encoding` option sets the + * encoding for the path while opening the directory and subsequent read operations." + * + * This method variant is meant for guest-side dispatch. + * + * @param path Path to the directory to open. + * @param options Options to apply to the operation. + * @param callback Callback function to invoke with the directory iterator. + */ + @Polyglot public fun opendir(path: Value, options: Value? = null, callback: Value) + + /** + * ## Method: `fs.opendirSync` + * + * Synchronously open a directory at the provided [path], employing the provided [options]. + * + * From the Node.js docs: + * "Synchronously open a directory. See `opendir(3)`. Creates an `fs.Dir`, which contains all further functions for + * reading from and cleaning up the directory. The encoding option sets the encoding for the path while opening the + * directory and subsequent read operations." + * + * This method variant is meant for implementation and host-side dispatch. + * + * @param path Path to the directory to open. + * @param options Options to apply to the operation. + * @return [Directory] handle instance. + */ + @Polyglot public fun opendirSync(path: Path, options: OpenDirOptions = OpenDirOptions.DEFAULTS): Directory + + /** + * ## Method: `fs.opendirSync` + * + * Synchronously open a directory at the provided [path], employing the provided [options], if any. + * + * From the Node.js docs: + * "Synchronously open a directory. See `opendir(3)`. Creates an `fs.Dir`, which contains all further functions for + * reading from and cleaning up the directory. The encoding option sets the encoding for the path while opening the + * directory and subsequent read operations." + * + * This method variant is meant for guest-side dispatch. + * + * @param path Path to the directory to open. + * @param options Options to apply to the operation. + * @return [Directory] handle instance. + */ + @Polyglot public fun opendirSync(path: Value, options: Value? = null): Directory } /** diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemPromiseAPI.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemPromiseAPI.kt index 16970cf9bc..0da1b188f7 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemPromiseAPI.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/FilesystemPromiseAPI.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Elide Technologies, Inc. + * Copyright (c) 2024-2025 Elide Technologies, Inc. * * Licensed under the MIT license (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at @@ -341,6 +341,28 @@ import elide.vm.annotations.Polyglot * @return Upon success, fulfills with `undefined`; otherwise, rejects with an error. */ @Polyglot public fun copyFile(src: Value, dest: Value, mode: Int): JsPromise + + /** + * ## Method: `fs.opendir` + * + * Asynchronously opens a directory for the streaming of entries. + * + * @param path Path to the directory to stream entries for. + * @param options Options to apply to the operation. + * @return Upon success, fulfills with an instance of [Dir]; otherwise, errors. + */ + public fun opendir(path: Path, options: OpenDirOptions? = null): JsPromise + + /** + * ## Method: `fs.opendir` + * + * Asynchronously opens a directory for the streaming of entries. + * + * @param path Path to the directory to stream entries for. + * @param options Options to apply to the operation. + * @return Upon success, fulfills with an instance of [Dir]; otherwise, errors. + */ + @Polyglot public fun opendir(path: Value, options: Value? = null): JsPromise } /** diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dir.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dir.kt index dc8d2f53fa..4ee8b6a257 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dir.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dir.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Elide Technologies, Inc. + * Copyright (c) 2024-2025 Elide Technologies, Inc. * * Licensed under the MIT license (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at @@ -12,10 +12,47 @@ */ package elide.runtime.intrinsics.js.node.fs +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyIterable import elide.annotations.API +import elide.runtime.interop.ReadOnlyProxyObject +import elide.vm.annotations.Polyglot /** - * + * ## Node Filesystem: Directory */ -@API public interface Dir { +@API public interface Dir : AutoCloseable, ProxyIterable, ReadOnlyProxyObject { + /** + * Public access to the original path used to create this instance. + */ + @get:Polyglot public val path: String + + /** + * Close the underlying resource for this directory instance, and then invoke the provided [callback]. + * + * @param callback Callback to invoke after closing + */ + @Polyglot public fun close(callback: Value) + + /** + * Close the underlying resource for this directory instance, and then invoke the provided [callback]. + * + * @param callback Callback to invoke after closing + */ + public fun close(callback: () -> Unit) + + /** + * Synchronously close underlying file resources. + */ + @Polyglot public fun closeSync() + + /** + * Asynchronously read the next directory entry via `readdir(3)` as an instance of [Dirent]. + */ + @Polyglot public fun read(callback: Value) + + /** + * Synchronously read the next directory entry as an instance of [Dirent]. + */ + @Polyglot public fun readSync(): Dirent? } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dirent.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dirent.kt index dcc7833e50..3ff8352426 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dirent.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/Dirent.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Elide Technologies, Inc. + * Copyright (c) 2024-2025 Elide Technologies, Inc. * * Licensed under the MIT license (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at @@ -13,9 +13,72 @@ package elide.runtime.intrinsics.js.node.fs import elide.annotations.API +import elide.runtime.interop.ReadOnlyProxyObject +import elide.vm.annotations.Polyglot /** + * ## Node Filesystem: Directory Entry * + * Corresponds to the `fs.Dirent` class structure provided by Node's built-in Filesystem API; such structures yield + * during streaming of directory contents. Each instance describes the name of a streamed directory entry, plus some + * boolean flags indicating the type of entry. + * + * [Node.js API](https://nodejs.org/docs/latest/api/fs.html#class-fsdirent) + * + * @see Dir Directory streaming */ -@API public interface Dirent { +@API public interface Dirent : ReadOnlyProxyObject { + /** + * Provides the name of this entry; this corresponds to the file-name if the entry represents a file, or the directory + * name if the entry represents a directory. + */ + @get:Polyglot public val name: String + + /** + * Parent path which contains this directory entry. + */ + @get:Polyglot public val parentPath: String + + /** + * Indicates whether this directory entry represents a directory (a nested directory). + */ + @get:Polyglot public val isDirectory: Boolean + + /** + * Indicates whether this directory entry represents a file. + */ + @get:Polyglot public val isFile: Boolean + + /** + * Indicates whether this directory entry represents a symbolic link. + */ + @get:Polyglot public val isSymbolicLink: Boolean + + /** + * Indicates whether this directory entry represents a block device. + * + * Note: This is always `false` on Elide. + */ + @get:Polyglot public val isBlockDevice: Boolean + + /** + * Indicates whether this directory entry represents a character device. + * + * Note: This is always `false` on Elide. + */ + @get:Polyglot public val isCharacterDevice: Boolean + + /** + * Indicates whether this directory entry represents a first-in-first-out queue. + * + * Note: This is always `false` on Elide. + */ + @get:Polyglot public val isFIFO: Boolean + + /** + * Indicates whether this directory entry represents a socket. + * + * Note: This is always `false` on Elide. + */ + @get:Polyglot public val isSocket: Boolean } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/OpenDirOptions.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/OpenDirOptions.kt new file mode 100644 index 0000000000..a9fe667566 --- /dev/null +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/fs/OpenDirOptions.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * 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 elide.runtime.intrinsics.js.node.fs + +import org.graalvm.polyglot.Value +import elide.runtime.gvm.js.JsError +import elide.runtime.intrinsics.js.err.AbstractJsException + +/** + * ## Options: `fs.opendir` + * + * Describes the options which can be provided to an open-directory operation. + */ +public data class OpenDirOptions( + /** + * The encoding to use for paths; defaults to `utf-8`. + */ + val encoding: StringOrBuffer? = null, + + /** + * Number of directory entries that are buffered internally when reading from the directory. Higher values lead to + * better performance but higher memory usage. Default: `32`. + */ + val bufferSize: Int? = null, + + /** + * Whether to operate recursively when listing directory contents. + */ + val recursive: Boolean = false, +) { + public companion object { + /** Default open-dir options. */ + public val DEFAULTS: OpenDirOptions = OpenDirOptions() + + /** + * Create open-dir-options from a guest object or map. + * + * @param obj Guest object. + * @return Open-dir options. + */ + @JvmStatic public fun from(obj: Value): OpenDirOptions? = when { + obj.isNull -> null + obj.isString -> OpenDirOptions(obj.asString()) + + obj.hasMembers() -> OpenDirOptions( + encoding = obj.getMember("encoding")?.asString(), + bufferSize = obj.getMember("bufferSize")?.asInt(), + recursive = obj.getMember("recursive")?.asBoolean() == true, + ) + + else -> throw JsError.typeError("Cannot use '$obj' as open-dir-options") + } + } +} + +/** + * ## Callback: `fs.opendir` + * + * Describes the callback function shape which is provided to the `opendir` operation. + */ +public typealias OpenDirCallback = (err: AbstractJsException?, dir: Dir?) -> Unit diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/Directory.kt b/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/Directory.kt new file mode 100644 index 0000000000..de32b22b57 --- /dev/null +++ b/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/Directory.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * 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 elide.runtime.node.fs + +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyExecutable +import org.graalvm.polyglot.proxy.ProxyInstantiable +import org.graalvm.polyglot.proxy.ProxyIterator +import java.io.File +import java.nio.file.DirectoryStream +import java.nio.file.Files +import java.nio.file.Path +import kotlinx.atomicfu.atomic +import elide.runtime.gvm.js.JsError +import elide.runtime.intrinsics.js.node.fs.Dir +import elide.runtime.intrinsics.js.node.fs.Dirent +import elide.vm.annotations.Polyglot + +private const val DIR_PROP_PATH = "path" +private const val DIR_METHOD_CLOSE = "close" +private const val DIR_METHOD_READ = "read" +private const val DIR_METHOD_READ_SYNC = "readSync" + +private val dirPropsAndMethods = arrayOf( + DIR_PROP_PATH, + DIR_METHOD_CLOSE, + DIR_METHOD_READ, + DIR_METHOD_READ_SYNC, +) + +/** + * ## Node Filesystem: Directory + * + * Implements a wrapping class/object for a directory instance; can iterate over the entries for the directory, as well + * as provide basic information about entries. + */ +public class Directory private constructor ( + file: File, + private val originalPath: String, + private val walker: DirectoryStream, +): Dir { + public companion object Factory: ProxyInstantiable { + /** + * Create a new [Directory] handle from the provided [file] and directory [walker]. + * + * @param file File to wrap. + * @param walker Directory walker. + * @return Directory instance. + */ + @JvmStatic public fun wrap(file: File, walker: DirectoryStream): Directory { + return Directory(file, file.path, walker) + } + + /** + * Create a new [Directory] handle from the provided [file]. + * + * @param file File to wrap. + * @param path Path to the file. + * @return Directory instance. + */ + @JvmStatic public fun of(file: File, path: Path): Directory { + return wrap(file, Files.newDirectoryStream(path)) + } + + override fun newInstance(vararg arguments: Value?): Any? { + TODO("Not yet implemented") + } + } + + private val handle = atomic(file) + private val closed = atomic(false) + private val closeCallback = atomic<(() -> Unit)?>(null) + private val activeIter = atomic>(walker.iterator()) + + private inline fun withNotClosed(cbk: () -> R): R { + require(!closed.value) { "Cannot operate on `Dir`: closed" } + return cbk.invoke() + } + + private fun iterateNext(): Path? { + val iter = activeIter.value + return if (iter.hasNext()) iter.next() else null + } + + override fun getMemberKeys(): Array = dirPropsAndMethods + + override fun getIterator(): ProxyIterator = walker.iterator().let { iter -> + object: ProxyIterator { + override fun hasNext(): Boolean = withNotClosed { + iter.hasNext() + } + + override fun getNext(): Dirent? = withNotClosed { + iter.next()?.let { DirectoryEntry.forPath(it) } + } + } + } + + override fun getMember(key: String): Any? = when (key) { + DIR_PROP_PATH -> path + + DIR_METHOD_CLOSE -> ProxyExecutable { + when (it.size) { + 0 -> close() + else -> close(it.first()) + } + } + + DIR_METHOD_READ -> ProxyExecutable { + val first = it.firstOrNull() ?: throw JsError.typeError("Must provide a callback to `read()`") + read(first) + } + + DIR_METHOD_READ_SYNC -> ProxyExecutable { + readSync() + } + + else -> null + } + + @get:Polyglot override val path: String get() = originalPath + + @Polyglot override fun close() { + closed.value = true + handle.value = null + closeCallback.value?.invoke() + closeCallback.value = null + } + + override fun close(callback: () -> Unit): Unit = withNotClosed { + closeCallback.value = callback + close() + } + + @Polyglot override fun close(callback: Value): Unit = withNotClosed { + close { + callback.executeVoid() + } + } + + @Polyglot override fun closeSync(): Unit = withNotClosed { + close() + } + + @Polyglot override fun read(callback: Value): Unit = withNotClosed { + require(callback.canExecute()) { "Callback passed to `read()` must be executable" } + + when (val next = iterateNext()) { + null -> callback.executeVoid() + else -> callback.execute(DirectoryEntry.forPath(next)) + } + } + + @Polyglot override fun readSync(): Dirent? = withNotClosed { + iterateNext()?.let { + DirectoryEntry.forPath(it) + } + } +} diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/DirectoryEntry.kt b/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/DirectoryEntry.kt new file mode 100644 index 0000000000..130104b30e --- /dev/null +++ b/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/DirectoryEntry.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * 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 elide.runtime.node.fs + +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyInstantiable +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.name +import elide.runtime.gvm.js.JsError +import elide.runtime.intrinsics.js.node.fs.Dirent + +// Properties and methods present on `Dirent` instances. +private const val DIRENT_PROP_NAME = "name" +private const val DIRENT_PROP_ISBLOCKDEVICE = "isBlockDevice" +private const val DIRENT_PROP_ISCHARACTERDEVICE = "isCharacterDevice" +private const val DIRENT_PROP_ISDIRECTORY = "isDirectory" +private const val DIRENT_PROP_ISFIFO = "isFIFO" +private const val DIRENT_PROP_ISFILE = "isFile" +private const val DIRENT_PROP_ISSOCKET = "isSocket" +private const val DIRENT_PROP_ISSYMBOLICLINK = "isSymbolicLink" +private const val DIRENT_PROP_PARENTPATH = "parentPath" + +// All methods and properties. +private val direntPropsAndMethods = arrayOf( + DIRENT_PROP_NAME, + DIRENT_PROP_ISBLOCKDEVICE, + DIRENT_PROP_ISCHARACTERDEVICE, + DIRENT_PROP_ISDIRECTORY, + DIRENT_PROP_ISFIFO, + DIRENT_PROP_ISFILE, + DIRENT_PROP_ISSOCKET, + DIRENT_PROP_ISSYMBOLICLINK, + DIRENT_PROP_PARENTPATH, +) + +/** + * ## Node Filesystem: Directory Entry Implementations + * + * Implements a sealed hierarchy of types, each of which ultimately behaves as a [Dirent] (directory entry) instance. + * + * Such instances hold information about a given entry in a directory's listing; the instance holds things like the name + * of the entry, and various booleans indicating what kind of entry it is. + * + * Directory entry instances are simple and behave like simple objects. They are not writable and do not expose any + * methods. For a richer interaction with a directory, specifically, see [Directory]. + * + * @see Directory directory listings + */ +public sealed interface DirectoryEntry : Dirent { + /** Factory methods for obtaining [Dirent] instances. */ + public companion object Factory: ProxyInstantiable { + /** + * Create a directory entry backed by a [File] instance. + * + * @param file File to create an entry from. + * @return [FileEntry] instance. + */ + @JvmStatic public fun forFile(file: File): FileEntry = FileEntry.of(file) + + /** + * Create a directory entry backed by a [Path] instance. + * + * @param path Path to create an entry from. + * @return [PathEntry] instance. + */ + @JvmStatic public fun forPath(path: Path): PathEntry = PathEntry.of(path) + + override fun newInstance(vararg arguments: Value?): Any? { + if (arguments.isEmpty() || arguments.first()?.isNull != false) { + throw JsError.typeError("Must provide argument to `Dirent` constructor (a file handle)") + } + TODO("Not yet implemented: Dirent constructor from guest") + } + } + + override fun getMemberKeys(): Array = direntPropsAndMethods + + override fun getMember(key: String): Any? = when (key) { + DIRENT_PROP_NAME -> name + DIRENT_PROP_ISBLOCKDEVICE -> isBlockDevice + DIRENT_PROP_ISCHARACTERDEVICE -> isCharacterDevice + DIRENT_PROP_ISDIRECTORY -> isDirectory + DIRENT_PROP_ISFIFO -> isFIFO + DIRENT_PROP_ISFILE -> isFile + DIRENT_PROP_ISSOCKET -> isSocket + DIRENT_PROP_ISSYMBOLICLINK -> isSymbolicLink + DIRENT_PROP_PARENTPATH -> parentPath + else -> null + } + + /** + * Implements a [DirectoryEntry] for a [File] instance. + */ + public class FileEntry private constructor (public val file: File) : DirectoryEntry { + private val fileAsPath by lazy { file.toPath() } + override val name: String get() = file.name + override val parentPath: String get() = file.parent + override val isFile: Boolean get() = file.isFile + override val isDirectory: Boolean get() = file.isDirectory + override val isSymbolicLink: Boolean get() = Files.isSymbolicLink(fileAsPath) + + // Note: Not supported from here on down. + override val isSocket: Boolean get() = false + override val isBlockDevice: Boolean get() = false + override val isCharacterDevice: Boolean get() = false + override val isFIFO: Boolean get() = false + + internal companion object { + // Create a file entry from a file. + @JvmStatic fun of(file: File): FileEntry = FileEntry(file) + } + } + + /** + * Implements a [DirectoryEntry] for a [Path] instance. + */ + public class PathEntry private constructor (public val path: Path) : DirectoryEntry { + override val name: String get() = path.name + override val parentPath: String get() = path.parent.toString() + override val isFile: Boolean get() = Files.isRegularFile(path) + override val isDirectory: Boolean get() = Files.isDirectory(path) + override val isSymbolicLink: Boolean get() = Files.isSymbolicLink(path) + + // Note: Not supported from here on down. + override val isSocket: Boolean get() = false + override val isBlockDevice: Boolean get() = false + override val isCharacterDevice: Boolean get() = false + override val isFIFO: Boolean get() = false + + internal companion object { + // Create a path entry from a path. + @JvmStatic fun of(path: Path): PathEntry = PathEntry(path) + } + } +} + +/** + * Shorthand to create a [DirectoryEntry] from a [File] instance. + * + * @receiver File to create from. + * @return Directory entry for the file. + */ +public fun File.asDirectoryEntry(): DirectoryEntry.FileEntry = DirectoryEntry.forFile(this) + +/** + * Shorthand to create a [DirectoryEntry] from a [Path] instance. + * + * @receiver Path to create from. + * @return Directory entry for the file. + */ +public fun Path.asDirectoryEntry(): DirectoryEntry.PathEntry = DirectoryEntry.forPath(this) diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/NodeFilesystem.kt b/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/NodeFilesystem.kt index 6fad908c0e..0259c09406 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/NodeFilesystem.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/node/fs/NodeFilesystem.kt @@ -14,6 +14,10 @@ package elide.runtime.node.fs +import com.oracle.truffle.api.strings.TruffleString +import com.oracle.truffle.js.lang.JavaScriptLanguage +import com.oracle.truffle.js.runtime.JSErrorType +import com.oracle.truffle.js.runtime.builtins.JSError import io.micronaut.core.annotation.ReflectiveAccess import org.graalvm.polyglot.Value import org.graalvm.polyglot.io.ByteSequence @@ -78,6 +82,8 @@ internal const val FS_F_MKDIR = "mkdir" private const val FS_F_MKDIR_SYNC = "mkdirSync" internal const val FS_F_COPY_FILE = "copyFile" private const val FS_F_COPY_FILE_SYNC = "copyFileSync" +internal const val FS_F_OPEN_DIR = "opendir" +private const val FS_F_OPEN_DIR_SYNC = "opendirSync" private const val FS_P_CONSTANTS = "constants" private const val FS_ENCODING_UTF8 = "utf8" private const val FS_ENCODING_UTF8_ALT = "utf-8" @@ -97,6 +103,8 @@ private const val FS_C_X_OK = "X_OK" private const val FS_C_COPYFILE_EXCL = "COPYFILE_EXCL" private const val FS_C_COPYFILE_FICLONE = "COPYFILE_FICLONE" private const val FS_C_COPYFILE_FICLONE_FORCE = "COPYFILE_FICLONE_FORCE" +private const val FS_CLS_DIR = "Dir" +private const val FS_CLS_DIRENT = "Dirent" // Listener bean for VFS init. @Singleton @Eager public class VfsInitializerListener : VfsListener, Supplier { @@ -150,9 +158,6 @@ internal fun resolveEncodingString(encoding: String): Charset = when (encoding.t // Implements the Node built-in filesystem modules. internal object NodeFilesystem { - internal const val SYMBOL_STD: String = "node_${NodeModuleName.FS}" - internal const val SYMBOL_PROMISES: String = "node_fs_promises" - /** @return Instance of the `fs` module. */ fun createStd( exec: GuestExecutor, @@ -487,6 +492,15 @@ internal abstract class FilesystemBase( return op() } + protected inline fun checkForDirectoryRead(path: java.nio.file.Path, op: () -> T): T { + when { + !Files.exists(path) -> JsError.error("EEXIST: directory does not exist, open '${path}'") + !Files.isReadable(path) -> JsError.error("EACCES: permission denied, open '${path}'") + !Files.isDirectory(path) -> JsError.error("ENOTDIR: not a directory, open '${path}'") + } + return op() + } + protected fun createDirectory(path: java.nio.file.Path, recursive: Boolean = false, op: ((Path) -> Unit)? = null) { try { if (recursive) Files.createDirectories(path) else Files.createDirectory(path) @@ -530,6 +544,8 @@ internal abstract class FilesystemBase( ReadOnlyProxyObject, FilesystemBase(path, exec, fs) { override fun getMemberKeys(): Array = arrayOf( + FS_CLS_DIR, + FS_CLS_DIRENT, FS_F_ACCESS, FS_F_ACCESS_SYNC, FS_F_EXISTS, @@ -538,6 +554,8 @@ internal abstract class FilesystemBase( FS_F_READ_FILE_SYNC, FS_F_WRITE_FILE, FS_F_WRITE_FILE_SYNC, + FS_F_OPEN_DIR, + FS_F_OPEN_DIR_SYNC, FS_F_MKDIR, FS_F_MKDIR_SYNC, FS_F_COPY_FILE, @@ -546,6 +564,9 @@ internal abstract class FilesystemBase( ) override fun getMember(key: String?): Any? = when (key) { + FS_CLS_DIR -> Directory.Factory + FS_CLS_DIRENT -> DirectoryEntry.Factory + FS_F_ACCESS -> ProxyExecutable { when (it.size) { 2 -> access(it.first(), it[1]) @@ -643,6 +664,22 @@ internal abstract class FilesystemBase( } } + FS_F_OPEN_DIR -> ProxyExecutable { + when (it.size) { + 0, 1 -> throw JsError.typeError("Not enough arguments for `opendir`") + 2 -> opendir(it.first(), null, it.last()) + else -> opendir(it.first(), it[1], it.last()) + } + } + + FS_F_OPEN_DIR_SYNC -> ProxyExecutable { + when (it.size) { + 0 -> throw JsError.typeError("Not enough arguments for `opendir`") + 1 -> opendirSync(it.first(), null) + else -> opendirSync(it.first(), it.last()) + } + } + FS_P_CONSTANTS -> FilesystemConstants else -> null @@ -732,8 +769,8 @@ internal abstract class FilesystemBase( if (err != null) callback(err, null) else callback(null, data) } else if (optionsIsCallback) { - if (err != null) options!!.executeVoid(err, null) - else options!!.executeVoid(null, data) + if (err != null) options.executeVoid(err, null) + else options.executeVoid(null, data) } else { throw JsError.typeError("Callback for `readFile` must be a function") } @@ -943,6 +980,56 @@ internal abstract class FilesystemBase( if (it != null) throw it } } + + override fun opendir(path: Path, options: OpenDirOptions, callback: OpenDirCallback) { + withExec { + val jpath = path.toJavaPath() + try { + checkForDirectoryRead(jpath) { + val file = jpath.toFile() + val walker = Files.newDirectoryStream(jpath) + val dir = Directory.wrap(file, walker) + callback.invoke(null, dir) + } + } catch (err: Throwable) { + if (err is AbstractJsException) { + callback.invoke(err, null) + } else { + throw JsError.error("Failed to open directory", err) + } + } + } + } + + @Polyglot override fun opendir(path: Value, options: Value?, callback: Value) { + val opts = options?.let { OpenDirOptions.from(it) } ?: OpenDirOptions.DEFAULTS + opendir(resolvePath("opendir", path), opts) { err: AbstractJsException?, value: Dir? -> + if (err != null) { + val jsErr = JSError.create( + JSErrorType.Error, + JavaScriptLanguage.getCurrentJSRealm(), + TruffleString.fromJavaStringUncached(err.toString(), TruffleString.Encoding.UTF_8), + ) + callback.executeVoid(jsErr) + } else if (value != null) { + callback.executeVoid(value) + } + } + } + + override fun opendirSync(path: Path, options: OpenDirOptions): Directory { + val jpath = path.toJavaPath() + return checkForDirectoryRead(jpath) { + val file = jpath.toFile() + val walker = Files.newDirectoryStream(jpath) + Directory.wrap(file, walker) + } + } + + @Polyglot override fun opendirSync(path: Value, options: Value?): Directory { + val opts = options?.let { OpenDirOptions.from(it) } ?: OpenDirOptions.DEFAULTS + return opendirSync(resolvePath("opendirSync", path), opts) + } } // Implements the Node `fs/promises` module. @@ -956,6 +1043,7 @@ internal abstract class FilesystemBase( FS_F_WRITE_FILE, FS_F_MKDIR, FS_F_COPY_FILE, + FS_F_OPEN_DIR, FS_P_CONSTANTS, ) @@ -1000,6 +1088,14 @@ internal abstract class FilesystemBase( } } + FS_F_OPEN_DIR -> ProxyExecutable { + when (it.size) { + 0 -> throw JsError.typeError("Not enough arguments for `fs.opendir`") + 1 -> opendir(it.first()) + else -> opendir(it.first(), it.last()) + } + } + FS_P_CONSTANTS -> FilesystemConstants else -> null @@ -1084,4 +1180,16 @@ internal abstract class FilesystemBase( nioPath }.toString() } + + override fun opendir(path: Path, options: OpenDirOptions?): JsPromise = withExec { + val jpath = path.toJavaPath() + checkForDirectoryRead(jpath) { + val file = jpath.toFile() + val walker = Files.newDirectoryStream(jpath) + Directory.wrap(file, walker) + } + } + + @Polyglot override fun opendir(path: Value, options: Value?): JsPromise = + opendir(resolvePath("opendir", path), options?.let { OpenDirOptions.from(it) }) } diff --git a/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsPromisesTest.kt b/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsPromisesTest.kt index 53f3674866..6e83cfa653 100644 --- a/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsPromisesTest.kt +++ b/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsPromisesTest.kt @@ -16,13 +16,17 @@ package elide.runtime.node import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.util.LinkedList import jakarta.inject.Provider +import kotlin.io.path.absolutePathString import kotlin.test.* import elide.annotations.Inject import elide.runtime.exec.GuestExecutorProvider import elide.runtime.gvm.vfs.EmbeddedGuestVFS import elide.runtime.intrinsics.js.asDeferred +import elide.runtime.intrinsics.js.node.WritableFilesystemAPI import elide.runtime.intrinsics.js.node.WritableFilesystemPromiseAPI +import elide.runtime.intrinsics.js.node.fs.Dirent import elide.runtime.intrinsics.js.node.fs.ReadFileOptions import elide.runtime.intrinsics.js.node.path.Path import elide.runtime.node.fs.NodeFilesystemModule @@ -66,6 +70,7 @@ import elide.testing.annotations.TestCase yield("mkdir") yield("mkdtemp") yield("open") + yield("opendir") yield("readdir") yield("readFile") yield("readlink") @@ -232,4 +237,27 @@ import elide.testing.annotations.TestCase assertEquals("Hello, world!", Files.readString(destPath)) } } + + @Test fun `opendir() with valid directory with contents`() = withTemp { tmp -> + filesystem.providePromises().let { fs -> + val samplePath = tmp.resolve("opendir-test-with-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(Path.from(samplePath)).asDeferred().await().also { + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + + val sampleFile = samplePath.resolve("sample-file.txt") + fs.writeFile(Path.from(sampleFile), "abc123").asDeferred().await() + assertTrue(Files.exists(sampleFile)) + assertTrue(Files.isRegularFile(sampleFile)) + + val allContents = LinkedList() + val dir = fs.opendir(Path.from(samplePath)).asDeferred().await() + allContents.add(assertNotNull(dir.readSync())) + assertTrue(allContents.isNotEmpty()) + assertEquals(1, allContents.size) + } + } } diff --git a/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsTest.kt b/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsTest.kt index acc179f7b9..02054d1ede 100644 --- a/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsTest.kt +++ b/packages/graalvm/src/test/kotlin/elide/runtime/node/NodeFsTest.kt @@ -23,12 +23,15 @@ import org.junit.jupiter.api.assertThrows import java.nio.charset.StandardCharsets import java.nio.file.AccessMode.* import java.nio.file.Files +import java.nio.file.Paths import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE +import java.util.LinkedList import java.util.concurrent.atomic.AtomicReference import java.util.function.Function import java.util.stream.Stream import jakarta.inject.Provider +import kotlin.io.path.absolutePathString import kotlin.streams.asStream import kotlin.test.* import elide.annotations.Inject @@ -39,11 +42,15 @@ import elide.runtime.intrinsics.js.err.Error import elide.runtime.intrinsics.js.err.TypeError import elide.runtime.intrinsics.js.err.ValueError import elide.runtime.intrinsics.js.node.WritableFilesystemAPI +import elide.runtime.intrinsics.js.node.fs.Dirent import elide.runtime.intrinsics.js.node.fs.ReadFileOptions import elide.runtime.intrinsics.js.node.fs.WriteFileOptions +import elide.runtime.node.fs.Directory +import elide.runtime.node.fs.DirectoryEntry import elide.runtime.node.fs.FilesystemConstants import elide.runtime.node.fs.NodeFilesystemModule import elide.runtime.node.fs.VfsInitializerListener +import elide.runtime.node.fs.asDirectoryEntry import elide.runtime.node.fs.resolveEncodingString import elide.runtime.node.path.NodePathsModule import elide.testing.annotations.TestCase @@ -1259,4 +1266,315 @@ import elide.runtime.intrinsics.js.node.path.Path as NodePath }.doesNotFail() } } + + @Test fun testDirectoryHandle() { + val somePath = Paths.get("/tmp") + val getDir = { Directory.of(somePath.toFile(), somePath) } + val dir = getDir() + assertNotNull(dir) + assertNotNull(dir.memberKeys) + dir.memberKeys.forEach { + assertNotNull(dir.getMember(it), "expected member '$it' on '$dir'") + } + assertDoesNotThrow { + dir.use { + // blah blah + } + } + } + + @Test fun testDirectoryHandleClosed() { + val somePath = Paths.get("/tmp") + val getDir = { Directory.of(somePath.toFile(), somePath) } + val dir = getDir() + assertNotNull(dir) + // using a closed handle should fail + getDir().let { dir -> + dir.close() + assertThrows { + dir.readSync() + } + } + } + + @Test fun testDirectoryHandleCloseCallback() { + val somePath = Paths.get("/tmp") + val getDir = { Directory.of(somePath.toFile(), somePath) } + val dir = getDir() + assertNotNull(dir) + // closing callback should be called + getDir().let { dir -> + var called = false + dir.close { + called = true + } + assertTrue(called) + } + } + + @Test fun testDirectoryEntryFactories() { + val somePath = Paths.get("/tmp") + assertNotNull(DirectoryEntry.forPath(somePath)) + assertNotNull(somePath.asDirectoryEntry()) + assertNotNull(DirectoryEntry.forFile(somePath.toFile())) + assertNotNull(somePath.toFile().asDirectoryEntry()) + val entries = listOf( + assertNotNull(DirectoryEntry.forPath(somePath)), + assertNotNull(DirectoryEntry.forFile(somePath.toFile())) + ) + entries.forEach { + assertNotNull(it.memberKeys) + it.memberKeys.forEach { key -> + assertNotNull(it.getMember(key), "expected member '$key' but was null") + } + } + } + + @Test fun `opendir() host-side test`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendir-host-test-with-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(NodePath.from(samplePath)) { + assertNull(it) + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + + val sampleFile = samplePath.resolve("sample-file.txt") + fs.writeFileSync(NodePath.from(sampleFile), "abc123") + assertTrue(Files.exists(sampleFile)) + assertTrue(Files.isRegularFile(sampleFile)) + + val allContents = LinkedList() + fs.opendir(NodePath.from(samplePath)) { err, value -> + assertNull(err) + assertNotNull(value) + + var next = value.readSync() + while (next != null) { + allContents.add(next) + next = value.readSync() + } + } + assertTrue(allContents.isNotEmpty()) + assertEquals(1, allContents.size) + val first = allContents.first() + assertEquals("sample-file.txt", first.name) + assertNotNull(first.parentPath) + assertFalse(first.isDirectory) + assertTrue(first.isFile) + assertFalse(first.isSymbolicLink) + assertFalse(first.isBlockDevice) + assertFalse(first.isCharacterDevice) + assertFalse(first.isFIFO) + assertFalse(first.isSocket) + val asFileEntry = DirectoryEntry.forFile((first as DirectoryEntry.PathEntry).path.toFile()) + assertEquals("sample-file.txt", asFileEntry.name) + assertNotNull(asFileEntry.parentPath) + assertFalse(asFileEntry.isDirectory) + assertTrue(asFileEntry.isFile) + assertFalse(asFileEntry.isSymbolicLink) + assertFalse(asFileEntry.isBlockDevice) + assertFalse(asFileEntry.isCharacterDevice) + assertFalse(asFileEntry.isFIFO) + assertFalse(asFileEntry.isSocket) + } + } + + @Test fun `opendir() host-side test with no contents`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendir-host-test-no-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(NodePath.from(samplePath)) { + assertNull(it) + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + + val allContents = LinkedList() + fs.opendir(NodePath.from(samplePath)) { err, value -> + assertNull(err) + assertNotNull(value) + + var next = value.readSync() + while (next != null) { + allContents.add(next) + next = value.readSync() + } + } + assertTrue(allContents.isEmpty()) + } + } + + @Test fun `opendir() with valid directory with contents`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendir-test-with-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(NodePath.from(samplePath)) { + assertNull(it) + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + assertTrue(Files.isDirectory(samplePath)) + + val sampleFile = samplePath.resolve("sample-file.txt") + fs.writeFileSync(NodePath.from(sampleFile), "abc123") + assertTrue(Files.exists(sampleFile)) + assertTrue(Files.isRegularFile(sampleFile)) + val absoluteDirPath = samplePath.absolutePathString() + + executeGuest { + // language=javascript + """ + const { opendir } = require("node:fs"); + test(opendir).isNotNull(); + let didError = false; + let didSeeContents = false; + const entries = []; + + opendir("$absoluteDirPath", (dir) => { + if (dir instanceof Error) { + didError = true; + } else { + const entry = dir.readSync(); + didSeeContents = true; + entries.push(entry); + } + }); + test(didError).shouldBeFalse(); + test(didSeeContents).shouldBeTrue(); + test(entries[0].name).isEqualTo("sample-file.txt"); + test(entries[0].isFile).shouldBeTrue(); + test(entries[0].isDirectory).shouldBeFalse(); + test(entries[0].isSymbolicLink).shouldBeFalse(); + """ + }.doesNotFail() + } + } + + @Test fun `opendir() with missing directory`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendir-test-missing").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist") + val absoluteDirPath = samplePath.absolutePathString() + + executeGuest { + // language=javascript + """ + const { opendir } = require("node:fs"); + test(opendir).isNotNull(); + let didError = false; + opendir("$absoluteDirPath", (dir) => { + didError = dir instanceof Error; + }); + test(didError).shouldBeTrue(); + """ + }.doesNotFail() + } + } + + @Test fun `opendirSync() host-side test`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendirsync-host-test-with-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(NodePath.from(samplePath)) { + assertNull(it) + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + + val sampleFile = samplePath.resolve("sample-file.txt") + fs.writeFileSync(NodePath.from(sampleFile), "abc123") + assertTrue(Files.exists(sampleFile)) + assertTrue(Files.isRegularFile(sampleFile)) + + val allContents = LinkedList() + val dir = fs.opendirSync(NodePath.from(samplePath)) + assertNotNull(dir) + var next = dir.readSync() + while (next != null) { + allContents.add(next.name) + next = dir.readSync() + } + assertTrue(allContents.isNotEmpty()) + assertEquals(1, allContents.size) + } + } + + @Test fun `opendirSync() host-side test with no contents`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendirsync-host-test-no-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(NodePath.from(samplePath)) { + assertNull(it) + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + + val allContents = LinkedList() + val dir = fs.opendirSync(NodePath.from(samplePath)) + assertNotNull(dir) + var next = dir.readSync() + while (next != null) { + allContents.add(next.name) + next = dir.readSync() + } + assertTrue(allContents.isEmpty()) + } + } + + @Test fun `opendirSync() with valid directory with contents`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendirsync-test-with-contents").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist before creation") + assertIs(fs) + fs.mkdir(NodePath.from(samplePath)) { + assertNull(it) + assertTrue(Files.exists(samplePath), "directory should exist after creation") + } + assertTrue(Files.exists(samplePath)) + assertTrue(Files.isDirectory(samplePath)) + + val sampleFile = samplePath.resolve("sample-file.txt") + fs.writeFileSync(NodePath.from(sampleFile), "abc123") + assertTrue(Files.exists(sampleFile)) + assertTrue(Files.isRegularFile(sampleFile)) + val absoluteDirPath = samplePath.absolutePathString() + + executeGuest { + // language=javascript + """ + const { opendirSync } = require("node:fs"); + test(opendirSync).isNotNull(); + const entries = []; + const dir = opendirSync("$absoluteDirPath"); + const entry = dir.readSync(); + entries.push(entry.name); + test(entries[0]).isEqualTo("sample-file.txt"); + """ + }.doesNotFail() + } + } + + @Test fun `opendirSync() with missing directory`() = withTemp { tmp -> + filesystem.provideStd().let { fs -> + val samplePath = tmp.resolve("opendirsync-test-missing").toAbsolutePath() + assertFalse(Files.exists(samplePath), "directory should not exist") + val absoluteDirPath = samplePath.absolutePathString() + + executeGuest { + // language=javascript + """ + const { opendirSync } = require("node:fs"); + test(opendirSync).isNotNull(); + opendirSync("$absoluteDirPath"); + """ + }.fails() + } + } }