feat(fls): export the editor model's system software

exportProject.ts, step X2: a switch's SSID and learning time, a host's
SSID, manual routes, RIP, DHCP client and server with static assignments, a
home router's port forwarding, the firewall of a router or home router (the
core's names as Java's numbers), and modems. What the model has no setting
for is what Java's constructors make. A remote link pair becomes modems on
the port its identifier names (12345 when it names none); the first of a
pair in node order serves, the others connect to localhost.

Checked by Java: every corpus file, imported, exported and written, loads
with the system software of the original up to two new named departures: a
switch SSID Java rolls is the one the model derives from the node id, and
the model keeps a modem pair, not which one serves (modem_beispiel.fls has
the client first; both ways work within one project). The synthetic model
now has settings away from every default, a home router with port
forwarding and two modem pairs. 21 mutation probes, each loaded by Java,
were all caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 09:04:30 +00:00
co-authored by Claude Opus 5.5
parent 6dc1286866
commit d3f476269a
4 changed files with 960 additions and 235 deletions
+149 -6
View File
@@ -7,7 +7,15 @@
// becomes a cable between those, drawn between its two nodes; each
// documentation item a GUIDocuItem. A host whose wireless is enabled has a
// wireless card, and its link to a switch is a wireless cable — the only
// wireless connection Java makes (JHostKonfiguration.updateWifiConnection). System software, files and programs
// wireless connection Java makes (JHostKonfiguration.updateWifiConnection).
//
// The system software: a switch's SSID and learning time, a host's SSID,
// manual routes, RIP, DHCP client and server, a home router's port
// forwarding, the firewall of a router or home router, and a modem. What the
// model has no setting for is what Java's constructors make. A remote link
// pair becomes modems on the port its identifier names (12345 when it names
// none): the first of a pair in node order serves, the others connect to
// localhost — the model's pairs live in one project. System software, files and programs
// follow in later steps.
//
// What the model keeps is written as it keeps it, and so differs from the
@@ -21,15 +29,28 @@
// Java reads them from the first interface only (getStandardGateway,
// getDNSServer), and the model keeps no others;
// - a documentation item always has a text and a font, bold or plain: where
// Java has none, the empty text and Dialog 12 the model gives it.
// Java has none, the empty text and Dialog 12 the model gives it;
// - a switch's SSID the file lacked (Java rolls one) is the one the model
// derives from the node's id.
//
// Checked by Java: every corpus file, imported, exported and written, loads in
// Java FILIUS to the topology of the original up to those departures.
import {
defaultSwitchConfiguration,
dhcpServerConfiguration,
effectiveMacAddress,
firewallConfiguration,
FIREWALL_PROTOCOL,
interfaceKey,
type DeviceConfiguration,
type DhcpClientConfiguration,
type DhcpServerConfiguration,
type FirewallConfiguration,
type GatewayPortForwardingRow,
type ManualRoute,
type RemoteLinkConfiguration,
type SwitchConfiguration,
type DocumentationItem,
type HostLabelPolicy,
type HostWirelessConfiguration,
@@ -40,8 +61,9 @@ import {
type TopologyGraph,
type TopologyNode,
} from '@mdg-filius/core';
import type { FlsDhcpServer, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
import type { NodeToWrite, TopologyToWrite } from './topologyWriter.js';
import type { NodeToWrite, SystemToWrite, TopologyToWrite } from './topologyWriter.js';
/** The parts of the editor model the topology is made of. */
export interface ProjectToExport {
@@ -49,6 +71,14 @@ export interface ProjectToExport {
readonly deviceConfigurations: ReadonlyMap<Id, DeviceConfiguration>;
readonly interfaceConfigurations: ReadonlyMap<InterfaceKey, InterfaceConfiguration>;
readonly hostWirelessConfigurations: ReadonlyMap<Id, HostWirelessConfiguration>;
readonly switchConfigurations: ReadonlyMap<Id, SwitchConfiguration>;
readonly manualRoutesByNodeId: ReadonlyMap<Id, readonly ManualRoute[]>;
readonly ripEnabledByNodeId: ReadonlyMap<Id, boolean>;
readonly firewallConfigurationsByNodeId: ReadonlyMap<Id, FirewallConfiguration>;
readonly dhcpClientConfigurations: ReadonlyMap<Id, DhcpClientConfiguration>;
readonly dhcpServerConfigurations: ReadonlyMap<Id, DhcpServerConfiguration>;
readonly portForwardingRowsByNodeId: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
readonly remoteLinkConfigurations: ReadonlyMap<Id, RemoteLinkConfiguration>;
readonly documentationItems: readonly DocumentationItem[];
}
@@ -74,7 +104,8 @@ export function exportProject(project: ProjectToExport): ExportedTopology {
return false;
});
const indexOfNode = new Map(exported.map((node, index) => [node.id, index]));
const nodes = exported.map((node) => exportNode(node, project));
const servingModems = firstOfEachPair(exported, project);
const nodes = exported.map((node) => exportNode(node, project, servingModems));
const links: FlsLink[] = [];
project.graph.links.forEach((link, index) => {
@@ -102,7 +133,7 @@ export function exportProject(project: ProjectToExport): ExportedTopology {
};
}
function exportNode(node: TopologyNode, project: ProjectToExport): NodeToWrite {
function exportNode(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>): NodeToWrite {
const javaClass = CLASS_OF_KIND[node.kind as Exclude<NodeKind, 'unsupported'>];
const interfaces = exportInterfaces(node, project);
return {
@@ -114,7 +145,119 @@ function exportNode(node: TopologyNode, project: ProjectToExport): NodeToWrite {
useMacAsName: showsMac(node.hostLabelPolicy),
interfaces,
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
system: {},
system: exportSystem(node, project, servingModems),
};
}
/** The first node of each remote link pair: its modem serves. */
function firstOfEachPair(nodes: readonly TopologyNode[], project: ProjectToExport): Set<Id> {
const seen = new Set<string>();
const serving = new Set<Id>();
for (const node of nodes) {
if (node.kind !== 'remoteLink') continue;
const pair = project.remoteLinkConfigurations.get(node.id)?.pairIdentifier ?? '';
if (seen.has(pair)) continue;
seen.add(pair);
serving.add(node.id);
}
return serving;
}
const JAVA_MODEM_PAIR = /^java-modem-port-([0-9]{1,5})$/;
const DEFAULT_MODEM_PORT = 12_345;
function modemPort(pairIdentifier: string | undefined): number {
const match = JAVA_MODEM_PAIR.exec(pairIdentifier ?? '');
const port = match === null ? DEFAULT_MODEM_PORT : Number(match[1]);
return port >= 1 && port <= 65_535 ? port : DEFAULT_MODEM_PORT;
}
function exportSystem(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>): SystemToWrite {
const id = node.id;
switch (node.kind) {
case 'networkSwitch': {
const configuration = project.switchConfigurations.get(id) ?? defaultSwitchConfiguration(id);
return { ssid: configuration.ssid, retentionTime: configuration.retentionTimeMilliseconds };
}
case 'remoteLink':
return {
modem: {
mode: servingModems.has(id) ? 1 : 2,
port: modemPort(project.remoteLinkConfigurations.get(id)?.pairIdentifier),
ipAddress: 'localhost',
},
};
case 'unsupported':
return {};
default:
break;
}
const system: SystemToWrite = {
manualRoutes: (project.manualRoutesByNodeId.get(id) ?? []).map((route) => [
route.destinationNetwork,
route.subnetMask,
route.gateway,
route.interfaceIPAddress,
]),
dhcpClient: project.dhcpClientConfigurations.get(id)?.isEnabled === true,
dhcpServer: exportDhcpServer(project.dhcpServerConfigurations.get(id) ?? dhcpServerConfiguration()),
};
switch (node.kind) {
case 'pc':
case 'notebook': {
const ssid = project.hostWirelessConfigurations.get(id)?.ssid ?? '';
return { ...system, ssid: ssid === '' ? null : ssid };
}
case 'router':
return {
...system,
ripEnabled: project.ripEnabledByNodeId.get(id) === true,
firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? firewallConfiguration()),
};
case 'gateway':
return {
...system,
staticNAT: (project.portForwardingRowsByNodeId.get(id) ?? []).map((row) => [
row.protocolValue,
row.publicPortValue,
row.lanIPAddress,
row.lanPortValue,
]),
firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? firewallConfiguration()),
};
}
}
function exportDhcpServer(server: DhcpServerConfiguration): FlsDhcpServer {
return {
isActive: server.isActive,
lowerBound: server.lowerBoundIPAddress,
upperBound: server.upperBoundIPAddress,
gateway: server.gatewayIPAddress,
dnsServer: server.dnsServerIPAddress,
useOwnSettings: server.useOwnSettings,
staticAssignedAddresses: server.staticAssignments.map((entry) => ({ mac: entry.macAddress, ip: entry.ipAddress })),
};
}
/** The core's names as Java's numbers: FirewallRule.DROP = 0, ACCEPT = 1. */
function exportFirewall(firewall: FirewallConfiguration): FlsFirewall {
const action = (value: 'drop' | 'accept') => (value === 'accept' ? 1 : 0);
return {
activated: firewall.isActive,
defaultPolicy: action(firewall.defaultPolicy),
dropICMP: firewall.dropIcmp,
filterSYNSegmentsOnly: firewall.filterSynSegmentsOnly,
filterUdp: firewall.filterUdp,
rules: firewall.rules.map((rule) => ({
srcIP: rule.sourceIPAddress,
srcMask: rule.sourceSubnetMask,
destIP: rule.destinationIPAddress,
destMask: rule.destinationSubnetMask,
port: rule.port,
protocol: FIREWALL_PROTOCOL[rule.protocol],
action: action(rule.action),
})),
};
}