feat(fls): topology of a .fls file as Java loads it
readTopology maps the decoded JavaBeans graph to devices, cables and documentation items the way SzenarioVerwaltung.netzwerkLaden does: the kind from the Java class (the palette label is not file data), devices without bounds at 0,0, interface values through Java's setters. All 23 corpus files match Java field by field. Found on the way: - listItems mixed a constructor's elements with added ones; a router's second interface was lost. Lists now replay add/set in order after the elements the constructor made. - The first 0 of a file may be SwingConstants.CENTER; measured like the getter defaults. - IP.ipCheck (javaIp.ts) measured on 46 cases: empty tokens skipped, signs, leading zeros and BMP digits of any script accepted. - The Java oracle took lower-case MACs in a file for random ones (8 interfaces); it now compares case-insensitively and reports the host name flags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,12 @@
|
||||
// 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".
|
||||
// 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
|
||||
// <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)
|
||||
@@ -46,8 +47,12 @@ export interface JavaObject {
|
||||
readonly properties: Map<string, JavaValue>;
|
||||
/** Method calls in order (`add`, `put`, …), with their arguments. */
|
||||
readonly calls: { method: string; args: JavaValue[] }[];
|
||||
/** `set(i, v)` on the object. */
|
||||
readonly indexed: Map<number, JavaValue>;
|
||||
/**
|
||||
* 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>;
|
||||
/** True when the object was not in the file but returned by a getter. */
|
||||
readonly fromGetter: boolean;
|
||||
}
|
||||
@@ -97,14 +102,40 @@ export function simpleClassName(className: string): string {
|
||||
return className.slice(className.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
/** The elements of a list: `add` calls, then `set(i, v)` replacing by index. */
|
||||
export function listItems(value: JavaValue | undefined): JavaValue[] {
|
||||
/**
|
||||
* 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 = value.calls.filter((call) => call.method === 'add').map((call) => call.args.at(-1) ?? null);
|
||||
for (const [index, item] of value.indexed) items[index] = item;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -194,7 +225,7 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
|
||||
factoryMethod: fields.factoryMethod,
|
||||
properties: new Map(),
|
||||
calls: [],
|
||||
indexed: new Map(),
|
||||
presetElements: new Map(),
|
||||
fromGetter: fields.fromGetter ?? false,
|
||||
};
|
||||
}
|
||||
@@ -297,6 +328,12 @@ class Decoder {
|
||||
}
|
||||
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}`;
|
||||
if (Object.hasOwn(DEFAULTS, constantKey)) return this.primitive(element, DEFAULTS[constantKey]!);
|
||||
}
|
||||
const args = element.children.filter((child) => child.name !== 'void');
|
||||
const object = newObject({
|
||||
className,
|
||||
@@ -342,7 +379,7 @@ class Decoder {
|
||||
: this.setProperty(target, attributes.property, args[0]!);
|
||||
} else if (attributes.index !== undefined) {
|
||||
const index = Number.parseInt(attributes.index, 10);
|
||||
value = args.length === 0 ? this.getIndex(target, index) : this.setIndex(target, index, args[0]!);
|
||||
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);
|
||||
} else if (attributes.idref !== undefined) {
|
||||
@@ -374,7 +411,7 @@ class Decoder {
|
||||
}
|
||||
if (isJavaObject(target)) {
|
||||
target.calls.push({ method, args });
|
||||
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0]);
|
||||
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -405,17 +442,24 @@ class Decoder {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
private getIndex(target: JavaValue | undefined, index: number): JavaValue {
|
||||
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 existing = listItems(target)[index];
|
||||
const items = replayList(target, 0);
|
||||
const existing = items[index];
|
||||
if (existing !== undefined) return existing;
|
||||
// A list the file never filled, like a node's interface list created by
|
||||
// its constructor: the element is a placeholder that later reads return.
|
||||
const placeholder = newObject({ className: 'unknown', fromGetter: true });
|
||||
target.indexed.set(index, placeholder);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -423,7 +467,7 @@ class Decoder {
|
||||
if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') {
|
||||
(target as JavaArray).items[index] = value;
|
||||
} else if (isJavaObject(target)) {
|
||||
target.indexed.set(index, value);
|
||||
target.calls.push({ method: 'set', args: [index, value] });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user