From 1a56fc8485e9792ddfbb1716a5742b072a8b7832 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:05:54 -0600 Subject: [PATCH 1/2] Report the host's reason when a new file cannot be created, and keep only the last failed write or chmod error --- src/natives/io.ts | 6 ++++-- src/natives/nio.ts | 16 +++++++++++----- src/node.ts | 17 +++++++++-------- test/java/CreateRefused.java | 26 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 test/java/CreateRefused.java diff --git a/src/natives/io.ts b/src/natives/io.ts index 96add50..9f96879 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -169,7 +169,9 @@ export function openFile(t: JThread, path: string, write: boolean, truncate: boo let data: Uint8Array; if (!st) { if (!write) notFound(path); - if (!fs.write(abs, new Uint8Array(0))) notFound(path, fs.writable(abs) ? "No such file or directory" : "Read-only file system"); + if (!fs.write(abs, new Uint8Array(0))) { + notFound(path, ERRNO[fs.writeError(abs) ?? ""] ?? (fs.writable(abs) ? "No such file or directory" : "Read-only file system")); + } data = new Uint8Array(0); } else { if (write && !fs.writable(abs)) notFound(path, "Read-only file system"); @@ -224,7 +226,7 @@ export function flush(t: JThread, f: OpenFile): boolean { const ERRNO: Record = { ENOSPC: "No space left on device", EFBIG: "File too large", EDQUOT: "Disk quota exceeded", EACCES: "Permission denied", EPERM: "Operation not permitted", EROFS: "Read-only file system", - ENOENT: "No such file or directory", + ENOENT: "No such file or directory", ENAMETOOLONG: "File name too long", ENOTDIR: "Not a directory", }; // Where Java learns of a write that failed: HotSpot reports it from write(), diff --git a/src/natives/nio.ts b/src/natives/nio.ts index df7336c..8276084 100644 --- a/src/natives/nio.ts +++ b/src/natives/nio.ts @@ -12,21 +12,23 @@ import type { Natives } from "./util.ts"; export const EPERM = 1, ENOENT = 2, EINTR = 4, EIO = 5, EBADF = 9, ENOMEM = 12, EACCES = 13, EEXIST = 17, EXDEV = 18, ENOTDIR = 20, EISDIR = 21, EINVAL = 22, ENOSPC = 28, EROFS = 30, - ENOTEMPTY = 39, ENOSYS = 38, ELOOP = 40, ENOTSUP = 95, ESTALE = 116; + ENOTEMPTY = 39, ENOSYS = 38, ENAMETOOLONG = 36, ELOOP = 40, ENOTSUP = 95, ESTALE = 116, EDQUOT = 122; const MESSAGES: Record = { [EPERM]: "Operation not permitted", [EINTR]: "Interrupted system call", [EIO]: "Input/output error", [ENOMEM]: "Cannot allocate memory", [ENOSPC]: "No space left on device", [ELOOP]: "Too many levels of symbolic links", [ENOTSUP]: "Operation not supported", [ESTALE]: "Stale file handle", + [ENAMETOOLONG]: "File name too long", [EDQUOT]: "Disk quota exceeded", [ENOENT]: "No such file or directory", [EBADF]: "Bad file descriptor", [EACCES]: "Permission denied", [EEXIST]: "File exists", [EXDEV]: "Invalid cross-device link", [ENOTDIR]: "Not a directory", [EISDIR]: "Is a directory", [EINVAL]: "Invalid argument", [EROFS]: "Read-only file system", [ENOTEMPTY]: "Directory not empty", [ENOSYS]: "Function not implemented", }; -const CHMOD_ERRNO: Record = { +// errno numbers of the host errors Node reports by name +const HOST_ERRNO: Record = { EPERM, ENOENT, EINTR, EIO, EBADF, ENOMEM, EACCES, ENOTDIR, EINVAL, ENOSPC, EROFS, ELOOP, - ENOTSUP, EOPNOTSUPP: ENOTSUP, ESTALE, + ENOTSUP, EOPNOTSUPP: ENOTSUP, ESTALE, ENAMETOOLONG, EDQUOT, }; // Linux open(2) flags @@ -66,7 +68,7 @@ function chmod(t: JThread, p: string, mode: number) { if (!fs.stat(p)) unixError(ENOENT); unixError(EROFS); } - unixError(CHMOD_ERRNO[fs.chmodError(p) ?? "EIO"] ?? EIO); + unixError(HOST_ERRNO[fs.chmodError(p) ?? "EIO"] ?? EIO); } function fillAttrs(t: JThread, attrs: JObject, p: string): number { @@ -148,7 +150,11 @@ export const natives: Natives = { fileOf(t, fd).ino = fileInode(t, abs); return fd; } catch (e) { - if (e instanceof PendingThrow) unixError(t.vm.fs.writable(abs) ? ENOENT : EROFS); + if (e instanceof PendingThrow) { + // a new file the host would not create: its reason + const host = st ? undefined : HOST_ERRNO[t.vm.fs.writeError(abs) ?? ""]; + unixError(host ?? (t.vm.fs.writable(abs) ? ENOENT : EROFS)); + } throw e; } }, diff --git a/src/node.ts b/src/node.ts index d4ac8a6..7238fda 100644 --- a/src/node.ts +++ b/src/node.ts @@ -80,19 +80,20 @@ export class NodeBackend implements Backend { if (!this.writable) this.lists.set(p, names); return names; } - private writeErrors = new Map(); + // the host's reason for the last failed write, read right after it + private writeFailure: [string, string] | null = null; write(p: string, data: Uint8Array) { try { fs.writeFileSync(this.host(p), data); - this.writeErrors.delete(p); + this.writeFailure = null; return true; } catch (e) { - this.writeErrors.set(p, (e as NodeJS.ErrnoException).code ?? "EIO"); + this.writeFailure = [p, (e as NodeJS.ErrnoException).code ?? "EIO"]; return false; } } writeError(p: string) { - return this.writeErrors.get(p); + return this.writeFailure?.[0] === p ? this.writeFailure[1] : undefined; } denied(p: string) { const host = this.host(p); @@ -103,19 +104,19 @@ export class NodeBackend implements Backend { return (e as NodeJS.ErrnoException).code === "EACCES" || (e as NodeJS.ErrnoException).code === "EPERM"; } } - private chmodErrors = new Map(); + private chmodFailure: [string, string] | null = null; chmod(p: string, f: (mode: number) => number) { try { fs.chmodSync(this.host(p), f(fs.statSync(this.host(p)).mode & 0o7777)); - this.chmodErrors.delete(p); + this.chmodFailure = null; return true; } catch (e) { - this.chmodErrors.set(p, (e as NodeJS.ErrnoException).code ?? "EIO"); + this.chmodFailure = [p, (e as NodeJS.ErrnoException).code ?? "EIO"]; return false; } } chmodError(p: string) { - return this.chmodErrors.get(p); + return this.chmodFailure?.[0] === p ? this.chmodFailure[1] : undefined; } access(p: string, mode: number) { try { diff --git a/test/java/CreateRefused.java b/test/java/CreateRefused.java new file mode 100644 index 0000000..16b4462 --- /dev/null +++ b/test/java/CreateRefused.java @@ -0,0 +1,26 @@ +import java.io.*; +import java.nio.file.*; + +// Creating a file the host refuses reports the host's reason, as HotSpot does. +public class CreateRefused { + public static void main(String[] args) throws Exception { + String name = Path.of(System.getProperty("java.io.tmpdir"), "x".repeat(300)).toString(); + try (var out = new FileOutputStream(name)) { + System.out.println("created"); + } catch (IOException e) { + System.out.println(e.getClass().getName() + ": " + e.getMessage().replace(name, "/")); + } + try { + Files.createFile(Path.of(name)); + System.out.println("created"); + } catch (IOException e) { + System.out.println(e.getClass().getName() + ": " + e.getMessage().replace(name, "/")); + } + try { + Files.newOutputStream(Path.of(name)).close(); + System.out.println("created"); + } catch (IOException e) { + System.out.println(e.getClass().getName() + ": " + e.getMessage().replace(name, "/")); + } + } +} From 28984cd59d5a7994cb71fc979b67b0f5a7b63b92 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:34:41 -0600 Subject: [PATCH 2/2] Name EIO and ELOOP when a new file cannot be created --- src/natives/io.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/natives/io.ts b/src/natives/io.ts index 9f96879..170ee2a 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -227,6 +227,7 @@ const ERRNO: Record = { ENOSPC: "No space left on device", EFBIG: "File too large", EDQUOT: "Disk quota exceeded", EACCES: "Permission denied", EPERM: "Operation not permitted", EROFS: "Read-only file system", ENOENT: "No such file or directory", ENAMETOOLONG: "File name too long", ENOTDIR: "Not a directory", + EIO: "Input/output error", ELOOP: "Too many levels of symbolic links", }; // Where Java learns of a write that failed: HotSpot reports it from write(),