Files
mdg-filius/packages/fls/test/javabeans.test.ts
T
julian.kiedaischandClaude Opus 5.5 8080e7dcb2 fix(fls): Gnutella's peer limit is random in Java, not a default
PeerToPeerAnwendung's constructor rolls its peer limit (3, 4 or 5). The
getter defaults measured it once each run — 4, then 5 — and c48d15d
committed 5 as a default. GetterDefaults now constructs every class 64
times and writes a value that varies as {"random": [...]}; the decoder
answers a reference to it as unresolved, and the topology layer reports
a peer limit the file does not set as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:12:14 +00:00

312 lines
14 KiB
TypeScript

// The XMLEncoder reader: one test per XMLDecoder rule, then the corpus —
// every node's class and position must be what Java FILIUS reads
// (corpus.java.json), including the files where the Swift reader loses nodes.
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
decodeJavaBeans,
isJavaObject,
isUnresolved,
listItems,
mapEntries,
readFlsArchive,
type JavaObject,
type JavaValue,
} from '../src/index.js';
function doc(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>\n<java version="17" class="java.beans.XMLDecoder">\n${body}\n</java>\n`;
}
function object(value: JavaValue | undefined): JavaObject {
if (!isJavaObject(value)) throw new Error(`not an object: ${JSON.stringify(value)}`);
return value;
}
describe('XMLDecoder rules', () => {
it('reads primitives and strings, including <char> codes inside a string', () => {
const values = decodeJavaBeans(
doc(`<string>Filius</string><int>42</int><long>9007199254740993</long><boolean>true</boolean>
<double>1.5</double><null/><string>a<char code="#9"/>b</string><string></string>`),
);
expect(values).toEqual(['Filius', 42, 9007199254740993n, true, 1.5, null, 'a\tb', '']);
});
it('creates an object with constructor arguments and properties', () => {
const [icon] = decodeJavaBeans(
doc(`<object class="javax.swing.ImageIcon"><string>gfx/pc.png</string>
<void property="description"><string>PC</string></void></object>`),
);
expect(object(icon).className).toBe('javax.swing.ImageIcon');
expect(object(icon).args).toEqual(['gfx/pc.png']);
expect(object(icon).properties.get('description')).toBe('PC');
});
it('shares a value by id and idref', () => {
const [list] = decodeJavaBeans(
doc(`<object class="java.util.LinkedList">
<void method="add"><object class="filius.hardware.Port" id="Port0"/></void>
<void method="add"><object idref="Port0"/></void>
</object>`),
);
const [first, second] = listItems(list);
expect(first).toBe(second);
});
it('sets fields through Rectangle.class.getField(name).set(rect, value)', () => {
const [rectangle] = decodeJavaBeans(
doc(`<object class="java.awt.Rectangle" id="Rectangle0">
<void class="java.awt.Rectangle" method="getField"><string>x</string>
<void method="set"><object idref="Rectangle0"/><int>5</int></void></void>
<void class="java.awt.Rectangle" method="getField"><string>y</string>
<void method="set"><object idref="Rectangle0"/><int>165</int></void></void>
</object>`),
);
expect(object(rectangle).properties.get('x')).toBe(5);
expect(object(rectangle).properties.get('y')).toBe(165);
});
it('answers a getter nobody set with the measured Java default, and shares it by id', () => {
const [button, rectangle] = decodeJavaBeans(
doc(`<object class="filius.gui.netzwerksicht.JSidebarButton">
<void id="Integer0" property="iconTextGap"/>
<void id="Boolean0" method="isEnabled"/>
</object>
<object class="java.awt.Rectangle" id="Rectangle0">
<void class="java.awt.Rectangle" method="getField"><string>x</string>
<void method="set"><object idref="Rectangle0"/><object idref="Integer0"/></void></void>
<void class="java.awt.Rectangle" method="getField"><string>y</string>
<void method="set"><object idref="Rectangle0"/><object idref="Boolean0"/></void></void>
</object>`),
);
expect(isJavaObject(button)).toBe(true);
expect(object(rectangle).properties.get('x')).toBe(4);
expect(object(rectangle).properties.get('y')).toBe(true);
});
it('says "unresolved" for a shared getter value it has no measurement for', () => {
const [, holder] = decodeJavaBeans(
doc(`<object class="com.example.Unknown"><void id="Integer0" property="size"/></object>
<object class="java.util.ArrayList"><void method="add"><object idref="Integer0"/></void></object>`),
);
const [item] = listItems(holder);
expect(isUnresolved(item)).toBe(true);
});
it('applies statements nested in a getter to the object the getter returns', () => {
const [node] = decodeJavaBeans(
doc(`<object class="filius.hardware.knoten.Rechner">
<void id="LinkedList0" property="netzwerkInterfaces">
<void id="NetzwerkInterface0" index="0">
<void property="ip"><string>10.0.0.1</string></void>
</void>
</void>
<void property="netzwerkInterfaces">
<void index="0"><void property="mac"><string>0A:00:00:00:00:01</string></void></void>
</void>
</object>`),
);
const interfaces = object(object(node).properties.get('netzwerkInterfaces'));
expect(interfaces.className).toBe('LinkedList');
expect(interfaces.fromGetter).toBe(true);
const nic = object(listItems(interfaces)[0]);
expect(nic.properties.get('ip')).toBe('10.0.0.1');
expect(nic.properties.get('mac')).toBe('0A:00:00:00:00:01');
});
it('reads static fields, static factories, maps and arrays', () => {
const [constant, manager, map, array] = decodeJavaBeans(
doc(`<object class="javax.swing.SwingConstants" field="LEFT"/>
<object class="javax.swing.ToolTipManager" method="sharedInstance"/>
<object class="java.util.HashMap"><void method="put"><string>k</string><int>1</int></void></object>
<array class="filius.hardware.Port" length="2"><void index="1"><string>b</string></void></array>`),
);
expect(object(constant)).toMatchObject({ className: 'javax.swing.SwingConstants', staticField: 'LEFT' });
expect(object(manager).factoryMethod).toBe('sharedInstance');
expect(mapEntries(map)).toEqual([['k', 1]]);
expect(listItems(array)).toEqual([null, 'b']);
});
it('answers a measured static constant with its value, which later references share', () => {
const [color, holder] = decodeJavaBeans(
doc(`<object class="java.awt.Color" id="Color0">
<object class="javax.swing.SwingConstants" field="CENTER" id="Integer0"/>
<int>128</int><int>255</int>
</object>
<object class="java.util.ArrayList"><void method="add"><object idref="Integer0"/></void></object>`),
);
expect(object(color).args).toEqual([0, 128, 255]);
expect(listItems(holder)).toEqual([0]);
});
it('keeps the elements a constructor made ahead of the ones the file adds', () => {
// A Java router starts with one interface; the file changes it by index
// and adds two more — three interfaces, in this order.
const [node] = decodeJavaBeans(
doc(`<object class="filius.hardware.knoten.Vermittlungsrechner">
<void id="LinkedList0" property="netzwerkInterfaces">
<void id="NetzwerkInterface0" index="0"><void property="ip"><string>10.0.0.1</string></void></void>
<void method="add"><object class="filius.hardware.NetzwerkInterface" id="NetzwerkInterface1"/></void>
<void method="add"><object class="filius.hardware.NetzwerkInterface" id="NetzwerkInterface2"/></void>
</void>
</object>`),
);
const interfaces = listItems(object(node).properties.get('netzwerkInterfaces'));
expect(interfaces.map((nic) => object(nic).id)).toEqual(['NetzwerkInterface0', 'NetzwerkInterface1', 'NetzwerkInterface2']);
expect(object(interfaces[0]).properties.get('ip')).toBe('10.0.0.1');
});
it('fills the constructed elements the file never mentions, in index order', () => {
// A switch has 24 ports; the file names the three that carry a cable,
// in any order.
const [ports] = decodeJavaBeans(
doc(`<object class="filius.hardware.knoten.Switch"><void id="LinkedList0" property="anschluesse">
<void id="Port9" index="1"/><void id="Port10" index="0"/><void id="Port11" index="2"/>
</void></object>`),
);
const list = object(ports).properties.get('anschluesse');
const items = listItems(list, 24);
expect(items).toHaveLength(24);
expect(items.slice(0, 3).map((port) => object(port).id)).toEqual(['Port10', 'Port9', 'Port11']);
expect(object(items[3])).toMatchObject({ className: 'unknown', fromGetter: true });
expect(listItems(list)).toHaveLength(3);
});
it("keeps the entries a constructor made in a map, reached by get(key), and shares them", () => {
// A router's operating system installs its web server in the constructor;
// the file reaches it through the map and changes it.
const [system] = decodeJavaBeans(
doc(`<object class="filius.software.system.VermittlungsrechnerBetriebssystem">
<void id="HashMap0" property="installierteAnwendungen">
<void id="WebServer0" method="get"><string>filius.software.www.WebServer</string>
<void property="aktiv"><boolean>true</boolean></void>
</void>
<void method="get"><string>filius.software.www.WebServer</string>
<void property="port"><int>8080</int></void>
</void>
<void method="put"><string>extra</string><int>1</int></void>
</void>
</object>`),
);
const entries = mapEntries(object(system).properties.get('installierteAnwendungen'));
expect(entries.map(([key]) => key)).toEqual(['filius.software.www.WebServer', 'extra']);
const server = object(entries[0]![1]);
expect(server).toMatchObject({ className: 'WebServer', id: 'WebServer0', fromGetter: true });
expect([...server.properties]).toEqual([
['aktiv', true],
['port', 8080],
]);
});
it('replays add at an index and set by index in order', () => {
const [list] = decodeJavaBeans(
doc(`<object class="java.util.ArrayList">
<void method="add"><string>a</string></void>
<void method="add"><string>c</string></void>
<void method="add"><int>1</int><string>b</string></void>
<void index="2"><string>C</string></void>
</object>`),
);
expect(listItems(list)).toEqual(['a', 'b', 'C']);
});
it('does not guess what an index means once the list was changed', () => {
const [, holder] = decodeJavaBeans(
doc(`<object class="filius.hardware.knoten.Rechner">
<void id="LinkedList0" property="netzwerkInterfaces">
<void method="add"><object class="filius.hardware.NetzwerkInterface"/></void>
<void id="NetzwerkInterface5" index="3"/>
</void>
</object>
<object class="java.util.ArrayList"><void method="add"><object idref="NetzwerkInterface5"/></void></object>`),
);
expect(isUnresolved(listItems(holder)[0])).toBe(true);
});
it('does not take a value Java rolls in its constructor as a default', () => {
// Gnutella's peer limit is 3, 4 or 5 at random (measured by constructing it 64 times).
const [, holder] = decodeJavaBeans(
doc(`<object class="filius.software.dateiaustausch.PeerToPeerAnwendung"><void id="Integer0" property="maxTeilnehmerZahl"/></object>
<object class="java.util.ArrayList"><void method="add"><object idref="Integer0"/></void></object>`),
);
const [item] = listItems(holder);
expect(item).toEqual({ kind: 'unresolved', reason: 'PeerToPeerAnwendung#maxTeilnehmerZahl is random in Java' });
});
it('reports an idref to an id that was never defined', () => {
const [value] = decodeJavaBeans(doc('<object idref="Missing0"/>'));
expect(isUnresolved(value)).toBe(true);
});
});
// ── Corpus ────────────────────────────────────────────────────────────────
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<
string,
{ nodes: { class: string; bounds: number[] | null }[] }
>;
function flsFiles(directory: string): string[] {
return readdirSync(directory)
.flatMap((name) => {
const path = join(directory, name);
return statSync(path).isDirectory() ? flsFiles(path) : name.endsWith('.fls') ? [path] : [];
})
.sort();
}
/** The GUIKnotenItem list: the first top-level list whose elements are node items. */
function nodeItems(values: JavaValue[]): JavaObject[] {
for (const value of values) {
const items = listItems(value);
if (items.length > 0 && isJavaObject(items[0]) && items[0].className.endsWith('GUIKnotenItem')) {
return items.map(object);
}
}
return [];
}
describe('corpus: node positions as Java reads them', () => {
for (const file of flsFiles(corpusRoot)) {
const name = relative(corpusRoot, file);
it(name, () => {
const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml);
const nodes = nodeItems(values);
const expected = javaView[name]!.nodes;
expect(nodes.map((item) => object(item.properties.get('knoten')).className.split('.').at(-1))).toEqual(
expected.map((node) => node.class),
);
// new Rectangle(x, y, width, height) in older files, fields set one by
// one in newer ones — the fields win, as they are set after construction.
// Only the position is compared: after loading, Swing sizes the label
// to its text, so Java's width and height are not the file's.
const positions = nodes.map((item) => {
const rectangle = object(object(item.properties.get('imageLabel')).properties.get('bounds'));
return ['x', 'y'].map((field, index) => rectangle.properties.get(field) ?? rectangle.args[index] ?? 0);
});
expect(positions).toEqual(expected.map((node) => node.bounds!.slice(0, 2)));
});
}
});