feat(fls): write the topology of a .fls, loaded by Java as the original
topologyWriter.ts writes konfiguration.xml from Java's view of a project: the version line, the node items (label position, device with name, host label flags, interfaces or ports), the cable items and the documentation items. What a device's constructor makes is changed through getters, as XMLEncoder does, and more is added; fewer than it makes set the list anew. Cable ends name the port objects the getters return (the writer now names a getter's result, <void id="Port0" property="port"/>), and each cable carries the nodes the editor draws it between. Checked by Java against Java: every corpus file, read and written again, loads in Java FILIUS (the oracle's loading method, --sha256 ties its result to the written bytes) to the same devices, names, positions, interfaces, cables and documentation as the original; so does a synthetic topology with what the corpus never sets. The oracle and the reader now also report a cable's drawing ends (JCablePanel ziel1/ziel2): in the corpus the nodes it joins, for all 183 cables. Mutation probes rewrite the files and let Java load each mutant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
// Writes `projekt/konfiguration.xml` from Java's view of a project — the
|
||||
// counterpart of topology.ts. The four values Java's netzwerkLaden reads: the
|
||||
// version line, the list of GUIKnotenItem, of GUIKabelItem and of
|
||||
// GUIDocuItem. What a node's constructor makes (its interfaces, a switch's
|
||||
// ports) is reached through getters and changed there, as XMLEncoder does;
|
||||
// what the constructor does not make is added. Cable ends name the port
|
||||
// objects of the nodes, so the same cable joins the same ports when Java
|
||||
// loads the file.
|
||||
//
|
||||
// Checked by Java: every corpus file, read and written again, loads in Java
|
||||
// FILIUS to the same topology (topologyWriter.test.ts, tools/java-oracle).
|
||||
|
||||
import {
|
||||
encodeJavaBeans,
|
||||
int,
|
||||
object,
|
||||
reference,
|
||||
type BeanObject,
|
||||
type BeanReference,
|
||||
type BeanStatement,
|
||||
type BeanValue,
|
||||
} from './javaBeansWriter.js';
|
||||
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNode, FlsNodeClass } from './topology.js';
|
||||
|
||||
/** The version line of the files this writer makes: the format of FILIUS 2.14. */
|
||||
export const WRITTEN_VERSION_LINE = 'Filius version: 2.14.0 (mdg-filius)';
|
||||
|
||||
export type NodeToWrite = Pick<
|
||||
FlsNode,
|
||||
'javaClass' | 'name' | 'x' | 'y' | 'useIpAsName' | 'useMacAsName' | 'interfaces' | 'portCount'
|
||||
>;
|
||||
|
||||
export interface TopologyToWrite {
|
||||
readonly nodes: readonly NodeToWrite[];
|
||||
readonly links: readonly FlsLink[];
|
||||
readonly documentation: readonly FlsDocumentationItem[];
|
||||
}
|
||||
|
||||
/** What each device class's constructor makes, and whether it is a host. */
|
||||
const CONSTRUCTED: Record<FlsNodeClass, { count: number; local: boolean; host: boolean }> = {
|
||||
Rechner: { count: 1, local: false, host: true },
|
||||
Notebook: { count: 1, local: false, host: true },
|
||||
Vermittlungsrechner: { count: 1, local: false, host: false },
|
||||
Gateway: { count: 2, local: false, host: false },
|
||||
Switch: { count: 24, local: true, host: false },
|
||||
Modem: { count: 1, local: true, host: false },
|
||||
};
|
||||
|
||||
export function writeTopologyXml(topology: TopologyToWrite): string {
|
||||
return encodeJavaBeans(topologyBeans(topology));
|
||||
}
|
||||
|
||||
export function topologyBeans(topology: TopologyToWrite): BeanValue[] {
|
||||
const ports: BeanValue[][] = [];
|
||||
const items = topology.nodes.map((node, index) => {
|
||||
const nodePorts: BeanValue[] = [];
|
||||
ports[index] = nodePorts;
|
||||
return nodeItem(node, nodePorts);
|
||||
});
|
||||
const cables = topology.links.map((link) => cableItem(link, items, ports));
|
||||
return [
|
||||
WRITTEN_VERSION_LINE,
|
||||
listOf(items),
|
||||
listOf(cables),
|
||||
listOf(topology.documentation.map(documentationItem)),
|
||||
];
|
||||
}
|
||||
|
||||
function listOf(values: readonly BeanValue[]): BeanObject {
|
||||
return object(
|
||||
'java.util.LinkedList',
|
||||
values.map((value) => ({ call: 'add', args: [value] })),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A GUIKnotenItem: its label (the position) and its device; `ports`
|
||||
* receives the device's port objects. The label's size is Java's to work
|
||||
* out when it loads the file; the written 0 × 0 does not move it (measured
|
||||
* on the corpus).
|
||||
*/
|
||||
function nodeItem(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
||||
const label = object('filius.gui.netzwerksicht.JSidebarButton', [
|
||||
{ set: 'bounds', value: object('java.awt.Rectangle', [], [int(node.x), int(node.y), int(0), int(0)]) },
|
||||
]);
|
||||
return object('filius.gui.netzwerksicht.GUIKnotenItem', [
|
||||
{ set: 'imageLabel', value: label },
|
||||
{ set: 'knoten', value: device(node, ports) },
|
||||
]);
|
||||
}
|
||||
|
||||
function device(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
||||
const constructed = CONSTRUCTED[node.javaClass];
|
||||
const statements: BeanStatement[] = [{ set: 'name', value: node.name }];
|
||||
if (constructed.host) {
|
||||
statements.push({ set: 'useIPAsName', value: node.useIpAsName }, { set: 'useMACAsName', value: node.useMacAsName });
|
||||
}
|
||||
if (constructed.local) {
|
||||
statements.push(
|
||||
constructedList('anschluesse', node.portCount, constructed.count, ports, () => ({ statements: [] }), () => {
|
||||
const port = object('filius.hardware.Port');
|
||||
return { value: port, port };
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
constructedList(
|
||||
'netzwerkInterfaces',
|
||||
node.interfaces.length,
|
||||
constructed.count,
|
||||
ports,
|
||||
(index) => {
|
||||
const port = reference('Port');
|
||||
return { statements: [...interfaceStatements(node.interfaces[index]!), { get: 'port', statements: [], as: port }], port };
|
||||
},
|
||||
(index) => {
|
||||
const port = reference('Port');
|
||||
const nic = object('filius.hardware.NetzwerkInterface', [
|
||||
...interfaceStatements(node.interfaces[index]!),
|
||||
{ get: 'port', statements: [], as: port },
|
||||
]);
|
||||
return { value: nic, port };
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return object(`filius.hardware.knoten.${node.javaClass}`, statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list the constructor filled with `constructed` elements: those are
|
||||
* changed through `get(index)`, more are added. With fewer, the list is set
|
||||
* anew. `ports` receives each element's port, in order.
|
||||
*/
|
||||
function constructedList(
|
||||
property: string,
|
||||
count: number,
|
||||
constructed: number,
|
||||
ports: BeanValue[],
|
||||
existing: (index: number) => { statements: BeanStatement[]; port?: BeanReference },
|
||||
added: (index: number) => { value: BeanValue; port: BeanValue },
|
||||
): BeanStatement {
|
||||
if (count < constructed) {
|
||||
const values = Array.from({ length: count }, (_, index) => {
|
||||
const { value, port } = added(index);
|
||||
ports.push(port);
|
||||
return value;
|
||||
});
|
||||
return { set: property, value: listOf(values) };
|
||||
}
|
||||
const statements: BeanStatement[] = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (index < constructed) {
|
||||
// The element's own port, or — a switch's — the element itself.
|
||||
const { statements: inner, port } = existing(index);
|
||||
if (port === undefined) {
|
||||
const element = reference('Port');
|
||||
ports.push(element);
|
||||
statements.push({ getIndex: index, statements: inner, as: element });
|
||||
} else {
|
||||
ports.push(port);
|
||||
statements.push({ getIndex: index, statements: inner });
|
||||
}
|
||||
} else {
|
||||
const { value, port } = added(index);
|
||||
ports.push(port);
|
||||
statements.push({ call: 'add', args: [value] });
|
||||
}
|
||||
}
|
||||
return { get: property, statements };
|
||||
}
|
||||
|
||||
/**
|
||||
* A network interface's setters. An address Java's setter rejected (null)
|
||||
* is not written: setIp(null) throws, and the constructor's address stays.
|
||||
* A MAC the file lacked stays unwritten, so Java rolls one as it did.
|
||||
*/
|
||||
function interfaceStatements(nic: FlsInterface): BeanStatement[] {
|
||||
const statements: BeanStatement[] = [];
|
||||
if (nic.ip !== null) statements.push({ set: 'ip', value: nic.ip });
|
||||
if (nic.mask !== null) statements.push({ set: 'subnetzMaske', value: nic.mask });
|
||||
if (nic.mac !== undefined) statements.push({ set: 'mac', value: nic.mac });
|
||||
statements.push({ set: 'gateway', value: nic.gateway }, { set: 'dns', value: nic.dns });
|
||||
return statements;
|
||||
}
|
||||
|
||||
function cableItem(link: FlsLink, items: readonly BeanObject[], ports: readonly BeanValue[][]): BeanObject {
|
||||
const [first, second] = link.ends;
|
||||
const end = (reference: { node: number; port: number }) => ports[reference.node]![reference.port]!;
|
||||
const cable = object('filius.hardware.Kabel', [
|
||||
{ set: 'anschluesse', value: { kind: 'array', componentClass: 'filius.hardware.Port', items: [end(first), end(second)] } },
|
||||
...(link.wireless ? [{ set: 'wireless', value: true }] : []),
|
||||
]);
|
||||
return object('filius.gui.netzwerksicht.GUIKabelItem', [
|
||||
{ set: 'dasKabel', value: cable },
|
||||
{
|
||||
get: 'kabelpanel',
|
||||
statements: [
|
||||
{ set: 'ziel1', value: items[first.node]! },
|
||||
{ set: 'ziel2', value: items[second.node]! },
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function documentationItem(item: FlsDocumentationItem): BeanObject {
|
||||
const statements: BeanStatement[] = [];
|
||||
if (item.color !== null) statements.push({ set: 'color', value: object('java.awt.Color', [], item.color.map(int)) });
|
||||
if (item.font !== null) {
|
||||
statements.push({ set: 'font', value: object('java.awt.Font', [], [item.font.name, int(item.font.style), int(item.font.size)]) });
|
||||
}
|
||||
statements.push(
|
||||
{ set: 'height', value: int(item.height) },
|
||||
{ set: 'text', value: item.text },
|
||||
{ set: 'type', value: int(item.type) },
|
||||
{ set: 'width', value: int(item.width) },
|
||||
{ set: 'x', value: int(item.x) },
|
||||
{ set: 'y', value: int(item.y) },
|
||||
);
|
||||
return object('filius.gui.netzwerksicht.GUIDocuItem', statements);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user