Files
mdg-filius/packages/fls/src/topologyWriter.ts
T
julian.kiedaischandClaude Opus 5.5 ccec734143 feat(fls): write the installed applications of a .fls, loaded by Java as the original
topologyWriter.ts puts each installed application of a PC or notebook under
its key and gives it the system software, as installApp does, with its
settings: a server's switch and port, a client's target, DNS recursion,
virtual hosts, the peer count, the personal firewall. The applications
follow the files, as in Java's own files: the web server, the peer-to-peer
application and the e-mail server make their directories only where the
files lack them, and the DNS server reads /dns/hosts when it is installed.
E-mail accounts, messages and address books are the next step.

The Java oracle now reports a personal firewall's rules in full, not their
number (the corpus has none; its result is byte-equal).

Checked by Java against Java: every corpus file, read and written again,
loads with the same applications as the original. The synthetic topology
adds every FILIUS application with settings away from its defaults, a
personal firewall with a rule, an own index.html (Java makes no second) and
a missing /peer2peer directory, which Java makes at the end. 12 mutation
probes, among them files and applications in reverse order, each loaded by
Java, were all caught.

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

416 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Writes `projekt/konfiguration.xml` from Java's view of a project — the
// counterpart of topology.ts and, for the system software, of
// systemSoftware.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.
//
// The system software is the one the node's constructor makes; its settings
// are set through getters. A router's or home router's firewall is reached
// the way XMLEncoder reaches it: through the web server's `konfig` plug-in,
// which holds the same object (its key among the installed applications
// differs: Firewall on a router, NatGateway on a home router). Files are
// added to the constructor's root, as XMLEncoder adds them — on a router
// after the files its constructor makes. The installed applications of a PC
// or notebook follow the files, as in Java's own files: the constructors of
// the web server, the peer-to-peer application and the e-mail server make
// their directories only where the files lack them, and the DNS server reads
// /dns/hosts when it is installed. Each is put under its key and given the
// system software, as installApp does. E-mail accounts, messages and address
// books are a later layer.
//
// Checked by Java: every corpus file, read and written again, loads in Java
// FILIUS to the same topology and system software (topologyWriter.test.ts,
// tools/java-oracle).
import {
encodeJavaBeans,
int,
long,
object,
reference,
type BeanObject,
type BeanReference,
type BeanStatement,
type BeanValue,
short,
} from './javaBeansWriter.js';
import type { FlsApplication, FlsFileTree, FlsFirewall, FlsSystemSoftware } from './systemSoftware.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)';
/** An installed application; e-mail accounts, messages and address books are a later layer. */
export type ApplicationToWrite = Omit<FlsApplication, 'source'>;
/** The system software's settings, files and applications. */
export type SystemToWrite = Omit<FlsSystemSoftware, 'applications'> & {
readonly applications?: readonly ApplicationToWrite[];
};
export type NodeToWrite = Pick<
FlsNode,
'javaClass' | 'name' | 'x' | 'y' | 'useIpAsName' | 'useMacAsName' | 'interfaces' | 'portCount'
> & { readonly system: SystemToWrite };
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 };
},
),
);
}
// Named for the applications, which are given it.
const system = (node.system.applications?.length ?? 0) > 0 ? reference('SystemSoftware') : undefined;
const systemStatementList = systemStatements(node.system, node.javaClass, system);
statements.push(
system === undefined
? { get: 'systemSoftware', statements: systemStatementList }
: { get: 'systemSoftware', statements: systemStatementList, as: system },
);
return object(`filius.hardware.knoten.${node.javaClass}`, statements);
}
const WEB_SERVER_CLASS = 'filius.software.www.WebServer';
/**
* The settings the file has, each set on the constructor's system software.
* A switch's SSID the file lacked stays unwritten, so Java rolls one as it did.
*/
function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass, self: BeanReference | undefined): BeanStatement[] {
const statements: BeanStatement[] = [];
if (system.firewall !== undefined) {
statements.push({
get: 'installierteAnwendungen',
statements: [
{
call: 'get',
args: [WEB_SERVER_CLASS],
statements: [
{ get: 'plugins', statements: [{ call: 'get', args: ['konfig'], statements: [{ get: 'firewall', statements: firewallStatements(system.firewall) }] }] },
],
},
],
});
}
if (system.fileSystem !== undefined) statements.push(fileSystemStatement(system.fileSystem, system.templateFiles !== false));
if (self !== undefined && system.applications !== undefined) {
statements.push({
get: 'installierteAnwendungen',
statements: system.applications.map((application) => ({
call: 'put',
args: [application.key, object(application.javaClass, applicationStatements(application, self))],
})),
});
}
if (system.dhcpServer !== undefined) {
const server = system.dhcpServer;
statements.push({
get: 'DHCPServer',
statements: [
{ set: 'aktiv', value: server.isActive },
{ set: 'untergrenze', value: server.lowerBound },
{ set: 'obergrenze', value: server.upperBound },
{ set: 'gatewayip', value: server.gateway },
{ set: 'dnsserverip', value: server.dnsServer },
{ set: 'ownSettings', value: server.useOwnSettings },
{
set: 'staticAssignedAddresses',
value: object(
'java.util.ArrayList',
server.staticAssignedAddresses.map(({ mac, ip }) => ({ call: 'add', args: [`${mac} ${ip}`] })),
),
},
],
});
}
if (system.dhcpClient !== undefined) statements.push({ set: 'DHCPKonfiguration', value: system.dhcpClient });
if (system.manualRoutes !== undefined) {
statements.push({ get: 'weiterleitungstabelle', statements: [{ set: 'manuelleTabelle', value: rows(system.manualRoutes) }] });
}
if (system.ripEnabled !== undefined) statements.push({ set: 'ripEnabled', value: system.ripEnabled });
if (system.retentionTime !== undefined) statements.push({ set: 'retentionTime', value: long(system.retentionTime) });
if (system.staticNAT !== undefined) statements.push({ set: 'staticNAT', value: rows(system.staticNAT) });
if (system.modem !== undefined) {
statements.push(
{ set: 'mode', value: int(system.modem.mode) },
{ set: 'port', value: int(system.modem.port) },
{ set: 'ipAdresse', value: system.modem.ipAddress },
);
}
// SwitchFirmware.setSSID, Betriebssystem.setSsid.
if (system.ssid !== undefined) statements.push({ set: javaClass === 'Switch' ? 'SSID' : 'ssid', value: system.ssid });
return statements;
}
/**
* The file system: the root's children added to the constructor's root
* (named "root", empty on a PC or notebook), or a root of its own that
* replaces it with the files a router's constructor makes. `arbeitsVerzeichnis`
* is the property Java writes; `root` names the same field.
*/
function fileSystemStatement(root: FlsFileTree, constructedRoot: boolean): BeanStatement {
if (!constructedRoot) return { get: 'dateisystem', statements: [{ set: 'arbeitsVerzeichnis', value: treeNode(root) }] };
const statements: BeanStatement[] = [];
if (!(root.kind === 'directory' && root.name === 'root')) statements.push({ set: 'userObject', value: userObject(root) });
statements.push(...childStatements(root));
return { get: 'dateisystem', statements: [{ get: 'arbeitsVerzeichnis', statements }] };
}
function childStatements(tree: FlsFileTree): BeanStatement[] {
return tree.children.map((child) => ({ call: 'add', args: [treeNode(child)] }));
}
/** A DefaultMutableTreeNode; one of another kind than directory or file is written without a user object. */
function treeNode(tree: FlsFileTree): BeanObject {
return object('javax.swing.tree.DefaultMutableTreeNode', childStatements(tree), tree.kind === 'other' ? [] : [userObject(tree)]);
}
/** A directory's name, or a file as `new Datei(name, typ, inhalt)`. */
function userObject(tree: FlsFileTree): BeanValue {
if (tree.kind === 'file') return object('filius.software.system.Datei', [], [tree.name, tree.type, tree.content]);
return tree.kind === 'directory' ? tree.name : null;
}
/**
* An application's settings, after the system software it runs on. A
* peer-to-peer application's peer count the file lacked stays unwritten, so
* Java rolls one as it did.
*/
function applicationStatements(application: ApplicationToWrite, system: BeanReference): BeanStatement[] {
const statements: BeanStatement[] = [{ set: 'systemSoftware', value: system }];
if (application.isActive !== undefined) statements.push({ set: 'aktiv', value: application.isActive });
if (application.port !== undefined) statements.push({ set: 'port', value: int(application.port) });
if (application.targetAddress !== undefined) statements.push({ set: 'zielIPAdresse', value: application.targetAddress });
if (application.targetPort !== undefined) statements.push({ set: 'zielPort', value: int(application.targetPort) });
if (application.recursiveResolutionEnabled !== undefined) {
statements.push({ set: 'recursiveResolutionEnabled', value: application.recursiveResolutionEnabled });
}
if (application.useVirtualHosts !== undefined) statements.push({ set: 'useVHost', value: application.useVirtualHosts });
if (application.maxPeers !== undefined && application.maxPeers !== null) {
statements.push({ set: 'maxTeilnehmerZahl', value: int(application.maxPeers) });
}
if (application.firewall !== undefined) statements.push(...firewallStatements(application.firewall));
return statements;
}
/** A LinkedList of String arrays: routes, static NAT. */
function rows(table: readonly (readonly (string | null)[])[]): BeanObject {
return listOf(table.map((row) => ({ kind: 'array', componentClass: 'java.lang.String', items: [...row] })));
}
function firewallStatements(firewall: FlsFirewall): BeanStatement[] {
return [
{ set: 'activated', value: firewall.activated },
{ set: 'defaultPolicy', value: short(firewall.defaultPolicy) },
{ set: 'dropICMP', value: firewall.dropICMP },
{ set: 'filterSYNSegmentsOnly', value: firewall.filterSYNSegmentsOnly },
{ set: 'filterUdp', value: firewall.filterUdp },
{
set: 'ruleset',
value: object(
'java.util.Vector',
firewall.rules.map((rule) => ({
call: 'add',
args: [
object('filius.software.firewall.FirewallRule', [], [
rule.srcIP,
rule.srcMask,
rule.destIP,
rule.destMask,
int(rule.port),
short(rule.protocol),
short(rule.action),
]),
],
})),
),
},
];
}
/**
* 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);
}