diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 03f9d8d204237a..30ebbc52489dff 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -178,6 +178,8 @@ deno_core::extension!(deno_node, ops::fs::op_node_statfs_sync, ops::fs::op_node_statfs, ops::fs::op_node_file_from_fd, + ops::fs::op_node_get_fd, + ops::fs::op_node_dup_fd, ops::winerror::op_node_sys_to_uv_error, ops::v8::op_v8_cached_data_version_tag, ops::v8::op_v8_get_heap_statistics, @@ -445,6 +447,7 @@ deno_core::extension!(deno_node, "internal/fs/streams.mjs", "internal/fs/utils.mjs", "internal/fs/handle.ts", + "internal/fs/fd_map.ts", "internal/hide_stack_frames.ts", "internal/http.ts", "internal/http2/util.ts", diff --git a/ext/node/ops/fs.rs b/ext/node/ops/fs.rs index 6e6675655839ed..863434c0999e18 100644 --- a/ext/node/ops/fs.rs +++ b/ext/node/ops/fs.rs @@ -574,3 +574,95 @@ pub fn op_node_file_from_fd( "op_node_file_from_fd is not supported on this platform", ))) } + +/// Create a file resource from a raw file descriptor by dup'ing it first. +/// This is safe for cross-worker use because the dup'd fd is independently +/// owned and can be closed without affecting the original. +#[cfg(unix)] +#[op2(fast)] +#[smi] +pub fn op_node_dup_fd( + state: &mut OpState, + #[smi] fd: i32, +) -> Result { + use std::fs::File as StdFile; + use std::os::unix::io::FromRawFd; + + if fd < 0 { + return Err(FsError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid file descriptor", + ))); + } + + // SAFETY: dup() creates a new fd pointing to the same open file description. + let new_fd = unsafe { libc::dup(fd) }; + if new_fd < 0 { + return Err(FsError::Io(std::io::Error::last_os_error())); + } + + // Clear O_NONBLOCK flag - the PTY fd might be in non-blocking mode, + // but StdFileResourceInner uses spawn_blocking for reads which expects + // blocking I/O. We need blocking reads so the read will wait for data. + // SAFETY: new_fd is valid, fcntl with F_GETFL/F_SETFL is safe + unsafe { + let flags = libc::fcntl(new_fd, libc::F_GETFL); + if flags >= 0 && (flags & libc::O_NONBLOCK) != 0 { + libc::fcntl(new_fd, libc::F_SETFL, flags & !libc::O_NONBLOCK); + } + } + + // SAFETY: new_fd is a valid fd we just created via dup(). + let std_file = unsafe { StdFile::from_raw_fd(new_fd) }; + let file: Rc = + Rc::new(deno_io::StdFileResourceInner::file(std_file, None)); + let rid = state + .resource_table + .add(FileResource::new(file, "fsFile".to_string())); + Ok(rid) +} + +#[cfg(not(unix))] +#[op2(fast)] +#[smi] +pub fn op_node_dup_fd( + _state: &mut OpState, + #[smi] _fd: i32, +) -> Result { + Err(FsError::Io(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "op_node_dup_fd is not supported on this platform", + ))) +} + +/// Retrieves the OS file descriptor for a given resource ID. +#[cfg(unix)] +#[op2(fast)] +pub fn op_node_get_fd( + state: &mut OpState, + #[smi] rid: ResourceId, +) -> Result { + use deno_core::ResourceHandle; + let handle = state.resource_table.get_handle(rid).map_err(|_| { + FsError::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Bad resource ID", + )) + })?; + match handle { + ResourceHandle::Fd(fd) => Ok(fd), + _ => Err(FsError::Io(std::io::Error::other( + "Resource is not a file descriptor", + ))), + } +} + +/// On non-Unix platforms, return the RID as-is for now. +#[cfg(not(unix))] +#[op2(fast)] +pub fn op_node_get_fd( + _state: &mut OpState, + #[smi] rid: ResourceId, +) -> Result { + Ok(rid as i32) +} diff --git a/ext/node/polyfills/_fs/_fs_close.ts b/ext/node/polyfills/_fs/_fs_close.ts index 616232357ecf7a..5fdb270d18a4ed 100644 --- a/ext/node/polyfills/_fs/_fs_close.ts +++ b/ext/node/polyfills/_fs/_fs_close.ts @@ -5,6 +5,7 @@ import { makeCallback, } from "ext:deno_node/_fs/_fs_common.ts"; import { getValidatedFd } from "ext:deno_node/internal/fs/utils.mjs"; +import { getRid, unregisterFd } from "ext:deno_node/internal/fs/fd_map.ts"; import { core, primordials } from "ext:core/mod.js"; const { @@ -29,9 +30,9 @@ export function close( setTimeout(() => { let error = null; try { - // TODO(@littledivy): Treat `fd` as real file descriptor. `rid` is an - // implementation detail and may change. - core.close(fd); + const rid = getRid(fd); + core.close(rid); + unregisterFd(fd); } catch (err) { error = ObjectPrototypeIsPrototypeOf(ErrorPrototype, err) ? err as Error @@ -43,7 +44,7 @@ export function close( export function closeSync(fd: number) { fd = getValidatedFd(fd); - // TODO(@littledivy): Treat `fd` as real file descriptor. `rid` is an - // implementation detail and may change. - core.close(fd); + const rid = getRid(fd); + core.close(rid); + unregisterFd(fd); } diff --git a/ext/node/polyfills/_fs/_fs_fchmod.ts b/ext/node/polyfills/_fs/_fs_fchmod.ts index 79be428cbada22..ae211d44d1b618 100644 --- a/ext/node/polyfills/_fs/_fs_fchmod.ts +++ b/ext/node/polyfills/_fs/_fs_fchmod.ts @@ -11,6 +11,7 @@ import { import { op_fs_fchmod_async, op_fs_fchmod_sync } from "ext:core/ops"; import { primordials } from "ext:core/mod.js"; import { promisify } from "ext:deno_node/internal/util.mjs"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; const { PromisePrototypeThen } = primordials; @@ -24,7 +25,7 @@ export function fchmod( callback = makeCallback(callback); PromisePrototypeThen( - op_fs_fchmod_async(fd, mode), + op_fs_fchmod_async(getRid(fd), mode), () => callback(null), callback, ); @@ -33,7 +34,7 @@ export function fchmod( export function fchmodSync(fd: number, mode: string | number) { validateInteger(fd, "fd", 0, 2147483647); - op_fs_fchmod_sync(fd, parseFileMode(mode, "mode")); + op_fs_fchmod_sync(getRid(fd), parseFileMode(mode, "mode")); } export const fchmodPromise = promisify(fchmod) as ( diff --git a/ext/node/polyfills/_fs/_fs_fchown.ts b/ext/node/polyfills/_fs/_fs_fchown.ts index 552aac22bd6b57..42319b60dbbcd4 100644 --- a/ext/node/polyfills/_fs/_fs_fchown.ts +++ b/ext/node/polyfills/_fs/_fs_fchown.ts @@ -11,6 +11,7 @@ import { promisify } from "ext:deno_node/internal/util.mjs"; import { primordials } from "ext:core/mod.js"; const { PromisePrototypeThen } = primordials; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; /** * Changes the owner and group of a file. @@ -27,7 +28,7 @@ export function fchown( callback = makeCallback(callback); PromisePrototypeThen( - op_fs_fchown_async(fd, uid, gid), + op_fs_fchown_async(getRid(fd), uid, gid), () => callback(null), callback, ); @@ -45,7 +46,7 @@ export function fchownSync( validateInteger(uid, "uid", -1, kMaxUserId); validateInteger(gid, "gid", -1, kMaxUserId); - op_fs_fchown_sync(fd, uid, gid); + op_fs_fchown_sync(getRid(fd), uid, gid); } export const fchownPromise = promisify(fchown) as ( diff --git a/ext/node/polyfills/_fs/_fs_fdatasync.ts b/ext/node/polyfills/_fs/_fs_fdatasync.ts index 71ea7a710ebbe9..4292dbad008a12 100644 --- a/ext/node/polyfills/_fs/_fs_fdatasync.ts +++ b/ext/node/polyfills/_fs/_fs_fdatasync.ts @@ -5,6 +5,7 @@ import { FsFile } from "ext:deno_fs/30_fs.js"; import { promisify } from "ext:deno_node/internal/util.mjs"; import { validateInt32 } from "ext:deno_node/internal/validators.mjs"; import { primordials } from "ext:core/mod.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; const { PromisePrototypeThen, SymbolFor } = primordials; @@ -14,7 +15,7 @@ export function fdatasync( ) { validateInt32(fd, "fd", 0); PromisePrototypeThen( - new FsFile(fd, SymbolFor("Deno.internal.FsFile")).syncData(), + new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).syncData(), () => callback(null), callback, ); @@ -22,7 +23,7 @@ export function fdatasync( export function fdatasyncSync(fd: number) { validateInt32(fd, "fd", 0); - new FsFile(fd, SymbolFor("Deno.internal.FsFile")).syncDataSync(); + new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).syncDataSync(); } export const fdatasyncPromise = promisify(fdatasync) as ( diff --git a/ext/node/polyfills/_fs/_fs_fstat.ts b/ext/node/polyfills/_fs/_fs_fstat.ts index 5dfd28aaeab1d2..cc30ccd0625570 100644 --- a/ext/node/polyfills/_fs/_fs_fstat.ts +++ b/ext/node/polyfills/_fs/_fs_fstat.ts @@ -13,6 +13,7 @@ import { } from "ext:deno_node/_fs/_fs_stat.ts"; import { FsFile } from "ext:deno_fs/30_fs.js"; import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; export function fstat(fd: number, callback: statCallback): void; export function fstat( @@ -42,7 +43,7 @@ export function fstat( if (!callback) throw new Error("No callback function supplied"); - new FsFile(fd, Symbol.for("Deno.internal.FsFile")).stat().then( + new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")).stat().then( (stat) => callback(null, CFISBIS(stat, options.bigint)), (err) => callback(denoErrorToNodeError(err, { syscall: "fstat" })), ); @@ -62,7 +63,7 @@ export function fstatSync( options?: statOptions, ): Stats | BigIntStats { try { - const origin = new FsFile(fd, Symbol.for("Deno.internal.FsFile")) + const origin = new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")) .statSync(); return CFISBIS(origin, options?.bigint || false); } catch (err) { diff --git a/ext/node/polyfills/_fs/_fs_fsync.ts b/ext/node/polyfills/_fs/_fs_fsync.ts index aa74ff1a16af01..355f84382bf48a 100644 --- a/ext/node/polyfills/_fs/_fs_fsync.ts +++ b/ext/node/polyfills/_fs/_fs_fsync.ts @@ -5,6 +5,7 @@ import { FsFile } from "ext:deno_fs/30_fs.js"; import { promisify } from "ext:deno_node/internal/util.mjs"; import { validateInt32 } from "ext:deno_node/internal/validators.mjs"; import { primordials } from "ext:core/mod.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; const { PromisePrototypeThen, SymbolFor } = primordials; @@ -14,7 +15,7 @@ export function fsync( ) { validateInt32(fd, "fd", 0); PromisePrototypeThen( - new FsFile(fd, SymbolFor("Deno.internal.FsFile")).sync(), + new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).sync(), () => callback(null), callback, ); @@ -22,7 +23,7 @@ export function fsync( export function fsyncSync(fd: number) { validateInt32(fd, "fd", 0); - new FsFile(fd, SymbolFor("Deno.internal.FsFile")).syncSync(); + new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).syncSync(); } export const fsyncPromise = promisify(fsync) as (fd: number) => Promise; diff --git a/ext/node/polyfills/_fs/_fs_ftruncate.ts b/ext/node/polyfills/_fs/_fs_ftruncate.ts index 3107093be5bc20..7e913a02df792f 100644 --- a/ext/node/polyfills/_fs/_fs_ftruncate.ts +++ b/ext/node/polyfills/_fs/_fs_ftruncate.ts @@ -4,6 +4,7 @@ import { primordials } from "ext:core/mod.js"; import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts"; import { FsFile } from "ext:deno_fs/30_fs.js"; import { promisify } from "ext:deno_node/internal/util.mjs"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; const { Error, @@ -26,14 +27,14 @@ export function ftruncate( if (!callback) throw new Error("No callback function supplied"); PromisePrototypeThen( - new FsFile(fd, SymbolFor("Deno.internal.FsFile")).truncate(len), + new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).truncate(len), () => callback(null), callback, ); } export function ftruncateSync(fd: number, len?: number) { - new FsFile(fd, SymbolFor("Deno.internal.FsFile")).truncateSync(len); + new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).truncateSync(len); } export const ftruncatePromise = promisify(ftruncate) as ( diff --git a/ext/node/polyfills/_fs/_fs_futimes.ts b/ext/node/polyfills/_fs/_fs_futimes.ts index 075b5e23709b65..828342b0cce6be 100644 --- a/ext/node/polyfills/_fs/_fs_futimes.ts +++ b/ext/node/polyfills/_fs/_fs_futimes.ts @@ -9,6 +9,7 @@ import { validateInteger } from "ext:deno_node/internal/validators.mjs"; import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; import { toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs"; import { promisify } from "ext:deno_node/internal/util.mjs"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; function getValidTime( time: number | string | Date, @@ -48,11 +49,11 @@ export function futimes( atime = getValidTime(atime, "atime"); mtime = getValidTime(mtime, "mtime"); - // TODO(@littledivy): Treat `fd` as real file descriptor. - new FsFile(fd, Symbol.for("Deno.internal.FsFile")).utime(atime, mtime).then( - () => callback(null), - callback, - ); + new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")).utime(atime, mtime) + .then( + () => callback(null), + callback, + ); } export function futimesSync( @@ -69,8 +70,10 @@ export function futimesSync( atime = getValidTime(atime, "atime"); mtime = getValidTime(mtime, "mtime"); - // TODO(@littledivy): Treat `fd` as real file descriptor. - new FsFile(fd, Symbol.for("Deno.internal.FsFile")).utimeSync(atime, mtime); + new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")).utimeSync( + atime, + mtime, + ); } export const futimesPromise = promisify(futimes) as ( diff --git a/ext/node/polyfills/_fs/_fs_open.ts b/ext/node/polyfills/_fs/_fs_open.ts index c533a7cc7c2048..1e9c5f6a8b79cf 100644 --- a/ext/node/polyfills/_fs/_fs_open.ts +++ b/ext/node/polyfills/_fs/_fs_open.ts @@ -10,7 +10,8 @@ import { import { FileHandle } from "ext:deno_node/internal/fs/handle.ts"; import type { Buffer } from "node:buffer"; import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; -import { op_node_open, op_node_open_sync } from "ext:core/ops"; +import { op_node_get_fd, op_node_open, op_node_open_sync } from "ext:core/ops"; +import { registerFd } from "ext:deno_node/internal/fs/fd_map.ts"; const { Promise, PromisePrototypeThen } = primordials; @@ -69,7 +70,11 @@ export function open( PromisePrototypeThen( op_node_open(path, flags, mode), - (rid: number) => callback(null, rid), + (rid: number) => { + const fd = op_node_get_fd(rid); + registerFd(fd, rid); + callback(null, fd); + }, (err: Error) => callback(denoErrorToNodeError(err, { syscall: "open", path })), ); @@ -109,7 +114,10 @@ export function openSync( const mode = parseFileMode(maybeMode, "mode", 0o666); try { - return op_node_open_sync(path, flags, mode); + const rid = op_node_open_sync(path, flags, mode); + const fd = op_node_get_fd(rid); + registerFd(fd, rid); + return fd; } catch (err) { throw denoErrorToNodeError(err as Error, { syscall: "open", path }); } diff --git a/ext/node/polyfills/_fs/_fs_read.ts b/ext/node/polyfills/_fs/_fs_read.ts index e983213e12dd5b..66c127da8cb9bd 100644 --- a/ext/node/polyfills/_fs/_fs_read.ts +++ b/ext/node/polyfills/_fs/_fs_read.ts @@ -21,6 +21,7 @@ import { import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; import { primordials } from "ext:core/mod.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; import { customPromisifyArgs, kEmptyObject, @@ -144,27 +145,28 @@ export function read( (async () => { try { + const rid = getRid(fd); let nread: number | null; if (typeof position === "number" && position >= 0) { const currentPosition = await op_fs_seek_async( - fd, + rid, 0, io.SeekMode.Current, ); // We use sync calls below to avoid being affected by others during // these calls. - op_fs_seek_sync(fd, position, io.SeekMode.Start); + op_fs_seek_sync(rid, position, io.SeekMode.Start); nread = io.readSync( - fd, + rid, arrayBufferViewToUint8Array(buffer).subarray( offset, offset + (length as number), ), ); - op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); + op_fs_seek_sync(rid, currentPosition, io.SeekMode.Start); } else { nread = await io.read( - fd, + rid, arrayBufferViewToUint8Array(buffer).subarray( offset, offset + (length as number), @@ -248,19 +250,20 @@ export function readSync( validatePosition(position, "position", length); } + const rid = getRid(fd); let currentPosition = 0; if (typeof position === "number" && position >= 0) { - currentPosition = op_fs_seek_sync(fd, 0, io.SeekMode.Current); - op_fs_seek_sync(fd, position, io.SeekMode.Start); + currentPosition = op_fs_seek_sync(rid, 0, io.SeekMode.Current); + op_fs_seek_sync(rid, position, io.SeekMode.Start); } const numberOfBytesRead = io.readSync( - fd, + rid, arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length!), ); if (typeof position === "number" && position >= 0) { - op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); + op_fs_seek_sync(rid, currentPosition, io.SeekMode.Start); } return numberOfBytesRead ?? 0; diff --git a/ext/node/polyfills/_fs/_fs_readFile.ts b/ext/node/polyfills/_fs/_fs_readFile.ts index 9cd5a480d7e757..29c42fc5afc3ef 100644 --- a/ext/node/polyfills/_fs/_fs_readFile.ts +++ b/ext/node/polyfills/_fs/_fs_readFile.ts @@ -14,6 +14,7 @@ import { readAllSync } from "ext:deno_io/12_io.js"; import { FileHandle } from "ext:deno_node/internal/fs/handle.ts"; import { Encodings } from "ext:deno_node/_utils.ts"; import { FsFile } from "ext:deno_fs/30_fs.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; import { AbortError, denoErrorToNodeError, @@ -202,7 +203,8 @@ export function readFile( if (typeof pathOrRid === "string") { p = readFileAsync(pathOrRid, options); } else { - const fsFile = new FsFile(pathOrRid, Symbol.for("Deno.internal.FsFile")); + const rid = getRid(pathOrRid); + const fsFile = new FsFile(rid, Symbol.for("Deno.internal.FsFile")); p = fsFileReadAll(fsFile, options); } @@ -253,7 +255,8 @@ export function readFileSync( let data; if (typeof path === "number") { - const fsFile = new FsFile(path, Symbol.for("Deno.internal.FsFile")); + const rid = getRid(path); + const fsFile = new FsFile(rid, Symbol.for("Deno.internal.FsFile")); data = readAllSync(fsFile); } else { // Validate/convert path to string (throws on invalid types) diff --git a/ext/node/polyfills/_fs/_fs_readv.ts b/ext/node/polyfills/_fs/_fs_readv.ts index aa08f89903faee..992a5bcea83168 100644 --- a/ext/node/polyfills/_fs/_fs_readv.ts +++ b/ext/node/polyfills/_fs/_fs_readv.ts @@ -14,6 +14,7 @@ import * as io from "ext:deno_io/12_io.js"; import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; import process from "node:process"; import { primordials } from "ext:core/mod.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; import { customPromisifyArgs } from "ext:deno_node/internal/util.mjs"; const { @@ -62,8 +63,9 @@ export function readv( buffers: readonly ArrayBufferView[], position: number | null, ) => { + const rid = getRid(fd); if (typeof position === "number") { - await op_fs_seek_async(fd, position, io.SeekMode.Start); + await op_fs_seek_async(rid, position, io.SeekMode.Start); } let readTotal = 0; @@ -71,7 +73,7 @@ export function readv( let bufIdx = 0; let buf = buffers[bufIdx]; while (bufIdx < buffers.length) { - const nread = await io.read(fd, buf); + const nread = await io.read(rid, buf); if (nread === null) { break; } @@ -117,9 +119,10 @@ export function readvSync( if (buffers.length === 0) { return 0; } + const rid = getRid(fd); if (typeof position === "number") { validateInteger(position, "position", 0); - op_fs_seek_sync(fd, position, io.SeekMode.Start); + op_fs_seek_sync(rid, position, io.SeekMode.Start); } let readTotal = 0; @@ -127,7 +130,7 @@ export function readvSync( let bufIdx = 0; let buf = buffers[bufIdx]; while (bufIdx < buffers.length) { - const nread = io.readSync(fd, buf); + const nread = io.readSync(rid, buf); if (nread === null) { break; } diff --git a/ext/node/polyfills/_fs/_fs_write.ts b/ext/node/polyfills/_fs/_fs_write.ts index b3146c59099282..c8e48cf96cbf23 100644 --- a/ext/node/polyfills/_fs/_fs_write.ts +++ b/ext/node/polyfills/_fs/_fs_write.ts @@ -20,6 +20,7 @@ import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; import { maybeCallback } from "ext:deno_node/_fs/_fs_common.ts"; import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; import { primordials } from "ext:core/mod.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; import { customPromisifyArgs, kEmptyObject, @@ -56,15 +57,16 @@ export function writeSync( length: number, position: number | null | undefined, ) => { + const rid = getRid(fd); buffer = arrayBufferViewToUint8Array(buffer); if (typeof position === "number" && position >= 0) { - op_fs_seek_sync(fd, position, io.SeekMode.Start); + op_fs_seek_sync(rid, position, io.SeekMode.Start); } let currentOffset = offset; const end = offset + length; while (currentOffset - offset < length) { currentOffset += io.writeSync( - fd, + rid, (buffer as Uint8Array).subarray(currentOffset, end), ); } @@ -121,15 +123,16 @@ export function write( length: number, position: number | null | undefined, ) => { + const rid = getRid(fd); buffer = arrayBufferViewToUint8Array(buffer); if (typeof position === "number" && position >= 0) { - await op_fs_seek_async(fd, position, io.SeekMode.Start); + await op_fs_seek_async(rid, position, io.SeekMode.Start); } let currentOffset = offset; const end = offset + length; while (currentOffset - offset < length) { currentOffset += await io.write( - fd, + rid, (buffer as Uint8Array).subarray(currentOffset, end), ); } diff --git a/ext/node/polyfills/_fs/_fs_writeFile.ts b/ext/node/polyfills/_fs/_fs_writeFile.ts index a9eda028588cba..da7bca9df028b1 100644 --- a/ext/node/polyfills/_fs/_fs_writeFile.ts +++ b/ext/node/polyfills/_fs/_fs_writeFile.ts @@ -29,6 +29,7 @@ import { primordials } from "ext:core/mod.js"; import type { BufferEncoding } from "ext:deno_node/_global.d.ts"; import { URLPrototype } from "ext:deno_web/00_url.js"; import { validateFunction } from "ext:deno_node/internal/validators.mjs"; +import { getRid as getFdRid } from "ext:deno_node/internal/fs/fd_map.ts"; type WriteFileSyncData = | string @@ -64,17 +65,18 @@ async function getRid( flag: string = "w", ): Promise { if (typeof pathOrRid === "number") { - return pathOrRid; + return getFdRid(pathOrRid); } const fileHandle = await openPromise(pathOrRid, flag); - return fileHandle.fd; + return getFdRid(fileHandle.fd); } function getRidSync(pathOrRid: string | number, flag: string = "w"): number { if (typeof pathOrRid === "number") { - return pathOrRid; + return getFdRid(pathOrRid); } - return openSync(pathOrRid, flag); + const fd = openSync(pathOrRid, flag); + return getFdRid(fd); } export function writeFile( diff --git a/ext/node/polyfills/_fs/_fs_writev.ts b/ext/node/polyfills/_fs/_fs_writev.ts index 1c5196410c7f91..bb70bcfb56c4e0 100644 --- a/ext/node/polyfills/_fs/_fs_writev.ts +++ b/ext/node/polyfills/_fs/_fs_writev.ts @@ -13,6 +13,7 @@ import { WriteVResult } from "ext:deno_node/internal/fs/handle.ts"; import { maybeCallback } from "ext:deno_node/_fs/_fs_common.ts"; import * as io from "ext:deno_io/12_io.js"; import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; export interface WriteVResult { bytesWritten: number; @@ -51,6 +52,7 @@ export function writev( callback?: writeVCallback, ): void { const innerWritev = async (fd, buffers, position) => { + const rid = getRid(fd); const chunks: Buffer[] = []; const offset = 0; for (let i = 0; i < buffers.length; i++) { @@ -61,12 +63,12 @@ export function writev( } } if (typeof position === "number") { - await op_fs_seek_async(fd, position, io.SeekMode.Start); + await op_fs_seek_async(rid, position, io.SeekMode.Start); } const buffer = Buffer.concat(chunks); let currentOffset = 0; while (currentOffset < buffer.byteLength) { - currentOffset += await io.writeSync(fd, buffer.subarray(currentOffset)); + currentOffset += await io.writeSync(rid, buffer.subarray(currentOffset)); } return currentOffset - offset; }; @@ -100,6 +102,7 @@ export function writevSync( position?: number | null, ): number { const innerWritev = (fd, buffers, position) => { + const rid = getRid(fd); const chunks: Buffer[] = []; const offset = 0; for (let i = 0; i < buffers.length; i++) { @@ -110,12 +113,12 @@ export function writevSync( } } if (typeof position === "number") { - op_fs_seek_sync(fd, position, io.SeekMode.Start); + op_fs_seek_sync(rid, position, io.SeekMode.Start); } const buffer = Buffer.concat(chunks); let currentOffset = 0; while (currentOffset < buffer.byteLength) { - currentOffset += io.writeSync(fd, buffer.subarray(currentOffset)); + currentOffset += io.writeSync(rid, buffer.subarray(currentOffset)); } return currentOffset - offset; }; diff --git a/ext/node/polyfills/internal/fs/fd_map.ts b/ext/node/polyfills/internal/fs/fd_map.ts new file mode 100644 index 00000000000000..8f5fa3114c67ef --- /dev/null +++ b/ext/node/polyfills/internal/fs/fd_map.ts @@ -0,0 +1,48 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { primordials } from "ext:core/mod.js"; +import { op_node_dup_fd } from "ext:core/ops"; + +const { MapPrototypeGet, MapPrototypeSet, MapPrototypeDelete, SafeMap } = + primordials; + +// Maps OS file descriptors to Deno resource IDs. +const fdMap = new SafeMap(); + +// Pre-populate with stdio (RIDs 0-2 map to FDs 0-2). +MapPrototypeSet(fdMap, 0, 0); +MapPrototypeSet(fdMap, 1, 1); +MapPrototypeSet(fdMap, 2, 2); + +export function registerFd(fd: number, rid: number): void { + MapPrototypeSet(fdMap, fd, rid); +} + +export function getRid(fd: number): number { + const rid = MapPrototypeGet(fdMap, fd); + if (rid !== undefined) { + // Verify the resource still exists - it might have been closed. + // For fd 0, 1, 2 (stdio), the rid equals fd and they're always valid. + if (fd <= 2) { + return rid; + } + } + // The FD is not in the map. This can happen when a raw OS file descriptor + // is received from another thread (e.g. via worker_threads postMessage). + // OS file descriptors are process-wide, so we can create a local resource + // by dup'ing the fd. The dup'd fd is independently owned and closeable. + if (fd >= 3) { + try { + const newRid = op_node_dup_fd(fd); + MapPrototypeSet(fdMap, fd, newRid); + return newRid; + } catch { + // Fall through - fd may not be valid + } + } + return fd; +} + +export function unregisterFd(fd: number): void { + MapPrototypeDelete(fdMap, fd); +} diff --git a/ext/node/polyfills/internal/fs/handle.ts b/ext/node/polyfills/internal/fs/handle.ts index 3327f2e4959ba6..f1415ef51f43f5 100644 --- a/ext/node/polyfills/internal/fs/handle.ts +++ b/ext/node/polyfills/internal/fs/handle.ts @@ -19,6 +19,7 @@ import { import { createInterface } from "node:readline"; import type { Interface as ReadlineInterface } from "node:readline"; import { core, primordials } from "ext:core/mod.js"; +import { getRid, unregisterFd } from "ext:deno_node/internal/fs/fd_map.ts"; import { BinaryOptionsArgument, FileOptionsArgument, @@ -84,15 +85,17 @@ interface ReadResult { export class FileHandle extends EventEmitter { #rid: number; + #fd: number; [kRefs]: number; [kClosePromise]?: Promise | null; [kCloseResolve]?: () => void; [kCloseReject]?: (err: Error) => void; [kLocked]: boolean; - constructor(rid: number) { + constructor(fd: number) { super(); - this.#rid = rid; + this.#fd = fd; + this.#rid = getRid(fd); this[kRefs] = 1; this[kClosePromise] = null; @@ -100,7 +103,7 @@ export class FileHandle extends EventEmitter { } get fd() { - return this.#rid; + return this.#fd; } read( @@ -200,8 +203,10 @@ export class FileHandle extends EventEmitter { #close(): Promise { return new Promise((resolve, reject) => { try { - core.close(this.fd); + core.close(this.#rid); + unregisterFd(this.#fd); this.#rid = -1; + this.#fd = -1; resolve(); } catch (err) { reject(denoErrorToNodeError(err as Error, { syscall: "close" })); diff --git a/ext/node/polyfills/tty.js b/ext/node/polyfills/tty.js index ef354125c32118..37b7183777a9da 100644 --- a/ext/node/polyfills/tty.js +++ b/ext/node/polyfills/tty.js @@ -19,11 +19,20 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { primordials } from "ext:core/mod.js"; -import { op_node_is_tty } from "ext:core/ops"; +import { op_node_is_tty, op_set_raw } from "ext:core/ops"; +import { core, primordials } from "ext:core/mod.js"; +import { ERR_INVALID_FD } from "ext:deno_node/internal/errors.ts"; +import { validateInteger } from "ext:deno_node/internal/validators.mjs"; +import { TTY } from "ext:deno_node/internal_binding/tty_wrap.ts"; +import { Socket } from "node:net"; +import { setReadStream } from "ext:deno_node/_process/streams.mjs"; +import * as io from "ext:deno_io/12_io.js"; +import { getRid } from "ext:deno_node/internal/fs/fd_map.ts"; +import { release } from "node:os"; +import process from "node:process"; + const { ArrayPrototypeSome, - Error, ObjectEntries, ObjectPrototypeHasOwnProperty, RegExpPrototypeExec, @@ -33,15 +42,79 @@ const { StringPrototypeSplit, StringPrototypeToLowerCase, } = primordials; +const { internalRidSymbol } = core; + +// Helper class to wrap a resource ID as a stream-like object. +// Used for PTY file descriptors (fd > 2) that come from NAPI modules like node-pty. +// Similar to Stdin/Stdout/Stderr classes in io module. +class TTYStream { + #rid; + #ref = true; + #opPromise; + + constructor(rid) { + this.#rid = rid; + } -import { ERR_INVALID_FD } from "ext:deno_node/internal/errors.ts"; -import { validateInteger } from "ext:deno_node/internal/validators.mjs"; -import { TTY } from "ext:deno_node/internal_binding/tty_wrap.ts"; -import { Socket } from "node:net"; -import { setReadStream } from "ext:deno_node/_process/streams.mjs"; -import * as io from "ext:deno_io/12_io.js"; -import { release } from "node:os"; -import process from "node:process"; + get [internalRidSymbol]() { + return this.#rid; + } + + get rid() { + return this.#rid; + } + + async read(p) { + if (p.length === 0) return 0; + this.#opPromise = core.read(this.#rid, p); + if (!this.#ref) { + core.unrefOpPromise(this.#opPromise); + } + const nread = await this.#opPromise; + return nread === 0 ? null : nread; + } + + readSync(p) { + if (p.length === 0) return 0; + const nread = core.readSync(this.#rid, p); + return nread === 0 ? null : nread; + } + + write(p) { + return core.write(this.#rid, p); + } + + writeSync(p) { + return core.writeSync(this.#rid, p); + } + + close() { + core.tryClose(this.#rid); + } + + setRaw(mode, options = { __proto__: null }) { + const cbreak = !!(options.cbreak ?? false); + op_set_raw(this.#rid, mode, cbreak); + } + + isTerminal() { + return core.isTerminal(this.#rid); + } + + [io.REF]() { + this.#ref = true; + if (this.#opPromise) { + core.refOpPromise(this.#opPromise); + } + } + + [io.UNREF]() { + this.#ref = false; + if (this.#opPromise) { + core.unrefOpPromise(this.#opPromise); + } + } +} // Color depth constants const COLORS_2 = 1; @@ -274,14 +347,30 @@ export class ReadStream extends Socket { throw new ERR_INVALID_FD(fd); } - // We only support `stdin`. - if (fd != 0) throw new Error("Only fd 0 is supported."); - - const tty = new TTY(io.stdin); + let handle; + // For fd > 2 (PTY from NAPI modules like node-pty), create a TTYStream wrapper + const isPty = fd > 2; + if (isPty) { + // Security: Only allow TTY file descriptors. This prevents access to + // arbitrary fds (sockets, files, etc.) via tty.ReadStream/WriteStream. + // PTY devices from node-pty are real TTYs so isatty() returns true. + if (!op_node_is_tty(fd)) { + throw new ERR_INVALID_FD(fd); + } + // Get the rid from the fd map (will dup and create resource if needed) + const rid = getRid(fd); + const stream = new TTYStream(rid); + handle = new TTY(stream); + } else { + // For stdin/stdout/stderr, use the built-in handles + handle = new TTY( + fd === 0 ? io.stdin : fd === 1 ? io.stdout : io.stderr, + ); + } super({ readableHighWaterMark: 0, - handle: tty, - manualStart: true, + handle, + manualStart: !isPty, // PTY streams should auto-start reading ...options, }); @@ -306,22 +395,39 @@ export class WriteStream extends Socket { throw new ERR_INVALID_FD(fd); } - // We only support `stdin`, `stdout` and `stderr`. - if (fd > 2) throw new Error("Only fd 0, 1 and 2 are supported."); - - const tty = new TTY( - fd === 0 ? io.stdin : fd === 1 ? io.stdout : io.stderr, - ); + let handle; + if (fd > 2) { + // Security: Only allow TTY file descriptors. This prevents access to + // arbitrary fds (sockets, files, etc.) via tty.ReadStream/WriteStream. + if (!op_node_is_tty(fd)) { + throw new ERR_INVALID_FD(fd); + } + // For fd > 2 (PTY from NAPI modules), create a TTYStream wrapper + const rid = getRid(fd); + const stream = new TTYStream(rid); + handle = new TTY(stream); + } else { + // For stdin/stdout/stderr, use the built-in handles + handle = new TTY( + fd === 0 ? io.stdin : fd === 1 ? io.stdout : io.stderr, + ); + } super({ readableHighWaterMark: 0, - handle: tty, + handle, manualStart: true, }); - const { columns, rows } = Deno.consoleSize(); - this.columns = columns; - this.rows = rows; + try { + const { columns, rows } = Deno.consoleSize(); + this.columns = columns; + this.rows = rows; + } catch { + // consoleSize can fail if not a real TTY + this.columns = 80; + this.rows = 24; + } this.isTTY = true; } diff --git a/tests/specs/node/fs_file_descriptors/__test__.jsonc b/tests/specs/node/fs_file_descriptors/__test__.jsonc new file mode 100644 index 00000000000000..1b3d4a8c1454b2 --- /dev/null +++ b/tests/specs/node/fs_file_descriptors/__test__.jsonc @@ -0,0 +1,8 @@ +{ + "tests": { + "open_returns_real_fd": { + "args": "run --allow-read --allow-write main.ts", + "output": "main.out" + } + } +} diff --git a/tests/specs/node/fs_file_descriptors/main.out b/tests/specs/node/fs_file_descriptors/main.out new file mode 100644 index 00000000000000..95e6b27457191e --- /dev/null +++ b/tests/specs/node/fs_file_descriptors/main.out @@ -0,0 +1,6 @@ +fd >= 3: true +bytes written: 8 +stat.size: 8 +bytes read: 8 +data: hello fd +closed successfully diff --git a/tests/specs/node/fs_file_descriptors/main.ts b/tests/specs/node/fs_file_descriptors/main.ts new file mode 100644 index 00000000000000..2f6f89e6a93f0e --- /dev/null +++ b/tests/specs/node/fs_file_descriptors/main.ts @@ -0,0 +1,28 @@ +import { closeSync, fstatSync, openSync, readSync, writeSync } from "node:fs"; +import { Buffer } from "node:buffer"; + +const tempFile = Deno.makeTempFileSync(); + +// Test that openSync returns a real OS FD (should be >= 3, since 0-2 are stdio) +const fd = openSync(tempFile, "w+"); +console.log(`fd >= 3: ${fd >= 3}`); + +// Test that writeSync works with the FD +const written = writeSync(fd, Buffer.from("hello fd")); +console.log(`bytes written: ${written}`); + +// Test that fstatSync works with the FD +const stat = fstatSync(fd); +console.log(`stat.size: ${stat.size}`); + +// Test that readSync works with the FD (seek to beginning first) +const buf = Buffer.alloc(8); +const bytesRead = readSync(fd, buf, 0, 8, 0); +console.log(`bytes read: ${bytesRead}`); +console.log(`data: ${buf.toString()}`); + +// Test that closeSync works with the FD +closeSync(fd); +console.log("closed successfully"); + +Deno.removeSync(tempFile); diff --git a/tests/specs/node/worker_threads_fd_passing/__test__.jsonc b/tests/specs/node/worker_threads_fd_passing/__test__.jsonc new file mode 100644 index 00000000000000..076a40de3f6039 --- /dev/null +++ b/tests/specs/node/worker_threads_fd_passing/__test__.jsonc @@ -0,0 +1,10 @@ +{ + "tests": { + "pass_fd_to_worker": { + // Cross-worker fd passing uses dup() which is unix-only for now + "if": "unix", + "args": "run --allow-read main.mjs", + "output": "main.out" + } + } +} diff --git a/tests/specs/node/worker_threads_fd_passing/main.mjs b/tests/specs/node/worker_threads_fd_passing/main.mjs new file mode 100644 index 00000000000000..713a5c9f94a1e4 --- /dev/null +++ b/tests/specs/node/worker_threads_fd_passing/main.mjs @@ -0,0 +1,36 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +import { isMainThread, parentPort, Worker } from "node:worker_threads"; +import fs from "node:fs"; +import path from "node:path"; + +if (isMainThread) { + const testFile = path.join(import.meta.dirname, "main.mjs"); + const fd = fs.openSync(testFile, "r"); + console.log(`main: opened fd ${fd} (>= 3: ${fd >= 3})`); + + const w = new Worker(import.meta.filename); + w.postMessage(fd); + + w.once("message", (msg) => { + console.log(`main: worker result: ${msg}`); + fs.closeSync(fd); + console.log("main: closed fd"); + w.terminate(); + }); +} else { + parentPort.once("message", (fd) => { + console.log(`worker: received fd ${fd}`); + + // Read from the fd received from the main thread + const buf = Buffer.alloc(64); + const bytesRead = fs.readSync(fd, buf, 0, 64, 0); + console.log(`worker: read ${bytesRead} bytes`); + console.log( + `worker: starts with copyright: ${ + buf.toString().startsWith("// Copyright") + }`, + ); + + parentPort.postMessage("ok"); + }); +} diff --git a/tests/specs/node/worker_threads_fd_passing/main.out b/tests/specs/node/worker_threads_fd_passing/main.out new file mode 100644 index 00000000000000..13a6091778e0b1 --- /dev/null +++ b/tests/specs/node/worker_threads_fd_passing/main.out @@ -0,0 +1,6 @@ +main: opened fd [WILDCARD] (>= 3: true) +worker: received fd [WILDCARD] +worker: read 64 bytes +worker: starts with copyright: true +main: worker result: ok +main: closed fd diff --git a/tests/unit_node/_fs/_fs_appendFile_test.ts b/tests/unit_node/_fs/_fs_appendFile_test.ts index e194f1a6d533c5..e3e0ac4268ebc1 100644 --- a/tests/unit_node/_fs/_fs_appendFile_test.ts +++ b/tests/unit_node/_fs/_fs_appendFile_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assertEquals, assertThrows, fail } from "@std/assert"; -import { appendFile, appendFileSync } from "node:fs"; +import { appendFile, appendFileSync, closeSync, openSync } from "node:fs"; import { fromFileUrl } from "@std/path"; import { assertCallbackErrorUncaught } from "../_test_utils.ts"; @@ -63,20 +63,12 @@ Deno.test({ }); Deno.test({ - name: "Async: Data is written to passed in rid", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, + name: "Async: Data is written to passed in fd", async fn() { const tempFile: string = await Deno.makeTempFile(); - using file = await Deno.open(tempFile, { - create: true, - write: true, - read: true, - }); + const fd = openSync(tempFile, "a"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - appendFile(file.rid, "hello world", (err) => { + appendFile(fd, "hello world", (err) => { if (err) reject(); else resolve(); }); @@ -88,6 +80,7 @@ Deno.test({ fail("No error expected"); }) .finally(async () => { + closeSync(fd); await Deno.remove(tempFile); }); }, @@ -156,19 +149,12 @@ Deno.test({ }); Deno.test({ - name: "Sync: Data is written to passed in rid", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, + name: "Sync: Data is written to passed in fd", fn() { const tempFile: string = Deno.makeTempFileSync(); - using file = Deno.openSync(tempFile, { - create: true, - write: true, - read: true, - }); - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - appendFileSync(file.rid, "hello world"); + const fd = openSync(tempFile, "a"); + appendFileSync(fd, "hello world"); + closeSync(fd); const data = Deno.readFileSync(tempFile); assertEquals(decoder.decode(data), "hello world"); Deno.removeSync(tempFile); diff --git a/tests/unit_node/_fs/_fs_close_test.ts b/tests/unit_node/_fs/_fs_close_test.ts index 6a4130fc8ee355..19ff2860ae7658 100644 --- a/tests/unit_node/_fs/_fs_close_test.ts +++ b/tests/unit_node/_fs/_fs_close_test.ts @@ -6,16 +6,12 @@ import { setTimeout } from "node:timers/promises"; Deno.test({ name: "ASYNC: File is closed", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const tempFile: string = await Deno.makeTempFile(); - const file: Deno.FsFile = await Deno.open(tempFile); + const fd = openSync(tempFile, "r"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - close(file.rid, (err) => { + close(fd, (err) => { if (err !== null) reject(); else resolve(); }); @@ -38,17 +34,13 @@ Deno.test({ Deno.test({ name: "close callback should be asynchronous", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const tempFile: string = Deno.makeTempFileSync(); - const file: Deno.FsFile = Deno.openSync(tempFile); + const fd = openSync(tempFile, "r"); let foo: string; const promise = new Promise((resolve) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - close(file.rid, () => { + close(fd, () => { assert(foo === "bar"); resolve(); }); @@ -62,15 +54,11 @@ Deno.test({ Deno.test({ name: "SYNC: File is closed", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const tempFile: string = Deno.makeTempFileSync(); - const file: Deno.FsFile = Deno.openSync(tempFile); + const fd = openSync(tempFile, "r"); - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - closeSync(file.rid); + closeSync(fd); Deno.removeSync(tempFile); }, }); @@ -84,19 +72,16 @@ Deno.test({ Deno.test({ name: "[std/node/fs] close callback isn't called twice if error is thrown", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, }, async () => { const tempFile = await Deno.makeTempFile(); const importUrl = new URL("node:fs", import.meta.url); await assertCallbackErrorUncaught({ prelude: ` - import { close } from ${JSON.stringify(importUrl)}; + import { close, openSync } from ${JSON.stringify(importUrl)}; - const file = await Deno.open(${JSON.stringify(tempFile)}); + const fd = openSync(${JSON.stringify(tempFile)}, "r"); `, - invocation: "close(file.rid, ", + invocation: "close(fd, ", async cleanup() { await Deno.remove(tempFile); }, @@ -107,11 +92,11 @@ Deno.test({ name: "[std/node/fs] close with default callback if none is provided", }, async () => { const tempFile = await Deno.makeTempFile(); - const rid = openSync(tempFile, "r"); - close(rid); + const fd = openSync(tempFile, "r"); + close(fd); await setTimeout(1000); assertThrows(() => { - closeSync(rid), Deno.errors.BadResource; + closeSync(fd), Deno.errors.BadResource; }); await Deno.remove(tempFile); }); diff --git a/tests/unit_node/_fs/_fs_fdatasync_test.ts b/tests/unit_node/_fs/_fs_fdatasync_test.ts index aa9dfa3fcdcfda..2a92063f5baf3c 100644 --- a/tests/unit_node/_fs/_fs_fdatasync_test.ts +++ b/tests/unit_node/_fs/_fs_fdatasync_test.ts @@ -1,26 +1,24 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assertEquals, fail } from "@std/assert"; -import { fdatasync, fdatasyncSync } from "node:fs"; +import { + closeSync, + fdatasync, + fdatasyncSync, + openSync, + writeSync, +} from "node:fs"; Deno.test({ name: "ASYNC: flush any pending data operations of the given file stream to disk", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = await Deno.makeTempFile(); - using file = await Deno.open(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "rs+"); const data = new Uint8Array(64); - await file.write(data); + writeSync(fd, data); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - fdatasync(file.rid, (err: Error | null) => { + fdatasync(fd, (err: Error | null) => { if (err !== null) reject(); else resolve(); }); @@ -34,6 +32,7 @@ Deno.test({ }, ) .finally(async () => { + closeSync(fd); await Deno.remove(filePath); }); }, @@ -42,24 +41,17 @@ Deno.test({ Deno.test({ name: "SYNC: flush any pending data operations of the given file stream to disk.", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); - using file = Deno.openSync(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "rs+"); const data = new Uint8Array(64); - file.writeSync(data); + writeSync(fd, data); try { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - fdatasyncSync(file.rid); + fdatasyncSync(fd); assertEquals(Deno.readFileSync(filePath), data); } finally { + closeSync(fd); Deno.removeSync(filePath); } }, diff --git a/tests/unit_node/_fs/_fs_fstat_test.ts b/tests/unit_node/_fs/_fs_fstat_test.ts index 897d9ae8be38b7..e8b5e6f7aa5d58 100644 --- a/tests/unit_node/_fs/_fs_fstat_test.ts +++ b/tests/unit_node/_fs/_fs_fstat_test.ts @@ -1,33 +1,31 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import { fstat, fstatSync } from "node:fs"; +import { closeSync, fstat, fstatSync, openSync } from "node:fs"; import { fail } from "@std/assert"; import { assertStats, assertStatsBigInt } from "../_test_utils.ts"; import type { BigIntStats, Stats } from "node:fs"; Deno.test({ name: "ASYNC: get a file Stats", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = await Deno.makeTempFile(); - using file = await Deno.open(filePath); + const fd = openSync(filePath, "r"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - fstat(file.rid, (err: Error | null, stat: Stats) => { + fstat(fd, (err: Error | null, stat: Stats) => { if (err) reject(err); resolve(stat); }); }) .then( (stat) => { + using file = Deno.openSync(filePath); assertStats(stat, file.statSync()); }, () => fail(), ) .finally(() => { + closeSync(fd); Deno.removeSync(filePath); }); }, @@ -35,17 +33,13 @@ Deno.test({ Deno.test({ name: "ASYNC: get a file BigInt Stats", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = await Deno.makeTempFile(); - using file = await Deno.open(filePath); + const fd = openSync(filePath, "r"); await new Promise((resolve, reject) => { fstat( - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - file.rid, + fd, { bigint: true }, (err: Error | null, stat: BigIntStats) => { if (err) reject(err); @@ -54,10 +48,14 @@ Deno.test({ ); }) .then( - (stat) => assertStatsBigInt(stat, file.statSync()), + (stat) => { + using file = Deno.openSync(filePath); + assertStatsBigInt(stat, file.statSync()); + }, () => fail(), ) .finally(() => { + closeSync(fd); Deno.removeSync(filePath); }); }, @@ -65,17 +63,15 @@ Deno.test({ Deno.test({ name: "SYNC: get a file Stats", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); - using file = Deno.openSync(filePath); + const fd = openSync(filePath, "r"); try { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - assertStats(fstatSync(file.rid), file.statSync()); + using file = Deno.openSync(filePath); + assertStats(fstatSync(fd), file.statSync()); } finally { + closeSync(fd); Deno.removeSync(filePath); } }, @@ -83,25 +79,18 @@ Deno.test({ Deno.test({ name: "SYNC: get a file BigInt Stats", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); - using file = Deno.openSync(filePath); + const fd = openSync(filePath, "r"); try { - // HEAD - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - assertStatsBigInt(fstatSync(file.rid, { bigint: true }), file.statSync()); - // + using file = Deno.openSync(filePath); assertStatsBigInt( - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - fstatSync(file.rid, { bigint: true }), + fstatSync(fd, { bigint: true }), file.statSync(), ); - //main } finally { + closeSync(fd); Deno.removeSync(filePath); } }, diff --git a/tests/unit_node/_fs/_fs_fsync_test.ts b/tests/unit_node/_fs/_fs_fsync_test.ts index a405cd970ea285..622a0cb5a76245 100644 --- a/tests/unit_node/_fs/_fs_fsync_test.ts +++ b/tests/unit_node/_fs/_fs_fsync_test.ts @@ -1,25 +1,17 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assertEquals, fail } from "@std/assert"; -import { fsync, fsyncSync } from "node:fs"; +import { closeSync, fsync, fsyncSync, openSync } from "node:fs"; Deno.test({ name: "ASYNC: flush any pending data of the given file stream to disk", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = await Deno.makeTempFile(); - using file = await Deno.open(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "rs+"); const size = 64; - await file.truncate(size); + Deno.truncateSync(filePath, size); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - fsync(file.rid, (err: Error | null) => { + fsync(fd, (err: Error | null) => { if (err !== null) reject(); else resolve(); }); @@ -33,6 +25,7 @@ Deno.test({ }, ) .finally(async () => { + closeSync(fd); await Deno.remove(filePath); }); }, @@ -40,24 +33,17 @@ Deno.test({ Deno.test({ name: "SYNC: flush any pending data the given file stream to disk", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); - using file = Deno.openSync(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "rs+"); const size = 64; - file.truncateSync(size); + Deno.truncateSync(filePath, size); try { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - fsyncSync(file.rid); + fsyncSync(fd); assertEquals(Deno.statSync(filePath).size, size); } finally { + closeSync(fd); Deno.removeSync(filePath); } }, diff --git a/tests/unit_node/_fs/_fs_ftruncate_test.ts b/tests/unit_node/_fs/_fs_ftruncate_test.ts index 5264a1b9e35fc9..7b4744f144f73a 100644 --- a/tests/unit_node/_fs/_fs_ftruncate_test.ts +++ b/tests/unit_node/_fs/_fs_ftruncate_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assertEquals, assertThrows, fail } from "@std/assert"; -import { ftruncate, ftruncateSync } from "node:fs"; +import { closeSync, ftruncate, ftruncateSync, openSync } from "node:fs"; Deno.test({ name: "ASYNC: no callback function results in Error", @@ -18,21 +18,13 @@ Deno.test({ Deno.test({ name: "ASYNC: truncate entire file contents", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = Deno.makeTempFileSync(); await Deno.writeTextFile(filePath, "hello world"); - using file = await Deno.open(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "r+"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - ftruncate(file.rid, (err: Error | null) => { + ftruncate(fd, (err: Error | null) => { if (err !== null) reject(); else resolve(); }); @@ -47,6 +39,7 @@ Deno.test({ }, ) .finally(() => { + closeSync(fd); Deno.removeSync(filePath); }); }, @@ -54,21 +47,13 @@ Deno.test({ Deno.test({ name: "ASYNC: truncate file to a size of precisely len bytes", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = Deno.makeTempFileSync(); await Deno.writeTextFile(filePath, "hello world"); - using file = await Deno.open(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "r+"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - ftruncate(file.rid, 3, (err: Error | null) => { + ftruncate(fd, 3, (err: Error | null) => { if (err !== null) reject(); else resolve(); }); @@ -83,6 +68,7 @@ Deno.test({ }, ) .finally(() => { + closeSync(fd); Deno.removeSync(filePath); }); }, @@ -90,24 +76,17 @@ Deno.test({ Deno.test({ name: "SYNC: truncate entire file contents", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); Deno.writeFileSync(filePath, new TextEncoder().encode("hello world")); - using file = Deno.openSync(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "r+"); try { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - ftruncateSync(file.rid); + ftruncateSync(fd); const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath); assertEquals(fileInfo.size, 0); } finally { + closeSync(fd); Deno.removeSync(filePath); } }, @@ -115,24 +94,17 @@ Deno.test({ Deno.test({ name: "SYNC: truncate file to a size of precisely len bytes", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); Deno.writeFileSync(filePath, new TextEncoder().encode("hello world")); - using file = Deno.openSync(filePath, { - read: true, - write: true, - create: true, - }); + const fd = openSync(filePath, "r+"); try { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - ftruncateSync(file.rid, 3); + ftruncateSync(fd, 3); const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath); assertEquals(fileInfo.size, 3); } finally { + closeSync(fd); Deno.removeSync(filePath); } }, diff --git a/tests/unit_node/_fs/_fs_futimes_test.ts b/tests/unit_node/_fs/_fs_futimes_test.ts index eb1ec835c2a109..ba6f3d8cefa6d1 100644 --- a/tests/unit_node/_fs/_fs_futimes_test.ts +++ b/tests/unit_node/_fs/_fs_futimes_test.ts @@ -1,22 +1,18 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assertEquals, assertThrows, fail } from "@std/assert"; -import { futimes, futimesSync } from "node:fs"; +import { closeSync, futimes, futimesSync, openSync } from "node:fs"; const randomDate = new Date(Date.now() + 1000); Deno.test({ name: "ASYNC: change the file system timestamps of the object referenced by path", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, async fn() { const filePath = Deno.makeTempFileSync(); - using file = await Deno.open(filePath, { create: true, write: true }); + const fd = openSync(filePath, "w"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - futimes(file.rid, randomDate, randomDate, (err: Error | null) => { + futimes(fd, randomDate, randomDate, (err: Error | null) => { if (err !== null) reject(); else resolve(); }); @@ -32,6 +28,7 @@ Deno.test({ }, ) .finally(() => { + closeSync(fd); Deno.removeSync(filePath); }); }, @@ -66,22 +63,19 @@ Deno.test({ Deno.test({ name: "SYNC: change the file system timestamps of the object referenced by path", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, fn() { const filePath = Deno.makeTempFileSync(); - using file = Deno.openSync(filePath, { create: true, write: true }); + const fd = openSync(filePath, "w"); try { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - futimesSync(file.rid, randomDate, randomDate); + futimesSync(fd, randomDate, randomDate); const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath); assertEquals(fileInfo.mtime, randomDate); assertEquals(fileInfo.atime, randomDate); } finally { + closeSync(fd); Deno.removeSync(filePath); } }, diff --git a/tests/unit_node/_fs/_fs_writeFile_test.ts b/tests/unit_node/_fs/_fs_writeFile_test.ts index 5dacb7eb9a0c80..8e699b2d57213c 100644 --- a/tests/unit_node/_fs/_fs_writeFile_test.ts +++ b/tests/unit_node/_fs/_fs_writeFile_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assert, assertEquals, assertRejects, assertThrows } from "@std/assert"; -import { writeFile, writeFileSync } from "node:fs"; +import { closeSync, openSync, writeFile, writeFileSync } from "node:fs"; import * as path from "@std/path"; type TextEncodings = @@ -88,28 +88,19 @@ Deno.test("Invalid encoding results in error()", function testEncodingErrors() { }); Deno.test( - { - name: "Data is written to correct rid", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, - }, - async function testCorrectWriteUsingRid() { + "Data is written to correct fd", + async function testCorrectWriteUsingFd() { const tempFile: string = await Deno.makeTempFile(); - using file = await Deno.open(tempFile, { - create: true, - write: true, - read: true, - }); + const fd = openSync(tempFile, "w"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - writeFile(file.rid, "hello world", (err) => { + writeFile(fd, "hello world", (err) => { if (err) return reject(err); resolve(); }); }); + closeSync(fd); const data = await Deno.readFile(tempFile); await Deno.remove(tempFile); assertEquals(decoder.decode(data), "hello world"); @@ -187,9 +178,6 @@ Deno.test("Path can be an URL", async function testCorrectWriteUsingURL() { Deno.test({ name: "Mode is correctly set", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, }, async function testCorrectFileMode() { const filename = "_fs_writeFile_test_file.txt"; @@ -205,32 +193,26 @@ Deno.test({ }); Deno.test( - { - name: "Mode is not set when rid is passed", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, - }, + "Mode is not set when fd is passed", async function testCorrectFileModeRid() { const filename: string = await Deno.makeTempFile(); - using file = await Deno.open(filename, { - create: true, - write: true, - read: true, - }); + const originalMode = (await Deno.stat(filename)).mode!; + const fd = openSync(filename, "w"); await new Promise((resolve, reject) => { - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - writeFile(file.rid, "hello world", { mode: modeAsync }, (err) => { + writeFile(fd, "hello world", { mode: modeAsync }, (err) => { if (err) return reject(err); resolve(); }); }); + closeSync(fd); const fileInfo = await Deno.stat(filename); await Deno.remove(filename); assert(fileInfo.mode); - assertEquals(fileInfo.mode & 0o777, modeAsync); + // When an fd is passed, the mode option should be ignored, + // so the file mode should remain unchanged from the original. + assertEquals(fileInfo.mode & 0o777, originalMode & 0o777); }, ); @@ -261,23 +243,14 @@ Deno.test( ); Deno.test( - { - name: "Data is written synchronously to correct rid", - // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined. - // The fs APIs should be rewritten to use actual FDs, not RIDs - ignore: true, - }, - function testCorrectWriteSyncUsingRid() { + "Data is written synchronously to correct fd", + function testCorrectWriteSyncUsingFd() { const tempFile: string = Deno.makeTempFileSync(); - using file = Deno.openSync(tempFile, { - create: true, - write: true, - read: true, - }); + const fd = openSync(tempFile, "w"); - // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used - writeFileSync(file.rid, "hello world"); + writeFileSync(fd, "hello world"); + closeSync(fd); const data = Deno.readFileSync(tempFile); Deno.removeSync(tempFile); assertEquals(decoder.decode(data), "hello world");