feat(fls): DHCP, static NAT and modem settings as Java loads them

readSystemSoftware adds the DHCP client switch and server settings of
every IP stack, a home router's static NAT table (port forwarding) and a
modem's mode, port and address. All 23 corpus files match Java.

Static DHCP assignments follow setStaticAssignedAddresses: each entry is
split at Java whitespace (the same 25 characters as Character.isWhitespace,
measured over the BMP), and an entry without MAC and IP ends the list, as
Java throws there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:06:08 +00:00
co-authored by Claude Opus 5.5
parent ef3bb94cb9
commit 4dcc7e09fc
8 changed files with 1995 additions and 133 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `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, 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; 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` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht); gegen Java geprüft |
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem; `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/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
| `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java |
+1775 -124
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -3,5 +3,6 @@ export * from './archive.js';
export * from './importProject.js';
export * from './javaIp.js';
export * from './javabeans.js';
export * from './systemSoftware.js';
export * from './topology.js';
export * from './zip.js';
+101 -5
View File
@@ -7,8 +7,12 @@
// filius.software.vermittlungsschicht.Weiterleitungstabelle,
// filius.software.firewall.Firewall and FirewallRule.
//
// Checked against Java (corpus.java.json, `system`). DHCP, NAT, the modem,
// file systems and applications are later layers.
// Also DHCP (client switch and server settings), a home router's static NAT
// table (port forwarding) and a modem's connection settings.
// Java counterparts as well: filius.software.dhcp.DHCPServer, ModemFirmware.
//
// Checked against Java (corpus.java.json, `system`). File systems and
// applications are later layers.
//
// A router's firewall is one object its constructor makes and hangs in two
// places: the installed applications and the configuration plug-ins of its
@@ -43,6 +47,26 @@ export interface FlsFirewall {
rules: FlsFirewallRule[];
}
/** The settings of Java's DHCPServer under English names (Java's own: javaDhcpServer in the topology test). */
export interface FlsDhcpServer {
isActive: boolean;
lowerBound: string | null;
upperBound: string | null;
gateway: string | null;
dnsServer: string | null;
useOwnSettings: boolean;
/** As Java keeps them after setStaticAssignedAddresses: MAC and IP of each entry. */
staticAssignedAddresses: { mac: string; ip: string }[];
}
export interface FlsModem {
/** 1 server, 2 client. */
mode: number;
port: number;
/** Java's ipAdresse: the host a client modem connects to. */
ipAddress: string | null;
}
export interface FlsSystemSoftware {
/**
* Switch: its SSID, undefined when the file has none (Java rolls one).
@@ -57,6 +81,14 @@ export interface FlsSystemSoftware {
manualRoutes?: (string | null)[][];
/** Vermittlungsrechner, Gateway. */
firewall?: FlsFirewall;
/** Every node with an IP stack: whether it asks a DHCP server for its address. */
dhcpClient?: boolean;
/** Every node with an IP stack. */
dhcpServer?: FlsDhcpServer;
/** Gateway: rows of protocol, public port, LAN address, LAN port — as the file has them. */
staticNAT?: (string | null)[][];
/** Modem. */
modem?: FlsModem;
}
type Warn = (message: string) => void;
@@ -88,6 +120,16 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
}
if (javaClass !== 'Switch' && javaClass !== 'Modem') {
result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), warn);
result.dhcpClient = boolean(value('DHCPKonfiguration'), false, 'DHCPKonfiguration', warn);
result.dhcpServer = dhcpServer(value('DHCPServer'), warn);
}
if (javaClass === 'Gateway') result.staticNAT = rowsOfStrings(value('staticNAT'));
if (javaClass === 'Modem') {
result.modem = {
mode: integer(value('mode'), 2, 'modem mode', warn),
port: integer(value('port'), 12_345, 'modem port', warn),
ipAddress: modemAddress(optionalString(value('ipAdresse'), 'modem ipAdresse', warn)),
};
}
if (javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway') {
result.firewall = firewall(software, warn);
@@ -101,9 +143,63 @@ function manualRoutes(table: JavaValue | undefined, warn: Warn): (string | null)
warn(`weiterleitungstabelle is ${describe(table)}, ignored`);
return [];
}
return listItems(table.properties.get('manuelleTabelle')).map((row) =>
listItems(row).map((cell) => (typeof cell === 'string' || cell === null ? cell : null)),
);
return rowsOfStrings(table.properties.get('manuelleTabelle'));
}
function modemAddress(value: string | null | undefined): string | null {
return value === undefined ? 'localhost' : value;
}
/** A list of String arrays (routes, static NAT); anything but a string is null. */
function rowsOfStrings(list: JavaValue | undefined): (string | null)[][] {
return listItems(list).map((row) => listItems(row).map((cell) => (typeof cell === 'string' ? cell : null)));
}
/** The DHCP server every IP stack's constructor makes: off, all addresses 0.0.0.0. */
function dhcpServer(server: JavaValue | undefined, warn: Warn): FlsDhcpServer {
const at = (property: string) => (isJavaObject(server) ? server.properties.get(property) : undefined);
const address = (property: string) => {
const value = optionalString(at(property), `DHCPServer ${property}`, warn);
return value === undefined ? '0.0.0.0' : value;
};
return {
isActive: boolean(at('aktiv'), false, 'DHCPServer aktiv', warn),
lowerBound: address('untergrenze'),
upperBound: address('obergrenze'),
gateway: address('gatewayip'),
dnsServer: address('dnsserverip'),
useOwnSettings: boolean(at('ownSettings'), false, 'DHCPServer ownSettings', warn),
staticAssignedAddresses: staticAssignments(at('staticAssignedAddresses'), warn),
};
}
/**
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
* MAC and IP from the first two parts. An entry with fewer throws inside the
* loop — XMLDecoder swallows it, and the entries after it are lost.
*/
function staticAssignments(list: JavaValue | undefined, warn: Warn): { mac: string; ip: string }[] {
const result: { mac: string; ip: string }[] = [];
for (const entry of listItems(list)) {
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
if (parts.length < 2) {
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP; Java stops reading here`);
break;
}
result.push({ mac: parts[0]!, ip: parts[1]! });
}
return result;
}
/**
* Character.isWhitespace: space, line and paragraph separators except the
* no-break ones (U+00A0, U+2007, U+202F), and the controls TAB, LF, VT, FF,
* CR, FS, GS, RS, US.
*/
const JAVA_WHITESPACE = /(?:(?![\u00A0\u2007\u202F])[\p{Zs}\p{Zl}\p{Zp}]|[\t\n\u000B\f\r\u001C-\u001F])+/u;
function javaWhitespaceSplit(text: string): string[] {
return text.split(JAVA_WHITESPACE).filter((part) => part !== '');
}
/** Every object the file reaches the router's one firewall by. */
+75 -1
View File
@@ -7,7 +7,15 @@ import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { decodeJavaBeans, readFlsArchive, readTopology, type FlsNodeClass, type FlsTopology } from '../src/index.js';
import {
decodeJavaBeans,
readFlsArchive,
readTopology,
type FlsDhcpServer,
type FlsModem,
type FlsNodeClass,
type FlsTopology,
} from '../src/index.js';
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, unknown>;
@@ -34,6 +42,23 @@ const TYPE_LABELS: Record<FlsNodeClass, string> = {
Modem: 'Modem',
};
/** Java's property names, as FlsOracle.java writes them. */
function javaDhcpServer(server: FlsDhcpServer) {
return {
aktiv: server.isActive,
untergrenze: server.lowerBound,
obergrenze: server.upperBound,
gatewayip: server.gateway,
dnsserverip: server.dnsServer,
ownSettings: server.useOwnSettings,
staticAssignedAddresses: server.staticAssignedAddresses.map((entry) => `${entry.mac} ${entry.ip}`),
};
}
function javaModem(modem: FlsModem) {
return { mode: modem.mode, port: modem.port, ipAdresse: modem.ipAddress };
}
/** The topology in the shape FlsOracle.java writes, less what Swing computes (label width and height). */
function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
return {
@@ -53,6 +78,12 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
system: {
...node.system,
...('ssid' in node.system ? { ssid: node.system.ssid ?? null } : {}),
...(node.system.dhcpServer === undefined
? {}
: {
dhcpServer: javaDhcpServer(node.system.dhcpServer),
}),
...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }),
...(node.system.firewall === undefined
? {}
: {
@@ -235,6 +266,49 @@ describe('readTopology rules', () => {
});
});
it('splits static DHCP assignments at Java whitespace and stops where Java throws', () => {
// U+3000 separates, U+00A0 does not; the third entry has no IP, so Java
// throws there and the fourth is never read.
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="DHCPServer">
<void property="staticAssignedAddresses"><object class="java.util.ArrayList">
<void method="add"><string>0A:00:00:00:00:01<char code="#3000"/>10.0.0.1<char code="#9"/>extra</string></void>
<void method="add"><string>0A:00:00:00:00:02 10.0.0.2<char code="#A0"/>x</string></void>
<void method="add"><string>0A:00:00:00:00:03</string></void>
<void method="add"><string>0A:00:00:00:00:04 10.0.0.4</string></void>
</object></void>
</void></void></object>`)}</object>`);
expect(nodes[0]!.system.dhcpServer).toEqual({
isActive: false,
lowerBound: '0.0.0.0',
upperBound: '0.0.0.0',
gateway: '0.0.0.0',
dnsServer: '0.0.0.0',
useOwnSettings: false,
staticAssignedAddresses: [
{ mac: '0A:00:00:00:00:01', ip: '10.0.0.1' },
{ mac: '0A:00:00:00:00:02', ip: '10.0.0.2\u00A0x' },
],
});
expect(warnings).toEqual([
'node 1: DHCPServer: static assignment "0A:00:00:00:00:03" has no MAC and IP; Java stops reading here',
]);
});
it("gives a modem Java's defaults and keeps an address the file sets to null", () => {
const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Modem"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Modem"><void property="systemSoftware">
<void property="ipAdresse"><null/></void><void property="mode"><int>1</int></void>
</void></object>`)}</object>`);
expect(nodes.map((node) => node.system.modem)).toEqual([
{ mode: 2, port: 12_345, ipAddress: 'localhost' },
{ mode: 1, port: 12_345, ipAddress: null },
]);
});
it("reads colors as Java's constructors do and keeps an invalid one unset", () => {
const colors = ['<int>16711935</int>', '<int>1</int><int>2</int><int>3</int>', '<int>1</int><int>2</int><int>300</int><int>4</int>'];
const { documentation, warnings } = topologyOf(`<object class="java.util.LinkedList"/><object class="java.util.LinkedList"/>
+10
View File
@@ -16,3 +16,13 @@ vermittlungsschicht
# The class name filius.hardware.knoten.Rechner, as the device classes of
# the .fls format are keyed in packages/fls (topology.ts and its test).
packages/fls/::rechner
# Property names of Java FILIUS's DHCPServer (aktiv, untergrenze, obergrenze,
# gatewayip, dnsserverip) and ModemFirmware (ipAdresse): read from the .fls
# file and written so by the Java oracle; our own types call them isActive,
# lowerBound, upperBound, gateway, dnsServer and ipAddress.
packages/fls/::aktiv
packages/fls/::untergrenze
packages/fls/::obergrenze
packages/fls/::gatewayip
packages/fls/::dnsserverip
packages/fls/::adresse
+3 -2
View File
@@ -25,8 +25,9 @@ virtuellen Bildschirm statt „headless".
kleingeschriebene für gewürfelt.)
* Bisher ausgelesen: Geräte (Klasse, Name, Beschriftung, Position,
Schnittstellen, bei Rechner/Notebook `useIPAsName`/`useMACAsName`), unter
`system` SSID, Lernzeit des Switches, RIP, manuelle Routen und die Firewall
von Router und Heimrouter; Kabel (Enden als Gerät/Schnittstelle),
`system` SSID, Lernzeit des Switches, RIP, manuelle Routen, die Firewall
von Router und Heimrouter, DHCP-Client und -Server, die statische
NAT-Tabelle des Heimrouters und die Verbindung eines Modems; Kabel (Enden als Gerät/Schnittstelle),
Doku-Elemente.
Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise.
+29
View File
@@ -15,11 +15,13 @@ import filius.hardware.knoten.InternetKnoten;
import filius.hardware.knoten.Knoten;
import filius.hardware.knoten.LokalerKnoten;
import filius.rahmenprogramm.SzenarioVerwaltung;
import filius.software.dhcp.DHCPServer;
import filius.software.firewall.Firewall;
import filius.software.firewall.FirewallRule;
import filius.software.system.Betriebssystem;
import filius.software.system.GatewayFirmware;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.system.ModemFirmware;
import filius.software.system.SwitchFirmware;
import filius.software.system.SystemSoftware;
import filius.software.system.VermittlungsrechnerBetriebssystem;
@@ -235,6 +237,33 @@ public class FlsOracle {
}
}
json.put("manualRoutes", routes);
json.put("dhcpClient", system.isDHCPKonfiguration());
DHCPServer server = system.getDHCPServer();
if (server != null) {
Map<String, Object> dhcp = new LinkedHashMap<>();
dhcp.put("aktiv", server.isAktiv());
dhcp.put("untergrenze", server.getUntergrenze());
dhcp.put("obergrenze", server.getObergrenze());
dhcp.put("gatewayip", server.getGatewayip());
dhcp.put("dnsserverip", server.getDnsserverip());
dhcp.put("ownSettings", server.isOwnSettings());
dhcp.put("staticAssignedAddresses", server.getStaticAssignedAddresses());
json.put("dhcpServer", dhcp);
}
}
if (software instanceof GatewayFirmware firmware) {
List<Object> rows = new ArrayList<>();
if (firmware.getStaticNAT() != null) {
for (String[] row : firmware.getStaticNAT()) rows.add(row == null ? null : Arrays.asList(row));
}
json.put("staticNAT", rows);
}
if (software instanceof ModemFirmware firmware) {
Map<String, Object> modem = new LinkedHashMap<>();
modem.put("mode", firmware.getMode());
modem.put("port", firmware.getPort());
modem.put("ipAdresse", firmware.getIpAdresse());
json.put("modem", modem);
}
Firewall firewall = software instanceof VermittlungsrechnerBetriebssystem system ? system.holeFirewall()
: software instanceof GatewayFirmware firmware ? firmware.holeFirewall() : null;