Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ java-web targets headless Java: console apps, libraries and javac.

Locales other than English need `cheerpjInit({ localeData: true })`, or
`--locale-data` under Node (`localeData: true` for `runNode`). Adding
`jdk.localedata` or `ALL-SYSTEM` with `--add-modules` turns it on too. Turn it on
`jdk.localedata` or `ALL-SYSTEM` with `--add-modules` turns it on too, and a
`--limit-modules` list that leaves `jdk.localedata` out turns it off. Turn it on
when a program formats numbers, dates or currencies, or prints month and day
names, for a locale other than English: for example
`String.format(Locale.GERMANY, ...)`, or a German default locale. Without it,
Expand Down
39 changes: 33 additions & 6 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export interface Stat {
dir: boolean;
size: number;
mtime: number;
// the modification time in nanoseconds, when it was set with more precision
// than mtime keeps
mtimeNs?: bigint;
// permission and special bits, when the backend can report them
mode?: number;
// a character device; it streams and is never read whole
Expand Down Expand Up @@ -54,6 +57,8 @@ export function normalize(path: string): string {
}

export class VFS {
// path -> the time a VM set, and the mtime the backend reported right after
private setTimes = new Map<string, { ms: number; ns: bigint }>();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
private mounts: { prefix: string; backend: Backend }[] = [];
private hidden: string[] = [];

Expand Down Expand Up @@ -113,7 +118,12 @@ export class VFS {
if (this.hidden.length && this.isHidden(n)) return null;
const r = this.resolve(path);
const s = r ? r[0].stat(r[1]) : null;
if (s) return s;
if (s) {
const set = this.setTimes.get(n);
if (set?.ms === s.mtime) return { ...s, mtimeNs: set.ns };
if (set) this.setTimes.delete(n);
return s;
}
if (normalize(path) === "/" || this.mountsUnder(path).length > 0 || this.isMountPoint(path)) {
return { dir: true, size: 0, mtime: 0 };
}
Expand Down Expand Up @@ -170,23 +180,40 @@ export class VFS {

remove(path: string): boolean {
const r = this.resolve(path);
return !!r && r[0].writable && !!r[0].remove?.(r[1]);
if (!r || !r[0].writable || !r[0].remove?.(r[1])) return false;
this.setTimes.delete(normalize(path));
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
return true;
}

rename(from: string, to: string): boolean {
const a = this.resolve(from), b = this.resolve(to);
if (!a || !b || a[0] !== b[0] || !a[0].writable) return false;
return !!a[0].rename?.(a[1], b[1]);
if (!a || !b || a[0] !== b[0] || !a[0].writable || !a[0].rename?.(a[1], b[1])) return false;
// set times move with the files, which keep their times
const src = normalize(from), dst = normalize(to);
const under = (p: string, root: string) => p === root || p.startsWith(root + "/");
const moved = [...this.setTimes].filter(([p]) => under(p, src));
for (const p of [...this.setTimes.keys()]) if (under(p, src) || under(p, dst)) this.setTimes.delete(p);
for (const [p, set] of moved) this.setTimes.set(dst + p.slice(src.length), set);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
return true;
}

writable(path: string): boolean {
const r = this.resolve(path);
return !!r && r[0].writable;
}

setMtime(path: string, ms: number): boolean {
// ns, when given, is the exact time asked for. Hosts keep times as floating
// point seconds or milliseconds, which lose nanoseconds, so stat reports ns
// for as long as the file keeps the time this set.
setMtime(path: string, ms: number, ns?: bigint): boolean {
const r = this.resolve(path);
return !!r && r[0].writable && !!r[0].setMtime?.(r[1], ms);
if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms)) return false;
const n = normalize(path);
const st = ns === undefined ? null : r[0].stat(r[1]);
// only a time the host kept, give or take its rounding; a host can clamp it
if (st && Math.abs(st.mtime - ms) < 1) this.setTimes.set(n, { ms: st.mtime, ns: ns! });
else this.setTimes.delete(n);
return true;
}

denied(path: string): boolean {
Expand Down
14 changes: 10 additions & 4 deletions src/natives/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ export function syncPath(t: JThread, abs: string) {
}

// Pending writes go out first, so closing the file later does not change the time again.
export function setMtime(t: JThread, abs: string, ms: number): boolean {
export function setMtime(t: JThread, abs: string, ms: number, ns?: bigint): boolean {
syncPath(t, abs);
return t.vm.fs.setMtime(abs, ms);
return t.vm.fs.setMtime(abs, ms, ns);
}

export function fileOf(t: JThread, fd: number): OpenFile {
Expand Down Expand Up @@ -452,7 +452,13 @@ export const natives: Natives = {
const abs = resolvePath(t, pathOf(t, f));
return !!t.vm.fs.stat(abs) && t.vm.fs.access(abs, access);
},
[`${UFS}.getLastModifiedTime0(Ljava/io/File;)J`]: (t, [, f]) => BigInt(Math.floor(t.vm.fs.stat(resolvePath(t, pathOf(t, f)))?.mtime ?? 0)),
[`${UFS}.getLastModifiedTime0(Ljava/io/File;)J`]: (t, [, f]) => {
const st = t.vm.fs.stat(resolvePath(t, pathOf(t, f)));
if (st?.mtimeNs === undefined) return BigInt(Math.floor(st?.mtime ?? 0));
// down, as HotSpot divides a negative time
const ms = st.mtimeNs / 1_000_000n;
return st.mtimeNs < 0n && ms * 1_000_000n !== st.mtimeNs ? ms - 1n : ms;
},
[`${UFS}.getLength0(Ljava/io/File;)J`]: (t, [, f]) => {
const abs = resolvePath(t, pathOf(t, f));
syncPath(t, abs);
Expand All @@ -477,7 +483,7 @@ export const natives: Natives = {
[`${UFS}.createDirectory0(Ljava/io/File;)Z`]: (t, [, f]) => t.vm.fs.mkdir(resolvePath(t, pathOf(t, f))),
[`${UFS}.rename0(Ljava/io/File;Ljava/io/File;)Z`]: (t, [, a, b]) =>
renamePath(t, resolvePath(t, pathOf(t, a)), resolvePath(t, pathOf(t, b))),
[`${UFS}.setLastModifiedTime0(Ljava/io/File;J)Z`]: (t, [, f, ms]) => setMtime(t, resolvePath(t, pathOf(t, f)), Number(ms)),
[`${UFS}.setLastModifiedTime0(Ljava/io/File;J)Z`]: (t, [, f, ms]) => setMtime(t, resolvePath(t, pathOf(t, f)), Number(ms), BigInt(ms) * 1_000_000n),
[`${UFS}.setReadOnly0(Ljava/io/File;)Z`]: (t, [, f]) => chmod(t, f, (m) => m & ~0o222),
[`${UFS}.getSpace0(Ljava/io/File;I)J`]: () => 0n,
[`${UFS}.getNameMax0(Ljava/lang/String;)J`]: () => 255n,
Expand Down
12 changes: 9 additions & 3 deletions src/natives/nio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ function fillAttrs(t: JThread, attrs: JObject, p: string): number {
function fillStat(t: JThread, attrs: JObject, st: Stat, writable: boolean, ino: number, size = st.size, nlink = 1) {
const vm = t.vm, A = "sun/nio/fs/UnixFileAttributes";
const mode = st.device ? S_IFCHR | 0o666 : (st.dir ? S_IFDIR : S_IFREG) | (st.mode ?? (st.dir ? (writable ? 0o755 : 0o555) : (writable ? 0o644 : 0o444)));
const sec = BigInt(Math.floor(st.mtime / 1000)), nsec = BigInt(Math.floor((st.mtime % 1000) * 1e6));
let sec = BigInt(Math.floor(st.mtime / 1000)), nsec = BigInt(Math.floor((st.mtime % 1000) * 1e6));
if (st.mtimeNs !== undefined) {
sec = st.mtimeNs / 1_000_000_000n;
nsec = st.mtimeNs % 1_000_000_000n;
// before 1970: whole seconds round down, nanoseconds stay positive
if (nsec < 0n) [sec, nsec] = [sec - 1n, nsec + 1_000_000_000n];
}
vm.setField(attrs, A, "st_mode", mode);
vm.setField(attrs, A, "st_ino", BigInt(ino));
vm.setField(attrs, A, "st_dev", 1n);
Expand Down Expand Up @@ -215,11 +221,11 @@ export const natives: Natives = {
[`${N}utimensat0(IJJJI)V`]: (t, [, p, , mtime]) => {
const abs = path(t, p);
if (!t.vm.fs.stat(abs)) unixError(ENOENT);
if (!setMtime(t, abs, Number(mtime) / 1e6)) unixError(EROFS);
if (!setMtime(t, abs, Number(mtime) / 1e6, BigInt(mtime))) unixError(EROFS);
},
[`${N}futimens0(IJJ)V`]: (t, [fd, , mtime]) => {
const f = fileOf(t, fd);
if (!setMtime(t, f.path, Number(mtime) / 1e6)) unixError(EROFS);
if (!setMtime(t, f.path, Number(mtime) / 1e6, BigInt(mtime))) unixError(EROFS);
},
[`${N}opendir0(J)J`]: (t, [p]) => {
const abs = path(t, p);
Expand Down
38 changes: 38 additions & 0 deletions test/java/FileTimes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import java.io.File;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.concurrent.TimeUnit;

// A modification time set to the nanosecond reads back the same, as HotSpot
// gives it on a file system that keeps nanoseconds.
public class FileTimes {
public static void main(String[] args) throws Exception {
Path dir = Files.createTempDirectory("times");
Path file = Files.createFile(dir.resolve("f"));
try {
long[] times = { 1762300000123456789L, 1762300000999999999L, 1000000000000000001L };
for (long ns : times) {
FileTime t = FileTime.from(ns, TimeUnit.NANOSECONDS);
Files.setLastModifiedTime(file, t);
FileTime back = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
System.out.println(ns + " " + back.equals(t) + " " + back.to(TimeUnit.NANOSECONDS));
}
// the directory's time, as java/nio/file/attribute/BasicFileAttributeView/Basic.java copies it
FileTime dirTime = Files.readAttributes(dir, BasicFileAttributes.class).lastModifiedTime();
Files.getFileAttributeView(file, BasicFileAttributeView.class).setTimes(dirTime, null, null);
System.out.println("dir time " + Files.getLastModifiedTime(file).equals(dirTime));
// the exact time moves with the file
FileTime exact = FileTime.from(1762300000123456789L, TimeUnit.NANOSECONDS);
Files.setLastModifiedTime(file, exact);
Path moved = Files.move(file, dir.resolve("g"));
System.out.println("moved " + Files.getLastModifiedTime(moved).equals(exact));
Files.move(moved, file);
File f = file.toFile();
f.setLastModified(1762300000123L);
System.out.println("File " + f.lastModified() + " " + Files.getLastModifiedTime(file).toMillis());
} finally {
Files.delete(file);
Files.delete(dir);
}
}
}
Loading