A project's own applications (classes under projekt/anwendungen, installed on a host) are no longer left out. The decoder now keeps the Java type of every primitive — at setters, constructor and call arguments, also through shared ids like Integer0 — and which properties an object's own getter made. beanCapture.ts turns a decoded bean back into what the writer writes, with the node's system software as a mark the writer replaces by the system software it writes; what the format cannot say is refused, not guessed (an unresolved value, a Field, a factory-made object, an object a getter reaches elsewhere in the file). The reader keeps own applications as captured beans; the corpus no longer warns. The capture first took a setter with another object's getter result for an own getter; the round-trip test found it. The Java oracle did not show whether an application runs on its node's system software, so a mutation writing the mark instead survived; it now reports onItsSystem (true for all 192 applications of the corpus). Checked by Java: from the files written again, FILIUS loads Accumulator and Broadcast installed, switched on and on their node's system software, as in the original. The round-trip test covers every primitive, null, a class, an array, shared objects, getter chains, constructed elements, map reads and static constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
614 lines
26 KiB
TypeScript
614 lines
26 KiB
TypeScript
// Reads the output of java.beans.XMLEncoder — the format of
|
|
// `projekt/konfiguration.xml` — into a graph of generic values, following the
|
|
// rules of java.beans.XMLDecoder. Nothing is instantiated and no Java class
|
|
// is known here: an object is its class name, its constructor arguments, the
|
|
// properties set on it and the methods called on it. The mapping to the
|
|
// FILIUS model is a separate layer (topology.ts).
|
|
//
|
|
// New relative to filius-ipad, whose reader works element by element and
|
|
// cannot follow values XMLEncoder shares by reference (plan §13, M2):
|
|
// - `id`/`idref` resolve anywhere, including the results of getters;
|
|
// - a getter on a property nobody set answers the value Java would, from
|
|
// measured defaults (javaDefaults.json), or an explicit "unresolved";
|
|
// - static constants (`SwingConstants.CENTER`) are their measured values.
|
|
//
|
|
// XMLDecoder's rules, as far as XMLEncoder uses them:
|
|
// <object class="C">args… statements…</object> new C(args), then the statements
|
|
// <object class="C" field="F"/> the static field C.F (measured value if known)
|
|
// <object class="C" method="m">args</object> the static factory C.m(args)
|
|
// <object idref="X"/> the value stored under id X
|
|
// <void property="p"/> target.getP() (a value)
|
|
// <void property="p">value</void> target.setP(value)
|
|
// <void index="i"/> / <void index="i">v</void> target.get(i) / target.set(i, v)
|
|
// <void method="m">args</void> target.m(args)
|
|
// <void class="C" method="m">args</void> C.m(args) (static)
|
|
// id="X" on <object> or <void> store the value under X
|
|
// statements nested in a <void> apply to that void's value
|
|
|
|
import { SaxesParser } from 'saxes';
|
|
import javaDefaults from './javaDefaults.json' with { type: 'json' };
|
|
|
|
// ── Values ─────────────────────────────────────────────────────────────────
|
|
|
|
export type JavaValue = null | string | number | bigint | boolean | JavaObject | JavaArray | JavaField | JavaClass | Unresolved;
|
|
|
|
export interface JavaObject {
|
|
readonly kind: 'object';
|
|
/** Fully qualified where the file names it; the simple name for objects only known by id. */
|
|
readonly className: string;
|
|
readonly id: string | undefined;
|
|
/** Constructor arguments, or the factory method's arguments. */
|
|
readonly args: JavaValue[];
|
|
/** `C.F` for a static field constant. */
|
|
readonly staticField: string | undefined;
|
|
/** `C.m(args)` for an object made by a static factory. */
|
|
readonly factoryMethod: string | undefined;
|
|
/** Set through setters, `Field.set` or created lazily by a getter. */
|
|
readonly properties: Map<string, JavaValue>;
|
|
/** The Java type of each property set to a primitive (`int`, `long`, …): numbers alone do not say. */
|
|
readonly propertyKinds: Map<string, PrimitiveKind>;
|
|
/**
|
|
* The properties whose value this object's own getter made (a placeholder
|
|
* the file's statements then changed) — not set to an object from
|
|
* elsewhere.
|
|
*/
|
|
readonly propertiesFromGetters: Set<string>;
|
|
/** The Java types of the constructor's arguments, where they are primitives. */
|
|
readonly argKinds: (PrimitiveKind | undefined)[];
|
|
/** Method calls in order (`add`, `put`, …), with their arguments and those arguments' primitive types. */
|
|
readonly calls: { method: string; args: JavaValue[]; kinds?: (PrimitiveKind | undefined)[] }[];
|
|
/**
|
|
* Elements the object already held before the file's statements — a list
|
|
* its constructor filled, reached by `<void index="i">` on a getter's
|
|
* result. `add`/`set` go to `calls`, in order.
|
|
*/
|
|
readonly presetElements: Map<number, JavaValue>;
|
|
/**
|
|
* The same for a map: entries its constructor made (a router's installed
|
|
* applications), reached by `<void method="get"><string>key</string>`.
|
|
*/
|
|
readonly presetEntries: Map<string, JavaValue>;
|
|
/** True when the object was not in the file but returned by a getter. */
|
|
readonly fromGetter: boolean;
|
|
}
|
|
|
|
/** The primitive types XMLEncoder writes as elements of their own. */
|
|
export type PrimitiveKind = 'boolean' | 'byte' | 'char' | 'short' | 'int' | 'long' | 'float' | 'double';
|
|
|
|
const PRIMITIVE_TAGS = new Set<string>(['boolean', 'byte', 'char', 'short', 'int', 'long', 'float', 'double']);
|
|
|
|
/** A wrapper's id prefix, as XMLEncoder names a shared value (Integer0), and its primitive. */
|
|
const KIND_OF_ID_PREFIX: Readonly<Record<string, PrimitiveKind>> = {
|
|
Boolean: 'boolean',
|
|
Byte: 'byte',
|
|
Character: 'char',
|
|
Short: 'short',
|
|
Integer: 'int',
|
|
Long: 'long',
|
|
Float: 'float',
|
|
Double: 'double',
|
|
};
|
|
|
|
export interface JavaArray {
|
|
readonly kind: 'array';
|
|
readonly componentClass: string;
|
|
readonly id: string | undefined;
|
|
readonly items: JavaValue[];
|
|
}
|
|
|
|
/** A `java.lang.reflect.Field`, as `Class.getField(name)` returns it. */
|
|
export interface JavaField {
|
|
readonly kind: 'field';
|
|
readonly className: string;
|
|
readonly name: string;
|
|
}
|
|
|
|
export interface JavaClass {
|
|
readonly kind: 'class';
|
|
readonly name: string;
|
|
}
|
|
|
|
/** A value Java would know and this reader cannot: never a guess, always said so. */
|
|
export interface Unresolved {
|
|
readonly kind: 'unresolved';
|
|
readonly reason: string;
|
|
}
|
|
|
|
export class JavaBeansError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'JavaBeansError';
|
|
}
|
|
}
|
|
|
|
export function isJavaObject(value: JavaValue | undefined): value is JavaObject {
|
|
return typeof value === 'object' && value !== null && (value as { kind?: string }).kind === 'object';
|
|
}
|
|
|
|
export function isUnresolved(value: JavaValue | undefined): value is Unresolved {
|
|
return typeof value === 'object' && value !== null && (value as { kind?: string }).kind === 'unresolved';
|
|
}
|
|
|
|
/** `java.awt.Rectangle` → `Rectangle`. */
|
|
export function simpleClassName(className: string): string {
|
|
return className.slice(className.lastIndexOf('.') + 1);
|
|
}
|
|
|
|
/**
|
|
* The elements of a list: the ones it held before the file's statements,
|
|
* then `add`, `add(i, v)` and `set(i, v)` replayed in order.
|
|
*
|
|
* `constructed` is how many elements the list's constructor made — a Java
|
|
* `Switch` starts with 24 ports, a `Gateway` with two interfaces. The file
|
|
* only mentions those it changed; the rest come back as untouched objects.
|
|
*/
|
|
export function listItems(value: JavaValue | undefined, constructed = 0): JavaValue[] {
|
|
if (typeof value === 'object' && value !== null && (value as JavaArray).kind === 'array') {
|
|
return [...(value as JavaArray).items];
|
|
}
|
|
if (!isJavaObject(value)) return [];
|
|
const items = replayList(value, constructed);
|
|
return Array.from(items, (item) => item ?? newObject({ className: 'unknown', fromGetter: true }));
|
|
}
|
|
|
|
/** Like listItems, with `undefined` for a constructed element nobody touched. */
|
|
function replayList(value: JavaObject, constructed: number): (JavaValue | undefined)[] {
|
|
const presetLength = Math.max(constructed, ...Array.from(value.presetElements.keys(), (index) => index + 1));
|
|
const items: (JavaValue | undefined)[] = Array.from({ length: presetLength }, (_, index) =>
|
|
value.presetElements.get(index),
|
|
);
|
|
for (const { method, args } of value.calls) {
|
|
if (method === 'add' && args.length === 1) {
|
|
items.push(args[0]!);
|
|
} else if (method === 'add' && args.length === 2 && typeof args[0] === 'number') {
|
|
items.splice(args[0], 0, args[1]!);
|
|
} else if (method === 'set' && args.length === 2 && typeof args[0] === 'number') {
|
|
items[args[0]] = args[1]!;
|
|
} else if (method === 'clear' && args.length === 0) {
|
|
items.length = 0;
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/**
|
|
* The entries of a map: the ones its constructor made, then `put` calls in
|
|
* order — a later `put` of the same key replaces the value in place.
|
|
*/
|
|
export function mapEntries(value: JavaValue | undefined): [JavaValue, JavaValue][] {
|
|
if (!isJavaObject(value)) return [];
|
|
const entries: [JavaValue, JavaValue][] = [...value.presetEntries];
|
|
for (const call of value.calls) {
|
|
if (call.method !== 'put') continue;
|
|
const key = call.args[0] ?? null;
|
|
const existing = entries.find(([entryKey]) => entryKey === key);
|
|
if (existing === undefined) entries.push([key, call.args[1] ?? null]);
|
|
else existing[1] = call.args[1] ?? null;
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
// ── XML tree ───────────────────────────────────────────────────────────────
|
|
|
|
interface Element {
|
|
readonly name: string;
|
|
readonly attributes: Readonly<Record<string, string>>;
|
|
readonly children: Element[];
|
|
/** Text directly inside, including `<char>` resolved — only read for text elements. */
|
|
text: string;
|
|
/** Text and `<char>` children in document order, for `<string>`. */
|
|
readonly parts: (string | Element)[];
|
|
}
|
|
|
|
function parseXml(text: string): Element {
|
|
const parser = new SaxesParser({ xmlns: false, position: true });
|
|
const root: Element = { name: '#document', attributes: {}, children: [], text: '', parts: [] };
|
|
const stack: Element[] = [root];
|
|
parser.on('opentag', (tag) => {
|
|
const element: Element = {
|
|
name: tag.name,
|
|
attributes: tag.attributes as Record<string, string>,
|
|
children: [],
|
|
text: '',
|
|
parts: [],
|
|
};
|
|
const parent = stack.at(-1)!;
|
|
parent.children.push(element);
|
|
parent.parts.push(element);
|
|
stack.push(element);
|
|
});
|
|
parser.on('text', (value) => {
|
|
const current = stack.at(-1)!;
|
|
current.text += value;
|
|
current.parts.push(value);
|
|
});
|
|
parser.on('closetag', () => {
|
|
stack.pop();
|
|
});
|
|
parser.on('error', (error) => {
|
|
throw new JavaBeansError(`XML: ${error.message}`);
|
|
});
|
|
parser.write(text).close();
|
|
const java = root.children[0];
|
|
if (java === undefined || java.name !== 'java') throw new JavaBeansError('The root element is not <java>.');
|
|
return java;
|
|
}
|
|
|
|
/** The bytes as text, in the encoding the XML declaration names (UTF-8 otherwise). */
|
|
export function decodeXmlBytes(bytes: Uint8Array): string {
|
|
const head = new TextDecoder('latin1').decode(bytes.subarray(0, 200));
|
|
const declared = /<\?xml[^>]*encoding=["']([A-Za-z0-9._-]+)["']/.exec(head)?.[1] ?? 'utf-8';
|
|
return new TextDecoder(declared.toLowerCase(), { fatal: true }).decode(bytes);
|
|
}
|
|
|
|
// ── Decoder ────────────────────────────────────────────────────────────────
|
|
|
|
const PRIMITIVE_ID_PREFIXES = new Set(['Integer', 'Boolean', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Character']);
|
|
type MeasuredValue = string | number | boolean | null;
|
|
/** A value a constructor rolls at random: measured as the set of values seen, never a default. */
|
|
type RandomValue = { random: string[] };
|
|
const MEASURED = javaDefaults as Readonly<Record<string, MeasuredValue | RandomValue>>;
|
|
|
|
/** The measured constant under `key`; undefined if nothing was measured or Java rolls it. */
|
|
function measuredConstant(key: string): MeasuredValue | undefined {
|
|
if (!Object.hasOwn(MEASURED, key)) return undefined;
|
|
const value = MEASURED[key]!;
|
|
return typeof value === 'object' && value !== null ? undefined : value;
|
|
}
|
|
|
|
function isRandomInJava(key: string): boolean {
|
|
const value = MEASURED[key];
|
|
return typeof value === 'object' && value !== null;
|
|
}
|
|
|
|
/**
|
|
* What a getter returns on a freshly constructed object of `simpleClass`, as
|
|
* measured (javaDefaults.json) — undefined where nothing was measured or
|
|
* where Java's constructor rolls the value.
|
|
*/
|
|
export function javaDefault(simpleClass: string, property: string): string | number | boolean | null | undefined {
|
|
return measuredConstant(`${simpleClass}#${property}`);
|
|
}
|
|
|
|
/** `getFoo`/`isFoo` → `foo`, as java.beans.Introspector derives it. */
|
|
function propertyOfGetter(method: string): string | undefined {
|
|
const match = /^(?:get|is)([A-Z].*)$/.exec(method);
|
|
if (match === null) return undefined;
|
|
const name = match[1]!;
|
|
return name.length > 1 && name[1] === name[1]!.toUpperCase() && name[1] !== name[1]!.toLowerCase()
|
|
? name
|
|
: name[0]!.toLowerCase() + name.slice(1);
|
|
}
|
|
|
|
function newObject(fields: Partial<JavaObject> & { className: string }): JavaObject {
|
|
return {
|
|
kind: 'object',
|
|
className: fields.className,
|
|
id: fields.id,
|
|
args: fields.args ?? [],
|
|
staticField: fields.staticField,
|
|
factoryMethod: fields.factoryMethod,
|
|
properties: new Map(),
|
|
propertyKinds: new Map(),
|
|
propertiesFromGetters: new Set(),
|
|
argKinds: [],
|
|
calls: [],
|
|
presetElements: new Map(),
|
|
presetEntries: new Map(),
|
|
fromGetter: fields.fromGetter ?? false,
|
|
};
|
|
}
|
|
|
|
class Decoder {
|
|
private readonly ids = new Map<string, JavaValue>();
|
|
/** The primitive type of each id that names a primitive value. */
|
|
private readonly idKinds = new Map<string, PrimitiveKind>();
|
|
|
|
/** The primitive type an argument element stands for, if any. */
|
|
private kindOf(element: Element): PrimitiveKind | undefined {
|
|
if (PRIMITIVE_TAGS.has(element.name)) return element.name as PrimitiveKind;
|
|
const idref = element.attributes.idref;
|
|
return idref === undefined ? undefined : this.idKinds.get(idref);
|
|
}
|
|
|
|
topLevel(java: Element): JavaValue[] {
|
|
return java.children.map((child) => this.expression(child));
|
|
}
|
|
|
|
private store(id: string | undefined, value: JavaValue): void {
|
|
if (id !== undefined) this.ids.set(id, value);
|
|
}
|
|
|
|
/** The value of an argument element. */
|
|
private expression(element: Element): JavaValue {
|
|
const attributes = element.attributes;
|
|
switch (element.name) {
|
|
case 'null':
|
|
return null;
|
|
case 'string': {
|
|
const value = element.parts
|
|
.map((part) => (typeof part === 'string' ? part : part.name === 'char' ? charValue(part) : ''))
|
|
.join('');
|
|
this.store(attributes.id, value);
|
|
return value;
|
|
}
|
|
case 'char': {
|
|
const value = charValue(element);
|
|
this.store(attributes.id, value);
|
|
return value;
|
|
}
|
|
case 'boolean':
|
|
return this.primitive(element, element.text.trim() === 'true');
|
|
case 'int':
|
|
case 'short':
|
|
case 'byte':
|
|
return this.primitive(element, Number.parseInt(element.text.trim(), 10));
|
|
case 'float':
|
|
case 'double':
|
|
return this.primitive(element, Number.parseFloat(element.text.trim()));
|
|
case 'long': {
|
|
const big = BigInt(element.text.trim());
|
|
return this.primitive(element, Number.isSafeInteger(Number(big)) ? Number(big) : big);
|
|
}
|
|
case 'class': {
|
|
const value: JavaClass = { kind: 'class', name: element.text.trim() };
|
|
this.store(attributes.id, value);
|
|
return value;
|
|
}
|
|
case 'array':
|
|
return this.array(element);
|
|
case 'object':
|
|
return this.object(element);
|
|
case 'void':
|
|
// A void in argument position is still a statement; XMLDecoder
|
|
// evaluates it for its side effects and its id.
|
|
this.statement(element, undefined);
|
|
return null;
|
|
default:
|
|
throw new JavaBeansError(`Unknown element <${element.name}>.`);
|
|
}
|
|
}
|
|
|
|
private primitive(element: Element, value: JavaValue): JavaValue {
|
|
this.store(element.attributes.id, value);
|
|
const id = element.attributes.id;
|
|
// An element of its own (<int>), or a constant named by its wrapper (Integer0).
|
|
const kind = PRIMITIVE_TAGS.has(element.name) ? (element.name as PrimitiveKind) : KIND_OF_ID_PREFIX[id?.replace(/\d+$/, '') ?? ''];
|
|
if (id !== undefined && kind !== undefined) this.idKinds.set(id, kind);
|
|
return value;
|
|
}
|
|
|
|
private array(element: Element): JavaArray {
|
|
const attributes = element.attributes;
|
|
const length = attributes.length === undefined ? undefined : Number.parseInt(attributes.length, 10);
|
|
const array: JavaArray = {
|
|
kind: 'array',
|
|
componentClass: attributes.class ?? 'java.lang.Object',
|
|
id: attributes.id,
|
|
items: length === undefined ? [] : Array.from({ length }, () => null),
|
|
};
|
|
this.store(attributes.id, array);
|
|
for (const child of element.children) {
|
|
if (child.name === 'void') {
|
|
this.statement(child, array);
|
|
} else {
|
|
array.items.push(this.expression(child));
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
private object(element: Element): JavaValue {
|
|
const attributes = element.attributes;
|
|
if (attributes.idref !== undefined) {
|
|
const value = this.ids.get(attributes.idref);
|
|
if (value === undefined && !this.ids.has(attributes.idref)) {
|
|
return { kind: 'unresolved', reason: `idref ${attributes.idref} is not defined` };
|
|
}
|
|
this.applyStatements(element.children, value ?? null);
|
|
return value ?? null;
|
|
}
|
|
const className = attributes.class;
|
|
if (className === undefined) throw new JavaBeansError('<object> without class or idref.');
|
|
if (attributes.field !== undefined) {
|
|
// A static constant: XMLEncoder writes a shared Integer 0 as
|
|
// SwingConstants.CENTER when that was the first 0 it met.
|
|
const constantKey = `${simpleClassName(className)}.${attributes.field}`;
|
|
const constant = measuredConstant(constantKey);
|
|
if (constant !== undefined) return this.primitive(element, constant);
|
|
}
|
|
const args = element.children.filter((child) => child.name !== 'void');
|
|
const object = newObject({
|
|
className,
|
|
id: attributes.id,
|
|
staticField: attributes.field,
|
|
factoryMethod: attributes.method,
|
|
});
|
|
this.store(attributes.id, object);
|
|
for (const arg of args) {
|
|
object.args.push(this.expression(arg));
|
|
object.argKinds.push(this.kindOf(arg));
|
|
}
|
|
this.applyStatements(
|
|
element.children.filter((child) => child.name === 'void'),
|
|
object,
|
|
);
|
|
return object;
|
|
}
|
|
|
|
private applyStatements(children: readonly Element[], target: JavaValue): void {
|
|
for (const child of children) {
|
|
if (child.name === 'void') this.statement(child, target);
|
|
}
|
|
}
|
|
|
|
/** Executes a `<void>` on `target`; returns its value (a getter's result, say). */
|
|
private statement(element: Element, target: JavaValue | undefined): JavaValue {
|
|
const attributes = element.attributes;
|
|
const args: JavaValue[] = [];
|
|
const kinds: (PrimitiveKind | undefined)[] = [];
|
|
const nested: Element[] = [];
|
|
for (const child of element.children) {
|
|
if (child.name === 'void') {
|
|
nested.push(child);
|
|
} else {
|
|
args.push(this.expression(child));
|
|
kinds.push(this.kindOf(child));
|
|
}
|
|
}
|
|
|
|
let value: JavaValue = null;
|
|
if (attributes.class !== undefined) {
|
|
value = this.staticCall(attributes.class, attributes.method ?? 'new', args, attributes.id);
|
|
} else if (attributes.property !== undefined) {
|
|
value =
|
|
args.length === 0
|
|
? this.getProperty(target, attributes.property, attributes.id)
|
|
: this.setProperty(target, attributes.property, args[0]!, kinds[0]);
|
|
} else if (attributes.index !== undefined) {
|
|
const index = Number.parseInt(attributes.index, 10);
|
|
value = args.length === 0 ? this.getIndex(target, index, attributes.id) : this.setIndex(target, index, args[0]!);
|
|
} else if (attributes.method !== undefined) {
|
|
value = this.call(target, attributes.method, args, attributes.id, kinds);
|
|
} else if (attributes.idref !== undefined) {
|
|
value = this.ids.get(attributes.idref) ?? null;
|
|
}
|
|
|
|
this.store(attributes.id, value);
|
|
// A getter's primitive result, shared by id: its wrapper names the type.
|
|
const prefix = attributes.id?.replace(/\d+$/, '');
|
|
const kind = prefix === undefined ? undefined : KIND_OF_ID_PREFIX[prefix];
|
|
if (kind !== undefined && (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean')) {
|
|
this.idKinds.set(attributes.id!, kind);
|
|
}
|
|
this.applyStatements(nested, value);
|
|
return value;
|
|
}
|
|
|
|
private staticCall(className: string, method: string, args: JavaValue[], id: string | undefined): JavaValue {
|
|
if (method === 'getField' && typeof args[0] === 'string') {
|
|
const field: JavaField = { kind: 'field', className, name: args[0] };
|
|
return field;
|
|
}
|
|
return newObject({ className, id, args, factoryMethod: method });
|
|
}
|
|
|
|
private call(
|
|
target: JavaValue | undefined,
|
|
method: string,
|
|
args: JavaValue[],
|
|
id: string | undefined,
|
|
kinds: (PrimitiveKind | undefined)[] = [],
|
|
): JavaValue {
|
|
if (typeof target === 'object' && target !== null && (target as JavaField).kind === 'field') {
|
|
const field = target as JavaField;
|
|
if (method === 'set' && args.length === 2) return this.setProperty(args[0]!, field.name, args[1]!, kinds[1]);
|
|
if (method === 'get' && args.length === 1) return this.getProperty(args[0]!, field.name, id);
|
|
}
|
|
if (args.length === 0) {
|
|
const property = propertyOfGetter(method);
|
|
if (property !== undefined) return this.getProperty(target, property, id);
|
|
}
|
|
if (isJavaObject(target)) {
|
|
target.calls.push(kinds.some((kind) => kind !== undefined) ? { method, args, kinds } : { method, args });
|
|
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
|
|
if (method === 'get' && args.length === 1 && typeof args[0] === 'string') return this.getEntry(target, args[0], id);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private setProperty(target: JavaValue | undefined, property: string, value: JavaValue, kind?: PrimitiveKind): JavaValue {
|
|
if (isJavaObject(target)) {
|
|
target.properties.set(property, value);
|
|
target.propertiesFromGetters.delete(property);
|
|
if (kind === undefined) target.propertyKinds.delete(property);
|
|
else target.propertyKinds.set(property, kind);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A getter: the value set before, else what Java returns for an untouched
|
|
* object (javaDefaults.json), else — for an object whose statements follow —
|
|
* a placeholder object that later getters return again.
|
|
*/
|
|
private getProperty(target: JavaValue | undefined, property: string, id: string | undefined): JavaValue {
|
|
if (!isJavaObject(target)) {
|
|
return { kind: 'unresolved', reason: `getter ${property} on a value that is not an object` };
|
|
}
|
|
if (target.properties.has(property)) return target.properties.get(property)!;
|
|
const defaultKey = `${simpleClassName(target.className)}#${property}`;
|
|
const measured = measuredConstant(defaultKey);
|
|
if (measured !== undefined) return measured;
|
|
if (isRandomInJava(defaultKey)) return { kind: 'unresolved', reason: `${defaultKey} is random in Java` };
|
|
const prefix = id?.replace(/\d+$/, '');
|
|
if (prefix !== undefined && PRIMITIVE_ID_PREFIXES.has(prefix)) {
|
|
return { kind: 'unresolved', reason: `no measured default for ${defaultKey}` };
|
|
}
|
|
const placeholder = newObject({ className: prefix ?? 'unknown', id, fromGetter: true });
|
|
target.properties.set(property, placeholder);
|
|
target.propertiesFromGetters.add(property);
|
|
return placeholder;
|
|
}
|
|
|
|
private getIndex(target: JavaValue | undefined, index: number, id: string | undefined): JavaValue {
|
|
if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') {
|
|
return (target as JavaArray).items[index] ?? null;
|
|
}
|
|
if (!isJavaObject(target)) return { kind: 'unresolved', reason: `index ${index} on a value that is not an object` };
|
|
const items = replayList(target, 0);
|
|
const existing = items[index];
|
|
if (existing !== undefined) return existing;
|
|
// An element the list held before the file touched it, like a node's
|
|
// interface list created by its constructor: a placeholder that later
|
|
// reads return. XMLEncoder writes these before any `add`; after one, the
|
|
// index could mean either, and this reader does not guess.
|
|
const changed = target.calls.some((call) => call.method === 'add' || call.method === 'set' || call.method === 'clear');
|
|
if (changed && index >= items.length) {
|
|
return { kind: 'unresolved', reason: `index ${index} after the list was changed` };
|
|
}
|
|
const placeholder = newObject({ className: id?.replace(/\d+$/, '') ?? 'unknown', id, fromGetter: true });
|
|
target.presetElements.set(index, placeholder);
|
|
return placeholder;
|
|
}
|
|
|
|
/**
|
|
* `map.get(key)`: the value put before, else an entry the map held before
|
|
* the file touched it — a placeholder that later reads return.
|
|
*/
|
|
private getEntry(target: JavaObject, key: string, id: string | undefined): JavaValue {
|
|
const put = mapEntries(target).find(([entryKey]) => entryKey === key);
|
|
if (put !== undefined) return put[1];
|
|
const placeholder = newObject({ className: id?.replace(/\d+$/, '') ?? 'unknown', id, fromGetter: true });
|
|
target.presetEntries.set(key, placeholder);
|
|
return placeholder;
|
|
}
|
|
|
|
private setIndex(target: JavaValue | undefined, index: number, value: JavaValue): JavaValue {
|
|
if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') {
|
|
(target as JavaArray).items[index] = value;
|
|
} else if (isJavaObject(target)) {
|
|
target.calls.push({ method: 'set', args: [index, value] });
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function charValue(element: Element): string {
|
|
const code = element.attributes.code;
|
|
if (code !== undefined) {
|
|
const numeric = code.startsWith('#') ? Number.parseInt(code.slice(1), 16) : Number.parseInt(code, 10);
|
|
return String.fromCharCode(numeric);
|
|
}
|
|
return element.text;
|
|
}
|
|
|
|
/**
|
|
* Decodes an XMLEncoder document into the objects `readObject()` would return,
|
|
* one per top-level element, in order.
|
|
*/
|
|
export function decodeJavaBeans(xml: string | Uint8Array): JavaValue[] {
|
|
const text = typeof xml === 'string' ? xml : decodeXmlBytes(xml);
|
|
return new Decoder().topLevel(parseXml(text));
|
|
}
|