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>
416 lines
17 KiB
TypeScript
416 lines
17 KiB
TypeScript
// 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);
|
||
}
|
||
|