From c3fe56ab161e2b203e1cbb055c2759cf2064c3ec Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 06:12:15 +0000 Subject: [PATCH] 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 --- CHANGES.md | 4 +- corpus/corpus.swift.json | 606 ++++++++++++++++--- packages/core/src/runtime/config.ts | 50 ++ packages/fls/src/importProject.ts | 116 +++- packages/fls/test/importProject.test.ts | 97 ++- tools/swift-oracle/Sources/Oracle/main.swift | 19 + 6 files changed, 800 insertions(+), 92 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a762bf1..1c08722 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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/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/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/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `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/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/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) | | `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 | diff --git a/corpus/corpus.swift.json b/corpus/corpus.swift.json index 2514a61..b4dc99f 100644 --- a/corpus/corpus.swift.json +++ b/corpus/corpus.swift.json @@ -264,7 +264,13 @@ 16 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : true + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -318,7 +324,14 @@ 83 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -404,7 +417,9 @@ 313 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -447,7 +462,13 @@ 25 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -655,7 +676,13 @@ 145 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -698,7 +725,13 @@ 276 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } } ] @@ -1036,7 +1069,10 @@ "hostWireless" : { "isEnabled" : true, "ssid" : "MyWiFi" - } + }, + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -1131,7 +1167,14 @@ 419 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -1174,7 +1217,13 @@ 422 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -1211,7 +1260,10 @@ "hostWireless" : { "isEnabled" : true, "ssid" : "MyWiFi" - } + }, + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -1832,7 +1884,19 @@ 19 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -1918,7 +1982,9 @@ 100 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -1977,7 +2043,15 @@ 235 ], "system" : { - + "installedPrograms" : [ + "webBrowser", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -3404,7 +3478,13 @@ 16 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : true + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -3458,7 +3538,14 @@ 83 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -3544,7 +3631,9 @@ 313 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -3587,7 +3676,13 @@ 25 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -3795,7 +3890,13 @@ 145 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -3838,7 +3939,13 @@ 276 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } } ] @@ -4015,7 +4122,9 @@ 97 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -4363,6 +4472,19 @@ ], "upperBoundIPAddress" : "192.168.0.29", "useOwnSettings" : false + }, + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false } } }, @@ -5325,7 +5447,14 @@ 147 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -5373,7 +5502,10 @@ 266 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -5421,7 +5553,10 @@ 375 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -5469,7 +5604,10 @@ 59 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -5517,7 +5655,10 @@ 161 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -5725,7 +5866,13 @@ 30 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -5773,7 +5920,9 @@ 285 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } }, { @@ -5821,7 +5970,9 @@ 414 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } } ] @@ -6250,7 +6401,10 @@ 169 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -6463,7 +6617,10 @@ 274 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -6799,7 +6956,13 @@ 606 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -6849,7 +7012,14 @@ 793 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -6897,7 +7067,9 @@ 417 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } }, { @@ -6945,7 +7117,9 @@ 684 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } }, { @@ -7021,6 +7195,19 @@ ], "upperBoundIPAddress" : "10.50.50.254", "useOwnSettings" : false + }, + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false } } }, @@ -7069,7 +7256,10 @@ 840 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -7195,7 +7385,10 @@ "hostWireless" : { "isEnabled" : true, "ssid" : "MyWiFiNetwork" - } + }, + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -7993,7 +8186,10 @@ 165 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -8489,7 +8685,13 @@ 606 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -8539,7 +8741,14 @@ 777 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -8587,7 +8796,9 @@ 417 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } }, { @@ -8635,7 +8846,9 @@ 649 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } }, { @@ -8711,6 +8924,19 @@ ], "upperBoundIPAddress" : "10.50.50.254", "useOwnSettings" : false + }, + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false } } }, @@ -8759,7 +8985,10 @@ 826 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -8885,7 +9114,10 @@ "hostWireless" : { "isEnabled" : true, "ssid" : "MyWiFiNetwork" - } + }, + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -10127,7 +10359,13 @@ 19 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -10177,7 +10415,15 @@ 235 ], "system" : { - + "installedPrograms" : [ + "webBrowser", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -10313,7 +10559,14 @@ 366 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -10822,7 +11075,10 @@ 264 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -10884,7 +11140,15 @@ 169 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "emailServer", + "webBrowser" + ] } }, { @@ -10932,7 +11196,10 @@ 120 ], "system" : { - + "installedPrograms" : [ + "emailClient", + "webBrowser" + ] } }, { @@ -11140,7 +11407,13 @@ 49 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -11188,7 +11461,9 @@ 382 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } }, { @@ -11236,7 +11511,9 @@ 405 ], "system" : { - + "installedPrograms" : [ + "emailServer" + ] } } ] @@ -11327,7 +11604,14 @@ 46 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -11361,7 +11645,9 @@ 182 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -11595,10 +11881,16 @@ 175 ], "system" : { + "gnutella" : { + "maximumKnownPeers" : 3 + }, "hostWireless" : { "isEnabled" : true, "ssid" : "Filius Wi Fi" - } + }, + "installedPrograms" : [ + "gnutella" + ] } }, { @@ -12629,7 +12921,13 @@ 119 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -12663,7 +12961,9 @@ 54 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -12713,7 +13013,15 @@ 202 ], "system" : { - + "installedPrograms" : [ + "webBrowser", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -12763,7 +13071,14 @@ 307 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -12813,7 +13128,14 @@ 363 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -13263,7 +13585,15 @@ 135 ], "system" : { - + "installedPrograms" : [ + "webBrowser", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -13297,7 +13627,9 @@ 148 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -13852,7 +14184,9 @@ 100 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -13902,7 +14236,15 @@ 235 ], "system" : { - + "installedPrograms" : [ + "webBrowser", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -14136,6 +14478,19 @@ ], "upperBoundIPAddress" : "141.99.2.250", "useOwnSettings" : false + }, + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false } } } @@ -14614,7 +14969,9 @@ 100 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -14664,7 +15021,9 @@ 227 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -14898,6 +15257,19 @@ ], "upperBoundIPAddress" : "42.0.0.254", "useOwnSettings" : false + }, + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false } } }, @@ -14993,7 +15365,14 @@ 255 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -15036,7 +15415,13 @@ 72 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -15421,7 +15806,10 @@ "hostWireless" : { "isEnabled" : true, "ssid" : "MyWiFi" - } + }, + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -15516,7 +15904,14 @@ 419 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -15559,7 +15954,13 @@ 422 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer" + ] } }, { @@ -15596,7 +15997,10 @@ "hostWireless" : { "isEnabled" : true, "ssid" : "MyWiFi" - } + }, + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -15866,7 +16270,14 @@ 93 ], "system" : { - + "installedPrograms" : [ + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -15905,7 +16316,9 @@ 151 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -16524,7 +16937,19 @@ 19 ], "system" : { - + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -16610,7 +17035,9 @@ 100 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -16669,7 +17096,15 @@ 235 ], "system" : { - + "installedPrograms" : [ + "webBrowser", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false + } } }, { @@ -16882,7 +17317,9 @@ 97 ], "system" : { - + "installedPrograms" : [ + "webBrowser" + ] } }, { @@ -17230,6 +17667,19 @@ ], "upperBoundIPAddress" : "192.168.0.29", "useOwnSettings" : false + }, + "dnsServer" : { + "forwardingServerIPAddress" : null, + "recursiveResolutionEnabled" : false + }, + "installedPrograms" : [ + "dnsServer", + "webServer" + ], + "webServer" : { + "documentRoot" : "\/www", + "port" : 80, + "virtualHosts" : false } } }, diff --git a/packages/core/src/runtime/config.ts b/packages/core/src/runtime/config.ts index 7546e46..4f774d4 100644 --- a/packages/core/src/runtime/config.ts +++ b/packages/core/src/runtime/config.ts @@ -149,6 +149,56 @@ export interface RemoteLinkConfiguration { 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) ───────── export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const; diff --git a/packages/fls/src/importProject.ts b/packages/fls/src/importProject.ts index b76c5b9..36097a5 100644 --- a/packages/fls/src/importProject.ts +++ b/packages/fls/src/importProject.ts @@ -27,7 +27,13 @@ // - 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; // - 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 { canonicalMacAddress, @@ -35,6 +41,7 @@ import { defaultSwitchConfiguration, dhcpServerConfiguration, DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, + DEFAULT_WEB_SERVER_PORT, deterministicDocumentationUuid, documentationColor, documentationItem, @@ -54,6 +61,12 @@ import { type DeviceConfiguration, type DhcpClientConfiguration, type DhcpServerConfiguration, + type DnsServerConfiguration, + type EchoServerConfiguration, + type GnutellaConfiguration, + type InstallableProgram, + type SimpleClientConfiguration, + type WebServerConfiguration, type GatewayPortForwardingRow, type RemoteLinkConfiguration, type FirewallAction, @@ -75,7 +88,7 @@ import { type TopologyNode, type VirtualFileContent, } 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'; export interface ImportedProject { @@ -96,6 +109,14 @@ export interface ImportedProject { remoteLinkConfigurations: Map; /** Every PC and notebook. */ virtualFileSystems: Map; + installedPrograms: Map>; + /** Server programs Java had switched on (aktiv). */ + activePrograms: Map>; + webServerConfigurations: Map; + gnutellaConfigurations: Map; + dnsServerConfigurations: Map; + echoServerConfigurations: Map; + simpleClientConfigurations: Map; /** In render order: rectangles below text, then by order. */ documentationItems: DocumentationItem[]; /** The topology's warnings, then this step's. */ @@ -132,6 +153,13 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP portForwardingRowsByNodeId: new Map(), remoteLinkConfigurations: 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 node = importNode(source, newId, warnings); @@ -140,6 +168,7 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP registerServiceConfigurations(source, node, system, newId, warnings); if (node.kind === 'pc' || node.kind === 'notebook') { system.virtualFileSystems.set(node.id, importFileSystem(source.system.fileSystem, node.displayName, warnings)); + registerPrograms(source, node, system, warnings); } return node; }); @@ -331,8 +360,91 @@ type SystemConfigurations = Pick< | 'portForwardingRowsByNodeId' | 'remoteLinkConfigurations' | 'virtualFileSystems' + | 'installedPrograms' + | 'activePrograms' + | 'webServerConfigurations' + | 'gnutellaConfigurations' + | 'dnsServerConfigurations' + | 'echoServerConfigurations' + | 'simpleClientConfigurations' >; +/** Java's application classes as Swift's programs. */ +export const PROGRAM_OF_CLASS: Readonly> = { + '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(); + const active = new Set(); + 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. */ function fourCells(row: (string | null)[]): [string, string, string, string] | undefined { if (row.length !== 4 || row.some((cell) => cell === null)) return undefined; diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index ebdf25f..f79bdde 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -28,6 +28,7 @@ import { importProject, readFlsArchive, readTopology, + type FlsApplication, type FlsFileTree, type FlsNode, type FlsTopology, @@ -91,6 +92,9 @@ function fileEntriesOf(fileSystem: VirtualFileSystem | undefined): unknown { const DEFAULT_FILES = describeFileEntries(VirtualFileSystem.defaultForDevice().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 * (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) { 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); if (remoteLink !== undefined) result.remoteLink = remoteLink; 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 - * file and node: the corrected fields. + * file and node: the corrected node. */ -const SWIFT_ERRORS: Record> = { - // useIPAsName is ``, the shared result of a - // button's isEnabled() — true. Swift's reader cannot follow it: it shows - // the label text as a manual name instead of the IP address. +type SwiftNode = { displayName: string; hostLabelPolicy: string; system: Record }; +const shownByIp = (displayName: string) => (node: SwiftNode) => ({ ...node, displayName, hostLabelPolicy: 'ipAddress' }); +const SWIFT_ERRORS: Record SwiftNode>> = { 'java-examples/multicast_server.fls': { - 0: { displayName: 'Rechner', hostLabelPolicy: 'ipAddress' }, - 1: { displayName: 'Notebook', hostLabelPolicy: 'ipAddress' }, - 2: { displayName: 'Notebook', hostLabelPolicy: 'ipAddress' }, + // useIPAsName is ``, the shared result of a + // button's isEnabled() — true. Swift's reader cannot follow it: it shows + // the label text as a manual name instead of the IP address. + 0: shownByIp('Rechner'), + // maxTeilnehmerZahl is ``, the shared result of + // a button's getIconTextGap() — 4. Swift falls back to its default 3. + 1: (node) => ({ ...shownByIp('Notebook')(node), system: { ...node.system, gnutella: { maximumKnownPeers: 4 } } }), + 2: shownByIp('Notebook'), }, }; function withSwiftErrorsCorrected(name: string, state: unknown): unknown { const corrections = SWIFT_ERRORS[name] ?? {}; - const { nodes, ...rest } = state as { nodes: object[] }; - return { ...rest, nodes: nodes.map((node, index) => ({ ...node, ...corrections[index] })) }; + const { nodes, ...rest } = state as { nodes: SwiftNode[] }; + 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)'); }); + 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 => ({ + 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', () => { const project = importProject( topology({ diff --git a/tools/swift-oracle/Sources/Oracle/main.swift b/tools/swift-oracle/Sources/Oracle/main.swift index 6990de3..2c88b13 100644 --- a/tools/swift-oracle/Sources/Oracle/main.swift +++ b/tools/swift-oracle/Sources/Oracle/main.swift @@ -384,6 +384,25 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON if let rows = state.runtimePortForwardingRowsByNodeID[node.id] { 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] { json["remoteLink"] = [ "pairIdentifier": link.pairIdentifier,