feat(fls): import installed programs and their settings

importProject carries a host's installed programs into the model with
the settings Swift keeps (web server port, Gnutella peer limit, DNS
recursion and forwarder, personal firewall) and two it does not (the echo
server's port, the generic client's target), plus Java's "switched on".
The Swift oracle writes programs and settings; they match on the 13 files
Swift reads completely.

Intended differences, named in the test:
- every FILIUS program is kept; Swift drops command prompt, text
  editor, file explorer, image viewer, echo server and generic client;
- a web server's files stay in /webserver, its document root; Swift
  moves them to /www. No program state goes into a host's files.
- multicast_server.fls: maxTeilnehmerZahl refers to a shared Integer
  (a button's icon text gap, 4); Swift falls back to 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 06:12:15 +00:00
co-authored by Claude Opus 5.5
parent 8080e7dcb2
commit c3fe56ab16
6 changed files with 800 additions and 92 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) | | `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) |
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Firewall) | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN |
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
@@ -37,7 +37,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen | | `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen |
| `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene |
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien); gegen Swifts importiertes Modell auf 13 Dateien geprüft | | `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | | `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) |
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
+528 -78
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -149,6 +149,56 @@ export interface RemoteLinkConfiguration {
export const DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS = 20; export const DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS = 20;
// ── Installed programs (Java installierteAnwendungen) ──────────────────────
/** Swift's TopologyRuntimeInstallableProgram. */
export type InstallableProgram =
| 'commandPrompt'
| 'fileExplorer'
| 'imageViewer'
| 'textEditor'
| 'webServer'
| 'webBrowser'
| 'echoServer'
| 'simpleClient'
| 'dnsServer'
| 'dhcpServer'
| 'firewall'
| 'emailClient'
| 'emailServer'
| 'gnutella';
export interface WebServerConfiguration {
readonly port: number;
/** Where the served files are: Java keeps them in /webserver, Swift moves them to /www. */
readonly documentRoot: string;
}
export const DEFAULT_WEB_SERVER_PORT = 80;
export interface GnutellaConfiguration {
readonly maximumKnownPeers: number;
}
/** The DNS records follow with the hosts file (Java's /dns/hosts). */
export interface DnsServerConfiguration {
readonly recursiveResolutionEnabled: boolean;
readonly forwardingServerIPAddress?: string;
}
/**
* Java's echo server (ServerBaustein) and generic client (ClientBaustein)
* keep their port and target in the file; Swift asks for them at run time.
*/
export interface EchoServerConfiguration {
readonly port: number;
}
export interface SimpleClientConfiguration {
readonly targetAddress: string;
readonly targetPort: number;
}
// ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ───────── // ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ─────────
export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const; export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const;
+114 -2
View File
@@ -27,7 +27,13 @@
// - a PC or notebook whose file has no file system gets an empty one, as in // - a PC or notebook whose file has no file system gets an empty one, as in
// Java — Swift gives it the demo files of a new device; // Java — Swift gives it the demo files of a new device;
// - a file system's root is Java's root, whatever its user object says. // - a file system's root is Java's root, whatever its user object says.
// Not here yet: applications (with them the web server's /webserver → /www). // - every installed application of FILIUS is kept: Swift drops the command
// prompt, text editor, file explorer, image viewer, echo server and
// generic client, and their settings (port, target) have a place here;
// - a web server's files stay in /webserver (its document root), where Swift
// moves them to /www; and no program state is written into a host's files
// (Swift keeps Gnutella and e-mail state there as hidden JSON files).
// Not here yet: DNS records (from /dns/hosts) and e-mail accounts.
import { import {
canonicalMacAddress, canonicalMacAddress,
@@ -35,6 +41,7 @@ import {
defaultSwitchConfiguration, defaultSwitchConfiguration,
dhcpServerConfiguration, dhcpServerConfiguration,
DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS,
DEFAULT_WEB_SERVER_PORT,
deterministicDocumentationUuid, deterministicDocumentationUuid,
documentationColor, documentationColor,
documentationItem, documentationItem,
@@ -54,6 +61,12 @@ import {
type DeviceConfiguration, type DeviceConfiguration,
type DhcpClientConfiguration, type DhcpClientConfiguration,
type DhcpServerConfiguration, type DhcpServerConfiguration,
type DnsServerConfiguration,
type EchoServerConfiguration,
type GnutellaConfiguration,
type InstallableProgram,
type SimpleClientConfiguration,
type WebServerConfiguration,
type GatewayPortForwardingRow, type GatewayPortForwardingRow,
type RemoteLinkConfiguration, type RemoteLinkConfiguration,
type FirewallAction, type FirewallAction,
@@ -75,7 +88,7 @@ import {
type TopologyNode, type TopologyNode,
type VirtualFileContent, type VirtualFileContent,
} from '@mdg-filius/core'; } from '@mdg-filius/core';
import type { FlsFileTree, FlsFirewall } from './systemSoftware.js'; import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js'; import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
export interface ImportedProject { export interface ImportedProject {
@@ -96,6 +109,14 @@ export interface ImportedProject {
remoteLinkConfigurations: Map<Id, RemoteLinkConfiguration>; remoteLinkConfigurations: Map<Id, RemoteLinkConfiguration>;
/** Every PC and notebook. */ /** Every PC and notebook. */
virtualFileSystems: Map<Id, VirtualFileSystem>; virtualFileSystems: Map<Id, VirtualFileSystem>;
installedPrograms: Map<Id, Set<InstallableProgram>>;
/** Server programs Java had switched on (aktiv). */
activePrograms: Map<Id, Set<InstallableProgram>>;
webServerConfigurations: Map<Id, WebServerConfiguration>;
gnutellaConfigurations: Map<Id, GnutellaConfiguration>;
dnsServerConfigurations: Map<Id, DnsServerConfiguration>;
echoServerConfigurations: Map<Id, EchoServerConfiguration>;
simpleClientConfigurations: Map<Id, SimpleClientConfiguration>;
/** In render order: rectangles below text, then by order. */ /** In render order: rectangles below text, then by order. */
documentationItems: DocumentationItem[]; documentationItems: DocumentationItem[];
/** The topology's warnings, then this step's. */ /** The topology's warnings, then this step's. */
@@ -132,6 +153,13 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
portForwardingRowsByNodeId: new Map(), portForwardingRowsByNodeId: new Map(),
remoteLinkConfigurations: new Map(), remoteLinkConfigurations: new Map(),
virtualFileSystems: new Map(), virtualFileSystems: new Map(),
installedPrograms: new Map(),
activePrograms: new Map(),
webServerConfigurations: new Map(),
gnutellaConfigurations: new Map(),
dnsServerConfigurations: new Map(),
echoServerConfigurations: new Map(),
simpleClientConfigurations: new Map(),
}; };
const nodes = topology.nodes.map((source) => { const nodes = topology.nodes.map((source) => {
const node = importNode(source, newId, warnings); const node = importNode(source, newId, warnings);
@@ -140,6 +168,7 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
registerServiceConfigurations(source, node, system, newId, warnings); registerServiceConfigurations(source, node, system, newId, warnings);
if (node.kind === 'pc' || node.kind === 'notebook') { if (node.kind === 'pc' || node.kind === 'notebook') {
system.virtualFileSystems.set(node.id, importFileSystem(source.system.fileSystem, node.displayName, warnings)); system.virtualFileSystems.set(node.id, importFileSystem(source.system.fileSystem, node.displayName, warnings));
registerPrograms(source, node, system, warnings);
} }
return node; return node;
}); });
@@ -331,8 +360,91 @@ type SystemConfigurations = Pick<
| 'portForwardingRowsByNodeId' | 'portForwardingRowsByNodeId'
| 'remoteLinkConfigurations' | 'remoteLinkConfigurations'
| 'virtualFileSystems' | 'virtualFileSystems'
| 'installedPrograms'
| 'activePrograms'
| 'webServerConfigurations'
| 'gnutellaConfigurations'
| 'dnsServerConfigurations'
| 'echoServerConfigurations'
| 'simpleClientConfigurations'
>; >;
/** Java's application classes as Swift's programs. */
export const PROGRAM_OF_CLASS: Readonly<Record<FlsApplicationClass, InstallableProgram>> = {
'filius.software.clientserver.ClientBaustein': 'simpleClient',
'filius.software.clientserver.ServerBaustein': 'echoServer',
'filius.software.dateiaustausch.PeerToPeerAnwendung': 'gnutella',
'filius.software.dns.DNSServer': 'dnsServer',
'filius.software.email.EmailAnwendung': 'emailClient',
'filius.software.email.EmailServer': 'emailServer',
'filius.software.firewall.Firewall': 'firewall',
'filius.software.lokal.FileExplorer': 'fileExplorer',
'filius.software.lokal.ImageViewer': 'imageViewer',
'filius.software.lokal.Terminal': 'commandPrompt',
'filius.software.lokal.TextEditor': 'textEditor',
'filius.software.www.WebBrowser': 'webBrowser',
'filius.software.www.WebServer': 'webServer',
};
/** The web server's document root: where Java keeps its files. */
export const JAVA_WEB_ROOT = '/webserver';
/** The program parts of Swift's finalizeCurrentNodeCandidate, for a PC or notebook. */
function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemConfigurations, warnings: string[]): void {
const installed = new Set<InstallableProgram>();
const active = new Set<InstallableProgram>();
for (const application of source.system.applications ?? []) {
const program = PROGRAM_OF_CLASS[application.javaClass];
installed.add(program);
if (application.isActive === true) active.add(program);
switch (program) {
case 'webServer':
system.webServerConfigurations.set(node.id, {
port: application.port ?? DEFAULT_WEB_SERVER_PORT,
documentRoot: JAVA_WEB_ROOT,
});
break;
case 'gnutella':
// Without one in the file Java rolls 3, 4 or 5; Swift's default is 3.
system.gnutellaConfigurations.set(node.id, { maximumKnownPeers: application.maxPeers ?? 3 });
break;
case 'dnsServer': {
// Java's DNSServer forwards to the host's own DNS server setting;
// only one that differs from the host's own address is a forwarder.
const recursive = application.recursiveResolutionEnabled === true;
const forwarder = trimmedOrEmpty(source.interfaces[0]?.dns);
const primary = trimmedOrEmpty(source.interfaces[0]?.ip);
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
system.dnsServerConfigurations.set(
node.id,
forwarding
? { recursiveResolutionEnabled: recursive, forwardingServerIPAddress: forwarder }
: { recursiveResolutionEnabled: recursive },
);
break;
}
case 'firewall':
if (application.firewall !== undefined) {
system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(application.firewall, `node '${node.displayName}'`, warnings));
}
break;
case 'echoServer':
system.echoServerConfigurations.set(node.id, { port: application.port ?? 55_555 });
break;
case 'simpleClient':
system.simpleClientConfigurations.set(node.id, {
targetAddress: trimmedOrEmpty(application.targetAddress),
targetPort: application.targetPort ?? 55_555,
});
break;
default:
break;
}
}
if (installed.size > 0) system.installedPrograms.set(node.id, installed);
if (active.size > 0) system.activePrograms.set(node.id, active);
}
/** A row of text cells, trimmed, or undefined unless it has exactly four. */ /** A row of text cells, trimmed, or undefined unless it has exactly four. */
function fourCells(row: (string | null)[]): [string, string, string, string] | undefined { function fourCells(row: (string | null)[]): [string, string, string, string] | undefined {
if (row.length !== 4 || row.some((cell) => cell === null)) return undefined; if (row.length !== 4 || row.some((cell) => cell === null)) return undefined;
+85 -8
View File
@@ -28,6 +28,7 @@ import {
importProject, importProject,
readFlsArchive, readFlsArchive,
readTopology, readTopology,
type FlsApplication,
type FlsFileTree, type FlsFileTree,
type FlsNode, type FlsNode,
type FlsTopology, type FlsTopology,
@@ -91,6 +92,9 @@ function fileEntriesOf(fileSystem: VirtualFileSystem | undefined): unknown {
const DEFAULT_FILES = describeFileEntries(VirtualFileSystem.defaultForDevice().allEntries()); const DEFAULT_FILES = describeFileEntries(VirtualFileSystem.defaultForDevice().allEntries());
const EMPTY_FILES = describeFileEntries(new VirtualFileSystem().allEntries()); const EMPTY_FILES = describeFileEntries(new VirtualFileSystem().allEntries());
/** Programs Swift's import drops and this one keeps (Java has them installed). */
const PROGRAMS_SWIFT_DROPS = new Set(['commandPrompt', 'fileExplorer', 'imageViewer', 'textEditor', 'echoServer', 'simpleClient']);
/** /**
* describeSystem's shape. A firewall as the core has it without one * describeSystem's shape. A firewall as the core has it without one
* (inactive, drop, no rules) is left out: Swift keeps a configuration only * (inactive, drop, no rules) is left out: Swift keeps a configuration only
@@ -126,6 +130,23 @@ function systemOf(project: ImportedProject, nodeId: Id) {
if (forwarding !== undefined) { if (forwarding !== undefined) {
result.portForwarding = forwarding.map((row) => [row.protocolValue, row.publicPortValue, row.lanIPAddress, row.lanPortValue]); result.portForwarding = forwarding.map((row) => [row.protocolValue, row.publicPortValue, row.lanIPAddress, row.lanPortValue]);
} }
const programs = project.installedPrograms.get(nodeId);
const kept = [...(programs ?? [])].filter((program) => !PROGRAMS_SWIFT_DROPS.has(program)).sort();
if (kept.length > 0) result.installedPrograms = kept;
const web = project.webServerConfigurations.get(nodeId);
if (web !== undefined) {
// Swift serves the same files from /www after moving them there.
result.webServer = { port: web.port, documentRoot: web.documentRoot === '/webserver' ? '/www' : web.documentRoot, virtualHosts: false };
}
const gnutella = project.gnutellaConfigurations.get(nodeId);
if (gnutella !== undefined) result.gnutella = gnutella;
const dns = project.dnsServerConfigurations.get(nodeId);
if (dns !== undefined) {
result.dnsServer = {
recursiveResolutionEnabled: dns.recursiveResolutionEnabled,
forwardingServerIPAddress: dns.forwardingServerIPAddress ?? null,
};
}
const remoteLink = project.remoteLinkConfigurations.get(nodeId); const remoteLink = project.remoteLinkConfigurations.get(nodeId);
if (remoteLink !== undefined) result.remoteLink = remoteLink; if (remoteLink !== undefined) result.remoteLink = remoteLink;
const firewall = project.firewallConfigurationsByNodeId.get(nodeId); const firewall = project.firewallConfigurationsByNodeId.get(nodeId);
@@ -217,23 +238,27 @@ function withJavaDefaultNames(state: unknown): unknown {
/** /**
* Where the Swift import is wrong and Java (corpus.java.json) is right, per * Where the Swift import is wrong and Java (corpus.java.json) is right, per
* file and node: the corrected fields. * file and node: the corrected node.
*/ */
const SWIFT_ERRORS: Record<string, Record<number, { displayName: string; hostLabelPolicy: string }>> = { type SwiftNode = { displayName: string; hostLabelPolicy: string; system: Record<string, unknown> };
const shownByIp = (displayName: string) => (node: SwiftNode) => ({ ...node, displayName, hostLabelPolicy: 'ipAddress' });
const SWIFT_ERRORS: Record<string, Record<number, (node: SwiftNode) => SwiftNode>> = {
'java-examples/multicast_server.fls': {
// useIPAsName is `<object idref="Boolean0"/>`, the shared result of a // useIPAsName is `<object idref="Boolean0"/>`, the shared result of a
// button's isEnabled() — true. Swift's reader cannot follow it: it shows // button's isEnabled() — true. Swift's reader cannot follow it: it shows
// the label text as a manual name instead of the IP address. // the label text as a manual name instead of the IP address.
'java-examples/multicast_server.fls': { 0: shownByIp('Rechner'),
0: { displayName: 'Rechner', hostLabelPolicy: 'ipAddress' }, // maxTeilnehmerZahl is `<object idref="Integer0"/>`, the shared result of
1: { displayName: 'Notebook', hostLabelPolicy: 'ipAddress' }, // a button's getIconTextGap() — 4. Swift falls back to its default 3.
2: { displayName: 'Notebook', hostLabelPolicy: 'ipAddress' }, 1: (node) => ({ ...shownByIp('Notebook')(node), system: { ...node.system, gnutella: { maximumKnownPeers: 4 } } }),
2: shownByIp('Notebook'),
}, },
}; };
function withSwiftErrorsCorrected(name: string, state: unknown): unknown { function withSwiftErrorsCorrected(name: string, state: unknown): unknown {
const corrections = SWIFT_ERRORS[name] ?? {}; const corrections = SWIFT_ERRORS[name] ?? {};
const { nodes, ...rest } = state as { nodes: object[] }; const { nodes, ...rest } = state as { nodes: SwiftNode[] };
return { ...rest, nodes: nodes.map((node, index) => ({ ...node, ...corrections[index] })) }; return { ...rest, nodes: nodes.map((node, index) => corrections[index]?.(node) ?? node) };
} }
/** /**
@@ -716,6 +741,58 @@ describe('importProject rules', () => {
expect(() => VirtualFileSystem.validateProjectQuotas(devices)).toThrow('totalSizeQuotaExceeded(100663296, 67108864)'); expect(() => VirtualFileSystem.validateProjectQuotas(devices)).toThrow('totalSizeQuotaExceeded(100663296, 67108864)');
}); });
it("keeps every installed program with its settings, and forwards DNS only to another server", () => {
const source = flsNode('Rechner').source;
const application = (javaClass: FlsApplication['javaClass'], fields: Partial<FlsApplication> = {}): FlsApplication => ({
key: javaClass,
javaClass,
source,
...fields,
});
const host = (dns: string, recursive: boolean) =>
flsNode('Rechner', {
interfaces: [{ ip: '10.0.0.1', mask: '255.255.255.0', mac: undefined, gateway: '', dns }],
system: { applications: [application('filius.software.dns.DNSServer', { isActive: true, port: 53, recursiveResolutionEnabled: recursive })] },
});
const project = importProject(
topology({
nodes: [
flsNode('Notebook', {
interfaces: [nic('10.0.0.2')],
system: {
applications: [
application('filius.software.lokal.Terminal'),
application('filius.software.www.WebServer', { isActive: true, port: 8080, useVirtualHosts: false }),
application('filius.software.clientserver.ServerBaustein', { isActive: false, port: 7 }),
application('filius.software.clientserver.ClientBaustein', { targetAddress: ' 10.0.0.9 ', targetPort: 7 }),
application('filius.software.firewall.Firewall', {
firewall: { activated: true, defaultPolicy: 1, dropICMP: false, filterSYNSegmentsOnly: true, filterUdp: true, rules: [] },
}),
],
},
}),
host('8.8.8.8', true),
host('10.0.0.1', true),
host('8.8.8.8', false),
],
}),
counter(),
);
const [notebook, forwarding, self, iterative] = project.graph.nodes.map((node) => node.id);
expect([...project.installedPrograms.get(notebook!)!].sort()).toEqual(['commandPrompt', 'echoServer', 'firewall', 'simpleClient', 'webServer']);
expect([...project.activePrograms.get(notebook!)!]).toEqual(['webServer']);
expect(project.webServerConfigurations.get(notebook!)).toEqual({ port: 8080, documentRoot: '/webserver' });
expect(project.echoServerConfigurations.get(notebook!)).toEqual({ port: 7 });
expect(project.simpleClientConfigurations.get(notebook!)).toEqual({ targetAddress: '10.0.0.9', targetPort: 7 });
expect(project.firewallConfigurationsByNodeId.get(notebook!)).toMatchObject({ isActive: true, defaultPolicy: 'accept' });
expect([forwarding, self, iterative].map((id) => project.dnsServerConfigurations.get(id!))).toEqual([
{ recursiveResolutionEnabled: true, forwardingServerIPAddress: '8.8.8.8' },
{ recursiveResolutionEnabled: true },
{ recursiveResolutionEnabled: false },
]);
});
it('leaves out a cable to itself and one to a port that already has a cable', () => { it('leaves out a cable to itself and one to a port that already has a cable', () => {
const project = importProject( const project = importProject(
topology({ topology({
@@ -384,6 +384,25 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON
if let rows = state.runtimePortForwardingRowsByNodeID[node.id] { if let rows = state.runtimePortForwardingRowsByNodeID[node.id] {
json["portForwarding"] = rows.map { [$0.protocolValue, $0.publicPortValue, $0.lanIPAddress, $0.lanPortValue] } json["portForwarding"] = rows.map { [$0.protocolValue, $0.publicPortValue, $0.lanIPAddress, $0.lanPortValue] }
} }
if let programs = state.runtimeInstalledProgramsByNodeID[node.id] {
json["installedPrograms"] = programs.map(\.rawValue).sorted()
}
if let web = state.runtimeWebServerConfigurationsByNodeID[node.id] {
json["webServer"] = [
"port": web.port,
"documentRoot": web.documentRoot,
"virtualHosts": web.virtualHostConfiguration != nil,
] as JSON
}
if let gnutella = state.runtimeGnutellaConfigurationsByNodeID[node.id] {
json["gnutella"] = ["maximumKnownPeers": gnutella.maximumKnownPeers] as JSON
}
if let dns = state.runtimeDNSServerConfigurationsByNodeID[node.id] {
json["dnsServer"] = [
"recursiveResolutionEnabled": dns.recursiveResolutionEnabled,
"forwardingServerIPAddress": dns.forwardingServerIPAddress ?? NSNull(),
] as JSON
}
if let link = state.remoteLinkConfigurationsByNodeID[node.id] { if let link = state.remoteLinkConfigurationsByNodeID[node.id] {
json["remoteLink"] = [ json["remoteLink"] = [
"pairIdentifier": link.pairIdentifier, "pairIdentifier": link.pairIdentifier,