Files
mdg-filius/packages/fls/src/topologyWriter.ts
T
julian.kiedaischandClaude Opus 5.5 3548652687 feat(fls): read and write a project's own applications as their beans
A project's own applications (classes under projekt/anwendungen, installed
on a host) are no longer left out. The decoder now keeps the Java type of
every primitive — at setters, constructor and call arguments, also through
shared ids like Integer0 — and which properties an object's own getter
made. beanCapture.ts turns a decoded bean back into what the writer writes,
with the node's system software as a mark the writer replaces by the
system software it writes; what the format cannot say is refused, not
guessed (an unresolved value, a Field, a factory-made object, an object a
getter reaches elsewhere in the file). The reader keeps own applications as
captured beans; the corpus no longer warns.

The capture first took a setter with another object's getter result for an
own getter; the round-trip test found it. The Java oracle did not show
whether an application runs on its node's system software, so a mutation
writing the mark instead survived; it now reports onItsSystem (true for all
192 applications of the corpus).

Checked by Java: from the files written again, FILIUS loads Accumulator and
Broadcast installed, switched on and on their node's system software, as
in the original. The round-trip test covers every primitive, null, a
class, an array, shared objects, getter chains, constructed elements, map
reads and static constants.

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

522 lines
22 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. A project's own application is
// written as the file had it (beanCapture.ts).
//
// E-mail: the server's domain and bean accounts with their mailboxes, the
// client's three folders and address book. The client's account is no bean
// (FILIUS 2.14 has no kontoListe property): it lives in /konten.txt, a file.
// Java's errors are not copied: a recipient an older file keeps as a string
// is written as the AddressEntry Java's own AddressEntry(String) makes of it
// — Java puts the string into the typed list unchecked, and its e-mail
// window fails on it; a null recipient is left out.
//
// 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 { withSystemSoftware } from './beanCapture.js';
import {
encodeJavaBeans,
int,
long,
object,
reference,
type BeanObject,
type BeanReference,
type BeanStatement,
type BeanValue,
short,
} from './javaBeansWriter.js';
import { addressEntry, type JavaAddressEntry, type JavaContact, type JavaEmail, type JavaEmailAccount, type JavaRecipient } from './javaEmail.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 with its settings. The e-mail accounts Java reads
* from files (/mailserver/konten.txt, /konten.txt) are written with the
* files; an older file's kontoListe, which FILIUS 2.14 does not read, is not
* written.
*/
export type ApplicationToWrite = Omit<FlsApplication, 'source' | 'fileAccounts' | 'fileAccount' | 'legacyAccounts'>;
/** 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 applicationCount = (node.system.applications?.length ?? 0) + (node.system.ownApplications?.length ?? 0);
const system = applicationCount > 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) {
statements.push({
get: 'installierteAnwendungen',
statements: [
...(system.applications ?? []).map((application): BeanStatement => ({
call: 'put',
args: [application.key, object(application.javaClass, applicationStatements(application, self))],
})),
// A project's own: its bean as the file had it, on this node's system software.
...(system.ownApplications ?? []).map((application): BeanStatement => ({
call: 'put',
args: [application.key, withSystemSoftware(application.bean, 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));
if (application.mailDomain !== undefined) statements.push({ set: 'mailDomain', value: application.mailDomain });
if (application.beanAccounts !== undefined) {
statements.push({ set: 'listeBenutzerkonten', value: listOf(application.beanAccounts.map(emailAccount)) });
}
if (application.received !== undefined) statements.push({ set: 'empfangeneNachrichten', value: emails(application.received) });
if (application.sent !== undefined) statements.push({ set: 'gesendeteNachrichten', value: emails(application.sent) });
if (application.drafts !== undefined) statements.push({ set: 'erstellteNachrichten', value: emails(application.drafts) });
if (application.addressBook !== undefined) {
statements.push({
set: 'adressbuch',
value: object(
'java.util.Vector',
application.addressBook.map((entry) => ({ call: 'add', args: [contact(entry)] })),
),
});
}
return statements;
}
const EMAIL_PACKAGE = 'filius.software.email';
function emailAccount(account: JavaEmailAccount): BeanObject {
return object(`${EMAIL_PACKAGE}.EmailKonto`, [
{ set: 'benutzername', value: account.username },
{ set: 'passwort', value: account.password },
{ set: 'nachname', value: account.lastName },
{ set: 'vorname', value: account.firstName },
{ set: 'emailAdresse', value: account.emailAddress },
{ set: 'pop3server', value: account.pop3Server },
{ set: 'pop3port', value: account.pop3Port },
{ set: 'smtpserver', value: account.smtpServer },
{ set: 'smtpport', value: account.smtpPort },
{ set: 'nachrichten', value: emails(account.messages) },
]);
}
function emails(messages: readonly JavaEmail[]): BeanObject {
return listOf(
messages.map((message) =>
object(`${EMAIL_PACKAGE}.Email`, [
{ set: 'absender', value: address(message.sender) },
{ set: 'empfaenger', value: recipients(message.to) },
{ set: 'cc', value: recipients(message.cc) },
{ set: 'bcc', value: recipients(message.bcc) },
{ set: 'betreff', value: message.subject },
{ set: 'text', value: message.text },
{ set: 'dateReceived', value: message.dateReceived },
{ set: 'neu', value: message.isNew },
{ set: 'delete', value: message.isDeleted },
{ set: 'versendet', value: message.isSent },
]),
),
);
}
/** A list of AddressEntry: a string read as Java's AddressEntry(String) reads it, null left out. */
function recipients(entries: readonly JavaRecipient[]): BeanObject {
return object(
'java.util.ArrayList',
entries.flatMap((entry) =>
entry === null ? [] : [{ call: 'add', args: [address(typeof entry === 'string' ? addressEntry(entry) : entry)] }],
),
);
}
function address(entry: JavaAddressEntry): BeanObject {
const statements: BeanStatement[] = [];
if (entry.name !== null) statements.push({ set: 'name', value: entry.name });
if (entry.mailAddress !== null) statements.push({ set: 'mailAddress', value: entry.mailAddress });
return object(`${EMAIL_PACKAGE}.AddressEntry`, statements);
}
function contact(entry: JavaContact): BeanObject {
return object(`${EMAIL_PACKAGE}.Kontakt`, [
{ set: 'name', value: entry.name },
{ set: 'vorname', value: entry.firstName },
{ set: 'strasse', value: entry.street },
{ set: 'hausnr', value: int(entry.houseNumber) },
{ set: 'plz', value: int(entry.postalCode) },
{ set: 'wohnort', value: entry.city },
{ set: 'email', value: entry.email },
{ set: 'telefon', value: entry.phone },
]);
}
/** 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 });
if (nic.wireless) statements.push({ set: 'wireless', value: true });
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);
}