From c62a3999facb3d48763ea1fe9aa178546c3a8d94 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:02:54 -0600 Subject: [PATCH 1/3] Implement the java.util.prefs file natives: chmod and lock files --- src/natives/io.ts | 58 ++++++++++++++++++++++++++++++++++++++++++ test/java/Prefs.java | 31 ++++++++++++++++++++++ test/jtreg/passing.txt | 8 ++++++ 3 files changed, 97 insertions(+) create mode 100644 test/java/Prefs.java diff --git a/src/natives/io.ts b/src/natives/io.ts index 6c3941f..5b1d282 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -356,10 +356,68 @@ function pathOf(t: JThread, file: JObject): string { return t.vm.jsString(t.vm.getField(file, "java/io/File", "path")); } +// java.util.prefs lock files: fcntl locks, which only this VM's threads can +// contend for here. path -> readers, or -1 while one writer holds it. +interface PrefsLock { path: string; shared: boolean } +function prefsLocks(t: JThread) { + let s = t.vm.state.get("prefsLocks") as { held: Map; fds: Map } | undefined; + if (!s) t.vm.state.set("prefsLocks", (s = { held: new Map(), fds: new Map() })); + return s; +} +const ERRNO_NUMBER: Record = { EPERM: 1, ENOENT: 2, EIO: 5, EAGAIN: 11, EACCES: 13, ENOTDIR: 20, EROFS: 30 }; + +function intPair(t: JThread, a: number, b: number): JArray { + const arr = t.vm.newArray(t.vm.primClass("I"), 2); + (arr.a as Int32Array).set([a, b]); + return arr; +} + const FOS = "java/io/FileOutputStream", FIS = "java/io/FileInputStream", RAF = "java/io/RandomAccessFile"; const UFS = "java/io/UnixFileSystem"; export const natives: Natives = { + // the errno of chmod(2), or 0 + "java/util/prefs/FileSystemPreferences.chmod(Ljava/lang/String;I)I": (t, [name, mode]) => { + const abs = resolvePath(t, t.vm.jsString(name)); + if (!t.vm.fs.stat(abs)) return ERRNO_NUMBER.ENOENT; + const ok = t.vm.fs.chmod(abs, (m) => (m & ~0o7777) | (mode & 0o7777)); + return ok === false ? ERRNO_NUMBER[t.vm.fs.chmodError(abs) ?? "EPERM"] ?? ERRNO_NUMBER.EPERM : 0; + }, + // [fd, errno]; fd 0 when the file could not be opened or locked + "java/util/prefs/FileSystemPreferences.lockFile0(Ljava/lang/String;IZ)[I": (t, [name, , shared]) => { + const abs = resolvePath(t, t.vm.jsString(name)); + let fd: number; + try { + fd = openFile(t, abs, !shared, false, false); + } catch { + return intPair(t, 0, t.vm.fs.stat(abs) ? ERRNO_NUMBER.EACCES : ERRNO_NUMBER.ENOENT); + } + const { held, fds } = prefsLocks(t); + const now = held.get(abs) ?? 0; + if (shared ? now < 0 : now !== 0) { + closeFd(t, fd); + return intPair(t, 0, ERRNO_NUMBER.EAGAIN); + } + held.set(abs, shared ? now + 1 : -1); + fds.set(fd, { path: abs, shared: !!shared }); + return intPair(t, fd, 0); + }, + "java/util/prefs/FileSystemPreferences.unlockFile0(I)I": (t, [fd]) => { + const { held, fds } = prefsLocks(t); + const lock = fds.get(fd); + if (lock) { + fds.delete(fd); + const now = held.get(lock.path) ?? 0; + if (lock.shared && now > 1) held.set(lock.path, now - 1); + else held.delete(lock.path); + } + try { + closeFd(t, fd); + } catch { + return ERRNO_NUMBER.EIO; + } + return 0; + }, // serialization: the default serialVersionUID depends on whether the class // itself declares a static initializer "java/io/ObjectStreamClass.initNative()V": () => undefined, diff --git a/test/java/Prefs.java b/test/java/Prefs.java new file mode 100644 index 0000000..71fb43e --- /dev/null +++ b/test/java/Prefs.java @@ -0,0 +1,31 @@ +import java.nio.file.*; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.Preferences; + +// User preferences are stored in files under java.util.prefs.userRoot, which +// FileSystemPreferences locks and chmods as on HotSpot. +public class Prefs { + public static void main(String[] args) throws Exception { + // its log lines carry the time; HotSpot logs the same ones + Logger.getLogger("java.util.prefs").setLevel(Level.OFF); + // a fresh root, not the user's own preferences + Path root = Files.createTempDirectory("prefs"); + System.setProperty("java.util.prefs.userRoot", root.toString()); + Preferences node = Preferences.userRoot().node("java-web-test"); + try { + node.put("greeting", "hello"); + node.putInt("count", 3); + node.flush(); + node.sync(); + System.out.println(node.get("greeting", "?") + " " + node.getInt("count", 0) + " " + String.join(",", node.keys())); + } finally { + node.removeNode(); + Preferences.userRoot().flush(); + try (var files = Files.walk(root)) { + files.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); + } + } + } +} diff --git a/test/jtreg/passing.txt b/test/jtreg/passing.txt index d35c862..3b6155c 100644 --- a/test/jtreg/passing.txt +++ b/test/jtreg/passing.txt @@ -3246,6 +3246,14 @@ java/util/logging/TestLoggingWithMainAppContext.java java/util/logging/TestMainAppContext.java java/util/logging/TestUILoggerContext.java java/util/logging/XMLFormatterDate.java +java/util/prefs/AddNodeChangeListener.java +java/util/prefs/CodePointZeroPrefsTest.java +java/util/prefs/CommentsInXml.java +java/util/prefs/ConflictInFlush.java +java/util/prefs/ExportNode.java +java/util/prefs/ExportSubtree.java +java/util/prefs/RemoveNullKeyCheck.java +java/util/prefs/RemoveUnregedListener.java java/util/prefs/SerializeExceptions.java java/util/regex/ImmutableMatchResultTest.java java/util/regex/NamedGroupsTests.java From 097944ce5370eb8283e9889a2bb81dbd48a14498 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:16:37 -0600 Subject: [PATCH 2/3] Map every errno name Node reports for prefs natives, and hold the test's logger so its level survives a collection --- src/natives/io.ts | 8 ++++++-- test/java/Prefs.java | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/natives/io.ts b/src/natives/io.ts index 5b1d282..fae9313 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -364,7 +364,11 @@ function prefsLocks(t: JThread) { if (!s) t.vm.state.set("prefsLocks", (s = { held: new Map(), fds: new Map() })); return s; } -const ERRNO_NUMBER: Record = { EPERM: 1, ENOENT: 2, EIO: 5, EAGAIN: 11, EACCES: 13, ENOTDIR: 20, EROFS: 30 }; +// Linux errno numbers of the codes Node reports by name +const ERRNO_NUMBER: Record = { + EPERM: 1, ENOENT: 2, EIO: 5, EBADF: 9, EAGAIN: 11, ENOMEM: 12, EACCES: 13, ENOTDIR: 20, EINVAL: 22, ENFILE: 23, + EMFILE: 24, ENOSPC: 28, EROFS: 30, ENAMETOOLONG: 36, ELOOP: 40, ENOTSUP: 95, EOPNOTSUPP: 95, EDQUOT: 122, +}; function intPair(t: JThread, a: number, b: number): JArray { const arr = t.vm.newArray(t.vm.primClass("I"), 2); @@ -381,7 +385,7 @@ export const natives: Natives = { const abs = resolvePath(t, t.vm.jsString(name)); if (!t.vm.fs.stat(abs)) return ERRNO_NUMBER.ENOENT; const ok = t.vm.fs.chmod(abs, (m) => (m & ~0o7777) | (mode & 0o7777)); - return ok === false ? ERRNO_NUMBER[t.vm.fs.chmodError(abs) ?? "EPERM"] ?? ERRNO_NUMBER.EPERM : 0; + return ok === false ? ERRNO_NUMBER[t.vm.fs.chmodError(abs) ?? "EIO"] ?? ERRNO_NUMBER.EIO : 0; }, // [fd, errno]; fd 0 when the file could not be opened or locked "java/util/prefs/FileSystemPreferences.lockFile0(Ljava/lang/String;IZ)[I": (t, [name, , shared]) => { diff --git a/test/java/Prefs.java b/test/java/Prefs.java index 71fb43e..8cc16a9 100644 --- a/test/java/Prefs.java +++ b/test/java/Prefs.java @@ -7,9 +7,12 @@ // User preferences are stored in files under java.util.prefs.userRoot, which // FileSystemPreferences locks and chmods as on HotSpot. public class Prefs { + // Its log lines carry the time; HotSpot logs the same ones. Held here, since + // the log manager keeps loggers weakly and a collected one forgets its level. + static final Logger PREFS_LOG = Logger.getLogger("java.util.prefs"); + public static void main(String[] args) throws Exception { - // its log lines carry the time; HotSpot logs the same ones - Logger.getLogger("java.util.prefs").setLevel(Level.OFF); + PREFS_LOG.setLevel(Level.OFF); // a fresh root, not the user's own preferences Path root = Files.createTempDirectory("prefs"); System.setProperty("java.util.prefs.userRoot", root.toString()); From 04505ed0a901e1e6669e5c1b6d64e68d336efad8 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:23:27 -0600 Subject: [PATCH 3/3] Give a prefs lock file its requested mode, and check what reached the backing file in the test --- src/natives/io.ts | 4 +++- test/java/Prefs.java | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/natives/io.ts b/src/natives/io.ts index fae9313..ce6571c 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -388,7 +388,7 @@ export const natives: Natives = { return ok === false ? ERRNO_NUMBER[t.vm.fs.chmodError(abs) ?? "EIO"] ?? ERRNO_NUMBER.EIO : 0; }, // [fd, errno]; fd 0 when the file could not be opened or locked - "java/util/prefs/FileSystemPreferences.lockFile0(Ljava/lang/String;IZ)[I": (t, [name, , shared]) => { + "java/util/prefs/FileSystemPreferences.lockFile0(Ljava/lang/String;IZ)[I": (t, [name, mode, shared]) => { const abs = resolvePath(t, t.vm.jsString(name)); let fd: number; try { @@ -396,6 +396,8 @@ export const natives: Natives = { } catch { return intPair(t, 0, t.vm.fs.stat(abs) ? ERRNO_NUMBER.EACCES : ERRNO_NUMBER.ENOENT); } + // the requested mode, as HotSpot's fchmod gives it; a failure is ignored there too + if (!shared) t.vm.fs.chmod(abs, (m) => (m & ~0o7777) | (mode & 0o7777)); const { held, fds } = prefsLocks(t); const now = held.get(abs) ?? 0; if (shared ? now < 0 : now !== 0) { diff --git a/test/java/Prefs.java b/test/java/Prefs.java index 8cc16a9..4d456db 100644 --- a/test/java/Prefs.java +++ b/test/java/Prefs.java @@ -23,6 +23,13 @@ public static void main(String[] args) throws Exception { node.flush(); node.sync(); System.out.println(node.get("greeting", "?") + " " + node.getInt("count", 0) + " " + String.join(",", node.keys())); + // what reached the backing file, read apart from the cached node + try (var files = Files.walk(root)) { + for (Path f : files.filter(f -> f.endsWith(Path.of("java-web-test", "prefs.xml"))).toList()) { + System.out.println(root.relativize(f)); + for (String line : Files.readAllLines(f)) if (line.contains("