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:
@@ -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';
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user