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
7 changes: 7 additions & 0 deletions docs/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,16 @@ try {
rejection; call a setter to wait for the write.
- Overloads are chosen by how well each argument fits the parameter type, then
varargs as a last resort. Pass an array or the elements for a varargs parameter.
Instance calls expose public methods. Static calls also expose package-private
and protected helpers used by native callbacks; private methods are excluded.
- Java strings are JS strings. Boxed `Integer`, `Double`, `Boolean` and the
other numbers are JS primitives, and a JS number passed as an `Object` becomes
an `Integer` when it is whole and a `Double` otherwise. Any other object is a proxy.
- Java `long` and boxed `Long` results are JS numbers, following
[CheerpJ's LiveConnect conversion rules](https://cheerpj.com/docs/reference/CJ3Library).
Values outside the JS safe-integer range can lose precision. Return a string
or `long[]` (a `BigInt64Array`) when every bit must be preserved. JS `bigint`
arguments can be passed to Java `long` parameters without losing precision.
- Arrays are copies. A primitive array is a typed array (`Int32Array` for `int[]`,
`Int8Array` for `byte[]`, `Uint8Array` for `boolean[]`) and a reference array is
a JS array. A typed array you pass to a call is updated with what Java wrote
Expand Down
7 changes: 6 additions & 1 deletion docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ java-web targets headless Java: console apps, libraries and javac.
drawing on a `BufferedImage`, and JavaBeans. So does `java.xml`, including
XML Schema validation and DTDs from the JDK's built-in catalog.
- `jrt:/` URLs read the JDK's resources but not its class files.
- `Deflater` writes valid DEFLATE streams using stored blocks. Compression
levels and strategies currently do not reduce output size; compressed bytes
and compression ratios differ from HotSpot's zlib output.
- Not supported: GUI apps (AWT and Swing windows), `ConvolveOp`, child
processes, and native libraries other than JS natives.

Expand Down Expand Up @@ -66,7 +69,9 @@ Locales other than English need `cheerpjInit({ localeData: true })`, or
`javaProperties` sets its own), and that proxy turns each request into a
`fetch`. The browser's rules apply: CORS, and no `http:` requests from an
`https:` page. A failed fetch is a 502 response. Redirects are followed by
`fetch`, and a response body is read whole before Java sees it. Raw sockets
`fetch`, even when Java disables redirect following: a browser's manual
redirect response hides the status and `Location` header. A response body is
read whole before Java sees it. Raw sockets
to other hosts still fail.
- The browser does the TLS for `https:`. The proxy answers `CONNECT host:port`
and then reads plain HTTP in the tunnel, and `fetch` does the TLS and checks
Expand Down
21 changes: 21 additions & 0 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface Stat {
dir: boolean;
size: number;
mtime: number;
// permission and special bits, when the backend can report them
mode?: number;
// a character device; it streams and is never read whole
device?: boolean;
}
Expand All @@ -14,6 +16,8 @@ export const DEVICES = new Set(["/dev/null", "/dev/zero", "/dev/random", "/dev/u

export interface Backend {
readonly writable: boolean;
// A host backend can follow symlinks before resolving parent components.
resolvePath?(path: string): string | null;
stat(path: string): Stat | null;
read(path: string): Uint8Array | null;
// bytes [off, off + len) of a file, for backends that can read part of one
Expand All @@ -31,6 +35,7 @@ export interface Backend {
denied?(path: string): boolean;
// sets the permission bits to f(current bits)
chmod?(path: string, f: (mode: number) => number): boolean;
chmodError?(path: string): string | undefined;
// whether the host allows reading (4) and executing (1), as access(2)
access?(path: string, mode: number): boolean;
}
Expand All @@ -51,6 +56,17 @@ export class VFS {
private mounts: { prefix: string; backend: Backend }[] = [];
private hidden: string[] = [];

resolvePath(path: string): string {
const normalized = normalize(path);
if (!path.includes("/..")) return normalized;
const r = this.resolve(normalized);
if (!r?.[0].resolvePath) return normalized;
const prefix = r[1] === "/" && normalized !== "/" ? normalized : normalized.slice(0, normalized.length - r[1].length);
const raw = prefix === "" ? path : path.startsWith(prefix + "/") ? path.slice(prefix.length) : r[1];
const resolved = r[0].resolvePath(raw);
return resolved === null ? normalized : normalize(prefix + resolved);
}

mount(prefix: string, backend: Backend) {
prefix = normalize(prefix);
this.mounts.push({ prefix, backend });
Expand Down Expand Up @@ -185,6 +201,11 @@ export class VFS {
return r[0].writable && r[0].chmod(r[1], f);
}

chmodError(path: string): string | undefined {
const r = this.resolve(path);
return r?.[0].chmodError?.(r[1]);
}

// access(2) on an existing path: mode is 4 read, 2 write, 1 execute
access(path: string, mode: number): boolean {
if (DEVICES.has(normalize(path))) return true;
Expand Down
13 changes: 11 additions & 2 deletions src/indy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,11 +465,20 @@ export function* constantObject(vm: JVM, t: JThread, cls: JClass, idx: number):
case Tag.Class: return vm.mirror(yield* vm.loadClassGen(t, cls.loader, className(cp, idx)));
}
const cached = cls.ldc[idx];
if (cached !== undefined && e.tag !== Tag.Dynamic) return cached;
if (cached !== undefined) {
if (e.tag !== Tag.Dynamic) return cached;
const desc = utf8(cp, cp[e.b]!.b);
return isPrim(desc) ? yield* box(t, desc, cached) : cached;
}
let v: any;
if (e.tag === Tag.MethodType) v = yield* methodType(vm, t, cls.loader, utf8(cp, e.a));
else if (e.tag === Tag.MethodHandle) v = yield* methodHandleConstant(vm, t, cls, cpArg(cls, idx) as MethodHandleRef);
else if (e.tag === Tag.Dynamic) return yield* dynamicConstant(vm, t, cls, idx);
else if (e.tag === Tag.Dynamic) {
v = yield* dynamicConstant(vm, t, cls, idx);
const desc = utf8(cp, cp[e.b]!.b);
cls.ldc[idx] = isPrim(desc) && v !== null ? vm.getField(v, v.cls.name, "value") : v;
return v;
}
else throw new PendingThrow("java/lang/InternalError", `bad constant tag ${e.tag}`);
cls.ldc[idx] = v;
return v;
Expand Down
53 changes: 50 additions & 3 deletions src/interp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,10 @@ export function raise(t: JThread, exc: JObject) {
return;
}
const h = findHandler(t.vm, f, exc);
if (h < -1) {
t.pushNative(resolveHandler(t, f, exc, -h - 2), null, "V", true);
return;
}
if (h >= 0) {
if (t.frames.length < MAX_FRAMES - 500) t.stackLimit = MAX_FRAMES;
f.sp = 0;
Expand All @@ -632,18 +636,61 @@ export function raise(t: JThread, exc: JObject) {
}
}

// Negative values below -1 identify an unresolved handler's table index.
function findHandler(vm: JVM, f: Frame, exc: JObject): number {
const handlers = f.m.code!.handlers;
const pc = f.pc;
for (const h of handlers) {
for (let i = 0; i < handlers.length; i++) {
const h = handlers[i];
if (pc < h.start || pc >= h.end) continue;
if (h.catchType === 0) return h.handler;
const name = vm.cpClassName(f.m.cls, h.catchType);
for (let c: JClass | null = exc.cls; c; c = c.superclass) if (c.name === name) return h.handler;
const cls = f.m.cls;
const name = vm.cpClassName(cls, h.catchType);
const self = name === cls.cf?.name || name === cls.cf?.renamedFrom;
const target = cls.rcp[h.catchType] ?? (self ? cls : vm.findLoaded(cls.loader, name) ?? (cls.loader === null ? vm.loadBoot(name) : null));
if (!target) return -i - 2;
cls.rcp[h.catchType] = target;
if (exc.cls.isSubclassOf(target)) return h.handler;
}
return -1;
}

function* resolveHandler(t: JThread, f: Frame, exc: JObject, start: number): NativeGen {
const cls = f.m.cls;
const handlers = f.m.code!.handlers;
for (let i = start; i < handlers.length; i++) {
const h = handlers[i];
if (f.pc < h.start || f.pc >= h.end) continue;
if (h.catchType !== 0) {
try {
const name = t.vm.cpClassName(cls, h.catchType);
const self = name === cls.cf?.name || name === cls.cf?.renamedFrom;
const target = cls.rcp[h.catchType] ?? (self ? cls : yield* t.vm.loadClassGen(t, cls.loader, name));
cls.rcp[h.catchType] = target;
if (!exc.cls.isSubclassOf(target)) continue;
} catch (e) {
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
if (e instanceof JavaThrow) exc = e.obj;
else if (e instanceof PendingThrow) exc = yield* t.vm.newThrowable(t, e.cls, e.msg, null, e.ctor);
else throw e;
// HotSpot restarts lookup at the failing handler's bytecode position.
f.pc = h.handler;
i = -1;
continue;
}
}
if (t.frames.length < MAX_FRAMES - 500) t.stackLimit = MAX_FRAMES;
f.sp = 1;
f.stack[0] = exc;
f.pc = h.handler;
return;
}
// Unwind the throwing frame while keeping this native frame on top.
const resolver = t.frames.pop()!;
popFrame(t, f);
t.frames.push(resolver);
throw new JavaThrow(exc);
}

export function handleError(t: JThread, e: unknown) {
if (e instanceof JavaThrow) raise(t, e.obj);
else if (e instanceof PendingThrow) t.vm.throwNew(t, e.cls, e.msg, e.ctor, e.native);
Expand Down
3 changes: 2 additions & 1 deletion src/jit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ function generate(t: JThread, m: JMethod): { src: string; consts: any[] } {
out.push(`default:return rt.viaInterpreter(t,f);}}catch(e){`);
if (g.regions.length) out.push(deoptCatch(g));
out.push(`if(e instanceof rt.JavaThrow){const h=H.findHandler(t,f,pc,e.obj);if(h>=0){${S(0)}=e.obj;pc=h;continue;}`);
out.push(`if(h< -1){x=pc*${exitCode(1, 0, 0)}+${EXIT_ERROR};xe=e;if(f.direct)f.direct=false;break X;}`);
out.push(`H.popFrame(t,f);if(f.direct)throw e;H.raise(t,e.obj);return 1;}`);
out.push(`x=(e instanceof rt.PendingThrow&&e.pc!==undefined?e.pc:pc)*${exitCode(1, 0, 0)}+${EXIT_ERROR};xe=e;if(f.direct)f.direct=false;break X;}}}`);
// the shared exit: spill into the frame's arrays, then leave as x says
Expand Down Expand Up @@ -884,7 +885,7 @@ function insn(g: Gen, b: Body, pc: number) {
}
case 0xba: {
const site = m.cache?.[pc];
if (!site || region) { emit(slow(pc, d)); break; }
if (!site || site instanceof PendingThrow || region) { emit(slow(pc, d)); break; }
let dd = d;
if (site.appendix !== undefined) { emit(`${S(dd)}=${K(site.appendix)};`); dd++; }
emit(genInvoke(g, 0xb8, pc, next, dd, site.m, cls, d));
Expand Down
15 changes: 14 additions & 1 deletion src/jvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,22 @@ export class JVM {
const cache = f.m.cache ??= [];
const pc = f.pc;
const site = cache[pc];
if (site instanceof PendingThrow) throw site;
if (site !== undefined) return site;
t.pushNative((function* (vm: JVM) {
cache[pc] = yield* linkCallSite(vm, t, f.m.cls, idx);
try {
cache[pc] = yield* linkCallSite(vm, t, f.m.cls, idx);
} catch (e) {
if (e instanceof JavaThrow && vm.isInstance(e.obj, "java/lang/LinkageError")) {
const cls = e.obj.cls;
const detail = vm.getField(e.obj, "java/lang/Throwable", "detailMessage");
// HotSpot recreates cached bootstrap errors without their original cause.
cache[pc] = new PendingThrow(cls.name, detail === null ? null : vm.jsString(detail));
} else if (e instanceof PendingThrow && vm.loadBoot(e.cls)?.isAssignableTo(vm.loadBootOrFail("java/lang/LinkageError"))) {
cache[pc] = e;
}
throw e;
}
})(this), null, "V", true);
return undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion src/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export class Library {
for (const c of classes) {
for (const m of c.methods) {
if (m.name !== name || m.isStatic !== isStatic || m.access & ACC_SYNTHETIC || seen.has(m.key)) continue;
if (!isStatic && !(m.access & ACC_PUBLIC)) continue;
if (m.isPrivate || (!isStatic && !(m.access & ACC_PUBLIC))) continue;
seen.add(m.key);
out.push(m);
}
Expand Down
52 changes: 45 additions & 7 deletions src/natives/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import { JArray, JObject } from "../runtime.ts";
import { JThread, PendingThrow } from "../thread.ts";
import type { NativeGen } from "../thread.ts";
import { normalize } from "../fs.ts";
import type { VFS } from "../fs.ts";
import type { Stat, VFS } from "../fs.ts";
import { closeSockFd } from "../sockets.ts";
import { nonNull, u8 } from "./util.ts";
import type { Natives } from "./util.ts";
Expand All @@ -23,6 +22,8 @@ export class OpenFile {
pos = 0;
writable: boolean;
contents: Contents;
ino?: number;
detached?: { path: string; stat: Stat; writable: boolean };
constructor(path: string, data: Uint8Array, writable: boolean, contents?: Contents) {
this.path = path;
this.contents = contents ?? { data, size: data.length, dirty: false };
Expand Down Expand Up @@ -124,14 +125,27 @@ function table(t: JThread): FdTable {
return tab;
}

export function fileInode(t: JThread, path: string): number {
let inodes = t.vm.state.get("inodes") as Map<string, number> | undefined;
if (!inodes) t.vm.state.set("inodes", (inodes = new Map()));
const ino = inodes.get(path);
if (ino === undefined) {
const next = (t.vm.state.get("inodeNext") as number | undefined) ?? 2;
inodes.set(path, next);
t.vm.state.set("inodeNext", next + 1);
return next;
}
return ino;
}

// rmdir fails on a path ending in "." or "..": "." stays the working directory
export function endsInDot(p: string): boolean {
const last = p.replace(/\/+$/, "").split("/").pop();
return last === "." || last === "..";
}

export function resolvePath(t: JThread, p: string): string {
return normalize(p.startsWith("/") ? p : `${t.vm.opts.cwd ?? "/"}/${p}`);
return t.vm.fs.resolvePath(p.startsWith("/") ? p : `${t.vm.opts.cwd ?? "/"}/${p}`);
}

function notFound(path: string, why = "No such file or directory"): never {
Expand Down Expand Up @@ -245,20 +259,44 @@ export function renamePath(t: JThread, from: string, to: string): boolean {
const files = [...table(t).files.values()];
const under = (f: OpenFile, p: string) => f.path === p || f.path.startsWith(p + "/");
const open = files.filter((f) => under(f, from));
const replaced = files.filter((f) => !open.includes(f) && under(f, to));
for (const f of replaced) detachFile(t, f);
for (const f of open) flush(t, f);
if (!t.vm.fs.rename(from, to)) return false;
if (!t.vm.fs.rename(from, to)) {
for (const f of replaced) f.detached = undefined;
return false;
}
// a file the rename replaced stays open, unlinked
for (const f of files) if (!open.includes(f) && under(f, to)) f.path = "";
for (const f of replaced) f.path = "";
for (const f of open) f.path = to + f.path.slice(from.length);
const inodes = t.vm.state.get("inodes") as Map<string, number> | undefined;
if (inodes) {
const moved = [...inodes].filter(([p]) => p === from || p.startsWith(from + "/"));
for (const p of inodes.keys()) if (p === to || p.startsWith(to + "/")) inodes.delete(p);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
for (const [p] of moved) inodes.delete(p);
for (const [p, ino] of moved) inodes.set(to + p.slice(from.length), ino);
}
return true;
}

export function removePath(t: JThread, abs: string): boolean {
if (!t.vm.fs.remove(abs)) return false;
for (const f of table(t).files.values()) if (f.path === abs) f.path = "";
const open = [...table(t).files.values()].filter((f) => f.path === abs);
for (const f of open) detachFile(t, f);
if (!t.vm.fs.remove(abs)) {
for (const f of open) f.detached = undefined;
return false;
}
for (const f of open) f.path = "";
t.vm.state.get("inodes")?.delete(abs);
return true;
}

function detachFile(t: JThread, f: OpenFile) {
f.ino ??= fileInode(t, f.path);
const stat = t.vm.fs.stat(f.path);
if (stat) f.detached = { path: f.path, stat, writable: t.vm.fs.writable(f.path) };
}

function fdOf(t: JThread, stream: JObject, owner: string): number {
const fdObj = t.vm.getField(stream, owner, "fd") as JObject;
return t.vm.getField(fdObj, "java/io/FileDescriptor", "fd");
Expand Down
Loading
Loading