diff --git a/src/natives/io.ts b/src/natives/io.ts index 6c3941f..ce6571c 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -356,10 +356,74 @@ 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; +} +// 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); + (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) ?? "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, mode, 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); + } + // 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) { + 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..4d456db --- /dev/null +++ b/test/java/Prefs.java @@ -0,0 +1,41 @@ +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 { + // 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 { + 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()); + 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())); + // 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(" 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