// 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('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: 'CENTER' }); expect(object(manager).factoryMethod).toBe('sharedInstance'); expect(mapEntries(map)).toEqual([['k', 1]]); expect(listItems(array)).toEqual([null, 'b']); }); 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))); }); } });