// 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 `\n\n${body}\n\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 codes inside a string', () => { const values = decodeJavaBeans( doc(`Filius429007199254740993true 1.5ab`), ); 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(`gfx/pc.png PC`), ); 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(` `), ); const [first, second] = listItems(list); expect(first).toBe(second); }); it('sets fields through Rectangle.class.getField(name).set(rect, value)', () => { const [rectangle] = decodeJavaBeans( doc(` x 5 y 165 `), ); 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(` x y `), ); expect(isJavaObject(button)).toBe(true); expect(object(rectangle).properties.get('x')).toBe(4); expect(object(rectangle).properties.get('y')).toBe(true); }); it('keeps the Java type of each primitive a setter, a constructor or a call gets', () => { // A shared getter result, as in the corpus: a button's measured icon text gap. const [, bean] = decodeJavaBeans( doc(` 3x 7 0.5 2 7 1 no longer a number key1.0 `), ); expect(object(bean).argKinds).toEqual(['short', 'char']); expect(Object.fromEntries(object(bean).propertyKinds)).toEqual({ count: 'long', ratio: 'float', gap: 'int', shared: 'byte', again: 'byte' }); expect(object(bean).calls).toEqual([{ method: 'put', args: ['key', 1], kinds: [undefined, 'double'] }]); }); it('says "unresolved" for a shared getter value it has no measurement for', () => { const [, holder] = decodeJavaBeans( doc(` `), ); 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(` 10.0.0.1 0A:00:00:00:00:01 `), ); 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(` k1 b`), ); 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(` 128255 `), ); 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(` 10.0.0.1 `), ); 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(` `), ); 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(` filius.software.www.WebServer true filius.software.www.WebServer 8080 extra1 `), ); 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(` a c 1b C `), ); expect(listItems(list)).toEqual(['a', 'b', 'C']); }); it('does not guess what an index means once the list was changed', () => { const [, holder] = decodeJavaBeans( doc(` `), ); 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(` `), ); 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('')); 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))); }); } });