// 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 { forwardsByDefault, type FlsApplication, type FlsFileTree, type FlsFirewall, type 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; /** The system software's settings, files and applications. */ export type SystemToWrite = Omit & { readonly applications?: readonly ApplicationToWrite[]; }; export type NodeToWrite = Pick< FlsNode, 'javaClass' | 'name' | 'x' | 'y' | 'useIpAsName' | 'useMacAsName' | 'interfaces' | 'portCount' > & { readonly system: SystemToWrite; readonly cloud?: boolean }; 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 = { 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]; // A switch drawn as a cloud: written only when true, before the name, as XMLEncoder orders Switch's properties. const statements: BeanStatement[] = [...(node.cloud === true ? [{ set: 'cloud', value: true }] : []), { 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 }); // Written only where it differs from what Java's constructor sets, as XMLEncoder does. if (system.ipForwardingEnabled !== undefined && system.ipForwardingEnabled !== forwardsByDefault(javaClass)) { statements.push({ set: 'ipForwardingEnabled', value: system.ipForwardingEnabled }); } 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); }