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:
+2
-2
@@ -51,8 +51,8 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
||||
| `test/javaBeansWriter.test.ts`, `test/beans/` | — | neu: 11 Dateien des Schreibers, von Javas `XMLDecoder` gelesen und beschrieben; Erwartungen von Hand |
|
||||
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); installierte Programme nach den Dateien (Webserver, Tauschbörse und E-Mail-Server legen ihre Ordner nur an, wo sie fehlen; der DNS-Server liest `/dns/hosts` beim Installieren), je unter ihrem Schlüssel mit der Systemsoftware wie `installApp`, samt Einstellungen und persönlicher Firewall; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; Kopf „Filius version: 2.14.0 (mdg-filius)“ |
|
||||
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware, Dateisysteme und Programme samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel, jedes Filius-Programm mit Einstellungen abseits der Vorgabe samt persönlicher Firewall, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) |
|
||||
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente |
|
||||
| `test/exportProject.test.ts`, `test/exported.java.json` | — | neu: alle 23 Korpus-Dateien importiert, exportiert, geschrieben und von Java geladen — Topologie wie beim Original bis auf benannte Abweichungen (abgeleiteter MAC statt gewürfeltem, Doku in Zeichenreihenfolge mit Text, Schrift und Mindestgröße des Modells); dazu ein künstliches Modell, das Java ebenfalls lädt |
|
||||
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente; X2 Systemsoftware — SSIDs, Lernzeit, Routen, RIP, DHCP-Client und -Server, Portweiterleitung, Firewall (Namen des Kerns als Javas Zahlen), Modems aus den Paaren des Modells (Port aus dem Paarnamen, der erste eines Paars dient, Ziel `localhost`) |
|
||||
| `test/exportProject.test.ts`, `test/exported.java.json` | — | neu: alle 23 Korpus-Dateien importiert, exportiert, geschrieben und von Java geladen — Topologie wie beim Original bis auf benannte Abweichungen (abgeleiteter MAC und abgeleitete Switch-SSID statt gewürfelter, Modem-Rollen nach Reihenfolge im Paar, Doku in Zeichenreihenfolge mit Text, Schrift und Mindestgröße des Modells); dazu ein künstliches Modell, das Java ebenfalls lädt |
|
||||
| `src/importEmail.ts` | `Persistence/TopologyProjectStore.swift` (`registerJavaEmailConfigurations`, `assignedJavaEmailMessages`) | übertragen, Eingabe ist Javas Sicht: Server-Konten aus `/mailserver/konten.txt`, ohne Datei aus der Bean-Liste (Java verlöre sie); Client-Konto aus `/konten.txt`, ohne Datei aus der älteren `kontoListe` (Filius 2.14 liest sie nicht mehr); Konten und Nachrichten bleiben, wo Swift verwirft, was seine Prüfung nicht besteht; Vor- und Nachname getrennt; Empfangszeit nur aus der Datei (Swift nummeriert); gegen Swifts Modell geprüft |
|
||||
| `test/importEmail.test.ts`, `test/email-*.swift.json` | `State/TopologyEmail.swift` (`decodeJavaServer`, `decodeJavaClient`) als Vergleich | neu: dieselben 43 Kontodateien mit Swifts Leser; wie Swift bis auf benannte Fälle, in denen Swift prüft und die ganze Datei verwirft, wo Java liest |
|
||||
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,16 @@ import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { DOCUMENTATION_MINIMUM_SIZE, stableGeneratedMacAddress, type Id, type TopologyNode } from '@mdg-filius/core';
|
||||
import {
|
||||
defaultSwitchConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
DOCUMENTATION_MINIMUM_SIZE,
|
||||
firewallConfiguration,
|
||||
firewallRule,
|
||||
stableGeneratedMacAddress,
|
||||
type Id,
|
||||
type TopologyNode,
|
||||
} from '@mdg-filius/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
decodeJavaBeans,
|
||||
@@ -42,13 +51,27 @@ function counter(): () => Id {
|
||||
return () => `00000000-0000-0000-0000-${String(++next).padStart(12, '0')}`;
|
||||
}
|
||||
|
||||
type JavaNode = { class: string; name: string | null; bounds: number[]; useIpAsName?: boolean; useMacAsName?: boolean; ports: unknown };
|
||||
type JavaNode = {
|
||||
class: string;
|
||||
name: string | null;
|
||||
bounds: number[];
|
||||
useIpAsName?: boolean;
|
||||
useMacAsName?: boolean;
|
||||
ports: unknown;
|
||||
system: Record<string, unknown>;
|
||||
};
|
||||
type JavaFile = { success: boolean; nodes: JavaNode[]; cables: unknown[]; documentation: unknown[]; sha256?: string };
|
||||
|
||||
const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, JavaFile>;
|
||||
const exportedPath = join(here, 'exported.java.json');
|
||||
const exported = JSON.parse(readFileSync(exportedPath, 'utf8')) as Record<string, JavaFile>;
|
||||
|
||||
/** The system software without its files and applications, later steps. */
|
||||
function systemOf(system: Record<string, unknown>) {
|
||||
const { fileSystem: _files, applications: _applications, ...settings } = system;
|
||||
return settings;
|
||||
}
|
||||
|
||||
/** The topology of Java's view: the label's position, not its size. */
|
||||
function topologyOf(view: JavaFile) {
|
||||
return {
|
||||
@@ -60,6 +83,7 @@ function topologyOf(view: JavaFile) {
|
||||
useIpAsName: node.useIpAsName,
|
||||
useMacAsName: node.useMacAsName,
|
||||
ports: node.ports,
|
||||
system: systemOf(node.system),
|
||||
})),
|
||||
cables: view.cables,
|
||||
documentation: view.documentation,
|
||||
@@ -73,7 +97,12 @@ function topologyOf(view: JavaFile) {
|
||||
* each load) has the one the simulation uses, derived from its port id;
|
||||
* - documentation: the items in the model's render order (rectangles below
|
||||
* text), a missing text empty, a missing font Dialog 12, and at least the
|
||||
* editor's smallest size (DOCUMENTATION_MINIMUM_SIZE).
|
||||
* editor's smallest size (DOCUMENTATION_MINIMUM_SIZE);
|
||||
* - switchSsid: a switch's SSID the file lacked (Java rolls one) is the one
|
||||
* the model derives from the node's id;
|
||||
* - modemRoles: the model keeps a pair of modems, not which one serves: the
|
||||
* first of a pair in node order serves, the others connect. Both ways
|
||||
* work within one project, where the client connects to localhost.
|
||||
*/
|
||||
const DEPARTURES = {
|
||||
generatedMac(view: JavaFile, macs: (string | undefined)[][]): JavaFile {
|
||||
@@ -87,6 +116,20 @@ const DEPARTURES = {
|
||||
})),
|
||||
};
|
||||
},
|
||||
switchSsid(view: JavaFile, nodeIds: Id[]): JavaFile {
|
||||
return mapSystems(view, (node, index) =>
|
||||
node.class === 'Switch' && node.system.ssid === null ? { ...node.system, ssid: defaultSwitchConfiguration(nodeIds[index]!).ssid } : node.system,
|
||||
);
|
||||
},
|
||||
modemRoles(view: JavaFile, pairs: (string | undefined)[]): JavaFile {
|
||||
const seen = new Set<string | undefined>();
|
||||
return mapSystems(view, (node, index) => {
|
||||
if (node.class !== 'Modem') return node.system;
|
||||
const serves = !seen.has(pairs[index]);
|
||||
seen.add(pairs[index]);
|
||||
return { ...node.system, modem: { ...(node.system.modem as object), mode: serves ? 1 : 2 } };
|
||||
});
|
||||
},
|
||||
documentation(view: JavaFile): JavaFile {
|
||||
const items = (view.documentation as Record<string, unknown>[]).map((item) => {
|
||||
const [x, y, width, height] = item.bounds as number[];
|
||||
@@ -101,6 +144,10 @@ const DEPARTURES = {
|
||||
},
|
||||
};
|
||||
|
||||
function mapSystems(view: JavaFile, system: (node: JavaNode, index: number) => Record<string, unknown>): JavaFile {
|
||||
return { ...view, nodes: view.nodes.map((node, index) => ({ ...node, system: system(node, index) })) };
|
||||
}
|
||||
|
||||
/** Documentation items in an order that does not depend on the list's. */
|
||||
function sortedDocumentation(items: unknown[]): unknown[] {
|
||||
return items.map((item) => JSON.stringify(item)).sort().map((text) => JSON.parse(text) as unknown);
|
||||
@@ -111,7 +158,12 @@ function portIds(project: ReturnType<typeof importProject>): Id[][] {
|
||||
return project.graph.nodes.map((node) => node.ports.map((port) => port.id));
|
||||
}
|
||||
|
||||
const departed = { generatedMac: new Set<string>(), documentation: new Set<string>() };
|
||||
const departed = {
|
||||
generatedMac: new Set<string>(),
|
||||
documentation: new Set<string>(),
|
||||
switchSsid: new Set<string>(),
|
||||
modemRoles: new Set<string>(),
|
||||
};
|
||||
|
||||
describe('the editor model exported, as Java loads it', () => {
|
||||
for (const file of flsFiles(corpusRoot)) {
|
||||
@@ -138,22 +190,54 @@ describe('the editor model exported, as Java loads it', () => {
|
||||
}),
|
||||
);
|
||||
const source = original[name]!;
|
||||
const withMacs = DEPARTURES.generatedMac(source, macs);
|
||||
const expected = DEPARTURES.documentation(withMacs);
|
||||
if (JSON.stringify(withMacs) !== JSON.stringify(source)) departed.generatedMac.add(name);
|
||||
if (JSON.stringify(expected.documentation) !== JSON.stringify(source.documentation)) departed.documentation.add(name);
|
||||
const nodeIds = project.graph.nodes.map((node) => node.id);
|
||||
const pairs = nodeIds.map((id) => project.remoteLinkConfigurations.get(id)?.pairIdentifier);
|
||||
const steps: [keyof typeof departed, (view: JavaFile) => JavaFile][] = [
|
||||
['generatedMac', (view) => DEPARTURES.generatedMac(view, macs)],
|
||||
['switchSsid', (view) => DEPARTURES.switchSsid(view, nodeIds)],
|
||||
['modemRoles', (view) => DEPARTURES.modemRoles(view, pairs)],
|
||||
['documentation', (view) => DEPARTURES.documentation(view)],
|
||||
];
|
||||
let expected = source;
|
||||
for (const [departure, apply] of steps) {
|
||||
const next = apply(expected);
|
||||
if (departure === 'documentation') {
|
||||
if (JSON.stringify(next.documentation) !== JSON.stringify(expected.documentation)) departed[departure].add(name);
|
||||
} else if (JSON.stringify(next) !== JSON.stringify(expected)) {
|
||||
departed[departure].add(name);
|
||||
}
|
||||
expected = next;
|
||||
}
|
||||
expect(topologyOf({ ...java!, documentation: sortedDocumentation(java!.documentation) })).toEqual(topologyOf(expected));
|
||||
});
|
||||
}
|
||||
|
||||
it('departs from Java where the model decides differently', () => {
|
||||
expect(departed.generatedMac.size).toBeGreaterThan(0);
|
||||
expect(departed.documentation.size).toBeGreaterThan(0);
|
||||
for (const [departure, files] of Object.entries(departed)) expect(files.size, departure).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── A model beyond the corpus, loaded by Java ──────────────────────────────
|
||||
|
||||
/** A model without nodes, settings or documentation. */
|
||||
function emptyProject(): ProjectToExport {
|
||||
return {
|
||||
graph: { nodes: [], links: [] },
|
||||
deviceConfigurations: new Map(),
|
||||
interfaceConfigurations: new Map(),
|
||||
hostWirelessConfigurations: new Map(),
|
||||
switchConfigurations: new Map(),
|
||||
manualRoutesByNodeId: new Map(),
|
||||
ripEnabledByNodeId: new Map(),
|
||||
firewallConfigurationsByNodeId: new Map(),
|
||||
dhcpClientConfigurations: new Map(),
|
||||
dhcpServerConfigurations: new Map(),
|
||||
portForwardingRowsByNodeId: new Map(),
|
||||
remoteLinkConfigurations: new Map(),
|
||||
documentationItems: [],
|
||||
};
|
||||
}
|
||||
|
||||
const port = (id: string) => ({ id, label: id, isOccupied: false });
|
||||
|
||||
/**
|
||||
@@ -162,12 +246,17 @@ const port = (id: string) => ({ id, label: id, isOccupied: false });
|
||||
* interfaces and a gateway, a half-transparent bold text and a rectangle.
|
||||
*/
|
||||
const SYNTHETIC: ProjectToExport = {
|
||||
...emptyProject(),
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'pc', kind: 'pc', displayName: 'PC', hostLabelPolicy: 'ipAndMAC', position: { x: 10.6, y: 20.2 }, ports: [port('p0')] },
|
||||
{ id: 'sw', kind: 'networkSwitch', displayName: 'SW', hostLabelPolicy: 'manual', position: { x: 100, y: 20 }, ports: ['s0', 's1', 's2', 's3', 's4'].map(port) },
|
||||
{ id: 'nb', kind: 'notebook', displayName: 'NB', hostLabelPolicy: 'macAddress', position: { x: 100, y: 120 }, ports: [port('n0')] },
|
||||
{ id: 'rt', kind: 'router', displayName: 'RT', hostLabelPolicy: 'manual', position: { x: 200, y: 20 }, ports: ['r0', 'r1', 'r2'].map(port) },
|
||||
{ id: 'gw', kind: 'gateway', displayName: 'GW', hostLabelPolicy: 'manual', position: { x: 300, y: 20 }, ports: ['g0', 'g1'].map(port) },
|
||||
{ id: 'm1', kind: 'remoteLink', displayName: 'M1', hostLabelPolicy: 'manual', position: { x: 400, y: 20 }, ports: [port('m1p')] },
|
||||
{ id: 'm2', kind: 'remoteLink', displayName: 'M2', hostLabelPolicy: 'manual', position: { x: 500, y: 20 }, ports: [port('m2p')] },
|
||||
{ id: 'm3', kind: 'remoteLink', displayName: 'M3', hostLabelPolicy: 'manual', position: { x: 600, y: 20 }, ports: [port('m3p')] },
|
||||
],
|
||||
links: [
|
||||
{ id: 'l1', sourceNodeId: 'pc', sourcePortId: 'p0', targetNodeId: 'sw', targetPortId: 's4' },
|
||||
@@ -184,6 +273,56 @@ const SYNTHETIC: ProjectToExport = {
|
||||
['rt/r0', { ipAddress: '10.1.0.1', subnetMask: '255.255.0.0' }],
|
||||
['rt/r1', { ipAddress: '10.0.0.254', subnetMask: '255.255.255.0' }],
|
||||
['rt/r2', { ipAddress: '10.2.0.1', subnetMask: '255.255.0.0' }],
|
||||
['gw/g0', { ipAddress: '42.0.0.7', subnetMask: '255.0.0.0' }],
|
||||
['gw/g1', { ipAddress: '192.168.1.1', subnetMask: '255.255.255.0' }],
|
||||
]),
|
||||
switchConfigurations: new Map([['sw', { ssid: 'S-net', retentionTimeMilliseconds: 1234 }]]),
|
||||
manualRoutesByNodeId: new Map([
|
||||
['pc', [{ destinationNetwork: '10.5.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.254', interfaceIPAddress: '10.0.0.1' }]],
|
||||
['rt', [{ destinationNetwork: '10.6.0.0', subnetMask: '255.255.0.0', gateway: '10.1.0.2', interfaceIPAddress: '10.1.0.1' }]],
|
||||
]),
|
||||
ripEnabledByNodeId: new Map([['rt', true]]),
|
||||
firewallConfigurationsByNodeId: new Map([
|
||||
[
|
||||
'rt',
|
||||
firewallConfiguration({
|
||||
isActive: true,
|
||||
defaultPolicy: 'accept',
|
||||
dropIcmp: true,
|
||||
filterSynSegmentsOnly: false,
|
||||
filterUdp: false,
|
||||
rules: [
|
||||
firewallRule({ sourceIPAddress: '10.0.0.0', sourceSubnetMask: '255.0.0.0', protocol: 'all', action: 'accept' }),
|
||||
firewallRule({ destinationIPAddress: '10.2.0.5', destinationSubnetMask: '255.255.255.255', port: 80, protocol: 'tcp' }),
|
||||
firewallRule({ port: 53, protocol: 'udp', action: 'accept' }),
|
||||
firewallRule({ protocol: 'icmp' }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
]),
|
||||
dhcpClientConfigurations: new Map([
|
||||
['pc', { isEnabled: true }],
|
||||
['gw', { isEnabled: true }],
|
||||
]),
|
||||
dhcpServerConfigurations: new Map([
|
||||
[
|
||||
'gw',
|
||||
dhcpServerConfiguration({
|
||||
isActive: true,
|
||||
lowerBoundIPAddress: '192.168.1.100',
|
||||
upperBoundIPAddress: '192.168.1.199',
|
||||
gatewayIPAddress: '192.168.1.1',
|
||||
dnsServerIPAddress: '8.8.8.8',
|
||||
useOwnSettings: true,
|
||||
staticAssignments: [{ id: 'a1', macAddress: 'AA:BB:CC:DD:EE:77', ipAddress: '192.168.1.77' }],
|
||||
}),
|
||||
],
|
||||
]),
|
||||
portForwardingRowsByNodeId: new Map([['gw', [{ protocolValue: 'TCP', publicPortValue: '8080', lanIPAddress: '192.168.1.10', lanPortValue: '80' }]]]),
|
||||
remoteLinkConfigurations: new Map([
|
||||
['m1', { pairIdentifier: 'java-modem-port-4242', latencyMilliseconds: 20, isEnabled: true }],
|
||||
['m2', { pairIdentifier: 'java-modem-port-4242', latencyMilliseconds: 20, isEnabled: true }],
|
||||
['m3', { pairIdentifier: 'our own pair', latencyMilliseconds: 20, isEnabled: true }],
|
||||
]),
|
||||
hostWirelessConfigurations: new Map([['nb', { isEnabled: true, ssid: 'net' }]]),
|
||||
documentationItems: [
|
||||
@@ -227,6 +366,15 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
);
|
||||
expect(warnings).toEqual([]);
|
||||
const card = (ip: string, mask: string, mac: string, gateway: string, dns: string, wireless = false) => ({ ip, mask, mac, gateway, dns, wireless });
|
||||
const modem = (name: string, x: number, mode: number, port: number) => ({
|
||||
class: 'Modem',
|
||||
name,
|
||||
position: [x, 20],
|
||||
useIpAsName: undefined,
|
||||
useMacAsName: undefined,
|
||||
ports: [{}],
|
||||
system: { modem: { mode, port, ipAdresse: 'localhost' } },
|
||||
});
|
||||
expect(topologyOf(java!)).toEqual({
|
||||
success: true,
|
||||
nodes: [
|
||||
@@ -237,8 +385,22 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
useIpAsName: true,
|
||||
useMacAsName: true,
|
||||
ports: [card('10.0.0.1', '255.255.255.0', stableGeneratedMacAddress('p0'), '10.0.0.254', '10.0.0.53')],
|
||||
system: {
|
||||
ssid: null,
|
||||
manualRoutes: [['10.5.0.0', '255.255.0.0', '10.0.0.254', '10.0.0.1']],
|
||||
dhcpClient: true,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
},
|
||||
},
|
||||
{
|
||||
class: 'Switch',
|
||||
name: 'SW',
|
||||
position: [100, 20],
|
||||
useIpAsName: undefined,
|
||||
useMacAsName: undefined,
|
||||
ports: [{}, {}, {}, {}, {}],
|
||||
system: { ssid: 'S-net', retentionTime: 1234 },
|
||||
},
|
||||
{ class: 'Switch', name: 'SW', position: [100, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}, {}, {}, {}, {}] },
|
||||
{
|
||||
class: 'Notebook',
|
||||
name: 'NB',
|
||||
@@ -246,6 +408,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
useIpAsName: false,
|
||||
useMacAsName: true,
|
||||
ports: [card('10.0.0.2', '255.255.255.0', stableGeneratedMacAddress('n0'), '', '', true)],
|
||||
system: { ssid: 'net', manualRoutes: [], dhcpClient: false, dhcpServer: JAVA_DHCP_DEFAULTS },
|
||||
},
|
||||
{
|
||||
class: 'Vermittlungsrechner',
|
||||
@@ -258,7 +421,56 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
card('10.0.0.254', '255.255.255.0', stableGeneratedMacAddress('r1'), '10.9.9.9', '10.0.0.53'),
|
||||
card('10.2.0.1', '255.255.0.0', stableGeneratedMacAddress('r2'), '10.9.9.9', '10.0.0.53'),
|
||||
],
|
||||
system: {
|
||||
ripEnabled: true,
|
||||
manualRoutes: [['10.6.0.0', '255.255.0.0', '10.1.0.2', '10.1.0.1']],
|
||||
dhcpClient: false,
|
||||
dhcpServer: JAVA_DHCP_DEFAULTS,
|
||||
firewall: {
|
||||
activated: true,
|
||||
defaultPolicy: 1,
|
||||
dropICMP: true,
|
||||
filterSYNSegmentsOnly: false,
|
||||
filterUdp: false,
|
||||
rules: [
|
||||
['10.0.0.0', '255.0.0.0', '', '', -1, -1, 1],
|
||||
['', '', '10.2.0.5', '255.255.255.255', 80, 6, 0],
|
||||
['', '', '', '', 53, 17, 1],
|
||||
['', '', '', '', -1, 1, 0],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
class: 'Gateway',
|
||||
name: 'GW',
|
||||
position: [300, 20],
|
||||
useIpAsName: undefined,
|
||||
useMacAsName: undefined,
|
||||
ports: [
|
||||
card('42.0.0.7', '255.0.0.0', stableGeneratedMacAddress('g0'), '', ''),
|
||||
card('192.168.1.1', '255.255.255.0', stableGeneratedMacAddress('g1'), '', ''),
|
||||
],
|
||||
system: {
|
||||
manualRoutes: [],
|
||||
dhcpClient: true,
|
||||
dhcpServer: {
|
||||
aktiv: true,
|
||||
untergrenze: '192.168.1.100',
|
||||
obergrenze: '192.168.1.199',
|
||||
gatewayip: '192.168.1.1',
|
||||
dnsserverip: '8.8.8.8',
|
||||
ownSettings: true,
|
||||
staticAssignedAddresses: ['AA:BB:CC:DD:EE:77 192.168.1.77'],
|
||||
},
|
||||
staticNAT: [['TCP', '8080', '192.168.1.10', '80']],
|
||||
firewall: { activated: false, defaultPolicy: 0, dropICMP: false, filterSYNSegmentsOnly: true, filterUdp: true, rules: [] },
|
||||
},
|
||||
},
|
||||
// A pair on Java's port: the first serves; a pair of our own: Java's default port.
|
||||
modem('M1', 400, 1, 4242),
|
||||
modem('M2', 500, 2, 4242),
|
||||
modem('M3', 600, 1, 12345),
|
||||
],
|
||||
cables: [
|
||||
{ ends: [[0, 0], [1, 4]], wireless: false, panel: [0, 1] },
|
||||
@@ -273,6 +485,17 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** The DHCP server as every IP stack's constructor leaves it. */
|
||||
const JAVA_DHCP_DEFAULTS = {
|
||||
aktiv: false,
|
||||
untergrenze: '0.0.0.0',
|
||||
obergrenze: '0.0.0.0',
|
||||
gatewayip: '0.0.0.0',
|
||||
dnsserverip: '0.0.0.0',
|
||||
ownSettings: false,
|
||||
staticAssignedAddresses: [],
|
||||
};
|
||||
|
||||
// ── Rules the corpus does not exercise ─────────────────────────────────────
|
||||
|
||||
describe('exportProject rules', () => {
|
||||
@@ -293,11 +516,9 @@ describe('exportProject rules', () => {
|
||||
targetPortId: target[1],
|
||||
});
|
||||
const project = (nodes: TopologyNode[], links: ReturnType<typeof link>[], wireless: [string, boolean][] = []): ProjectToExport => ({
|
||||
...emptyProject(),
|
||||
graph: { nodes, links },
|
||||
deviceConfigurations: new Map(),
|
||||
interfaceConfigurations: new Map(),
|
||||
hostWirelessConfigurations: new Map(wireless.map(([id, isEnabled]) => [id, { isEnabled, ssid: 'net' }])),
|
||||
documentationItems: [],
|
||||
});
|
||||
|
||||
it('leaves out a node of a kind Java does not have, and the links to it', () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user