Files
mdg-filius/packages/fls/src/systemSoftware.ts
T
julian.kiedaischandClaude Opus 5.5 3548652687 feat(fls): read and write a project's own applications as their beans
A project's own applications (classes under projekt/anwendungen, installed
on a host) are no longer left out. The decoder now keeps the Java type of
every primitive — at setters, constructor and call arguments, also through
shared ids like Integer0 — and which properties an object's own getter
made. beanCapture.ts turns a decoded bean back into what the writer writes,
with the node's system software as a mark the writer replaces by the
system software it writes; what the format cannot say is refused, not
guessed (an unresolved value, a Field, a factory-made object, an object a
getter reaches elsewhere in the file). The reader keeps own applications as
captured beans; the corpus no longer warns.

The capture first took a setter with another object's getter result for an
own getter; the round-trip test found it. The Java oracle did not show
whether an application runs on its node's system software, so a mutation
writing the mark instead survived; it now reports onItsSystem (true for all
192 applications of the corpus).

Checked by Java: from the files written again, FILIUS loads Accumulator and
Broadcast installed, switched on and on their node's system software, as
in the original. The round-trip test covers every primitive, null, a
class, an array, shared objects, getter chains, constructed elements, map
reads and static constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:43:16 +00:00

588 lines
25 KiB
TypeScript

// The settings of a node's system software that the topology needs, read as
// Java FILIUS holds them after loading: a switch's SSID and learning time, a
// host's wireless SSID, a router's RIP switch, the manual routes, and the
// firewall of a router or home router.
// Java counterparts: filius.software.system.SwitchFirmware, Betriebssystem,
// VermittlungsrechnerBetriebssystem, GatewayFirmware, InternetKnotenBetriebssystem,
// filius.software.vermittlungsschicht.Weiterleitungstabelle,
// filius.software.firewall.Firewall and FirewallRule.
//
// 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.
//
// And the file system: Java's tree of DefaultMutableTreeNodes, a
// directory's user object its name, a file's a filius.software.system.Datei.
// A PC's or notebook's constructor leaves the root empty. A router's or home
// router's makes files from its own templates (the start page of the
// firewall's web interface), which are not in the file; what the file adds
// to that root (a web server's www.conf) comes after them.
//
// Checked against Java (corpus.java.json, `system`). Applications are a
// later layer.
//
// 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
// web server. XMLEncoder writes its settings wherever it meets it first —
// in the corpus, under the web server's `konfig` plug-in. The decoder does
// not know Java's constructors and sees a placeholder per path; they are
// merged here.
import { captureBean } from './beanCapture.js';
import { isDomainName } from './dnsHosts.js';
import type { BeanObject } from './javaBeansWriter.js';
import {
EMAIL_CLIENT_FILE,
EMAIL_SERVER_FILE,
javaContacts,
javaEmailAccounts,
javaEmails,
javaLegacyEmailAccounts,
readEmailClientFile,
readEmailServerFile,
type JavaContact,
type JavaEmail,
type JavaEmailAccount,
} from './javaEmail.js';
import { javaFileContent } from './javaFileSystem.js';
import {
isJavaObject,
isUnresolved,
javaDefault,
listItems,
mapEntries,
simpleClassName,
type JavaObject,
type JavaValue,
} from './javabeans.js';
import type { FlsNodeClass } from './topology.js';
export interface FlsFirewallRule {
srcIP: string | null;
srcMask: string | null;
destIP: string | null;
destMask: string | null;
/** A port, or -1 for all. */
port: number;
/** -1 all, 1 ICMP, 6 TCP, 17 UDP. */
protocol: number;
/** 0 drop, 1 accept. */
action: number;
}
export interface FlsFirewall {
activated: boolean;
/** 0 drop, 1 accept. */
defaultPolicy: number;
dropICMP: boolean;
filterSYNSegmentsOnly: boolean;
filterUdp: boolean;
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 type FlsFileTree =
| { kind: 'directory'; name: string | null; children: FlsFileTree[] }
| { kind: 'file'; name: string | null; type: string | null; content: string | null; children: FlsFileTree[] }
/** A node whose user object is neither a name nor a file. */
| { kind: 'other'; className: string; children: FlsFileTree[] };
/** The applications FILIUS 2.14 can install (config/Desktop_*.txt). */
export const FILIUS_APPLICATION_CLASSES = [
'filius.software.clientserver.ClientBaustein',
'filius.software.clientserver.ServerBaustein',
'filius.software.dateiaustausch.PeerToPeerAnwendung',
'filius.software.dns.DNSServer',
'filius.software.email.EmailAnwendung',
'filius.software.email.EmailServer',
'filius.software.firewall.Firewall',
'filius.software.lokal.FileExplorer',
'filius.software.lokal.ImageViewer',
'filius.software.lokal.Terminal',
'filius.software.lokal.TextEditor',
'filius.software.www.WebBrowser',
'filius.software.www.WebServer',
] as const;
export type FlsApplicationClass = (typeof FILIUS_APPLICATION_CLASSES)[number];
/** Server applications (ServerAnwendung): they listen on a port and can be switched on. */
const SERVER_APPLICATIONS = new Set<string>([
'filius.software.clientserver.ServerBaustein',
'filius.software.dns.DNSServer',
'filius.software.www.WebServer',
]);
/** An application of the project's own: its key, class and bean, which refers to its node's system software by a mark. */
export interface FlsOwnApplication {
key: string | null;
javaClass: string;
bean: BeanObject;
}
/** An installed application with the settings the model reads. */
export interface FlsApplication {
/** The key in installierteAnwendungen — the class name in every corpus file. */
key: string | null;
javaClass: FlsApplicationClass;
/** Server applications: switched on (Java's aktiv) and the port; EmailServer: switched on. */
isActive?: boolean;
port?: number;
/** ClientBaustein: the server it talks to (Java's zielIPAdresse, zielPort). */
targetAddress?: string | null;
targetPort?: number;
/** DNSServer: whether it asks other servers for names it does not know. */
recursiveResolutionEnabled?: boolean;
/** WebServer: serve by host name (Java's useVHost). */
useVirtualHosts?: boolean;
/**
* PeerToPeerAnwendung: how many peers it keeps (Java's maxTeilnehmerZahl);
* null when the file has none — Java's constructor rolls 3, 4 or 5.
*/
maxPeers?: number | null;
/** Firewall: the personal firewall. */
firewall?: FlsFirewall;
/** EmailServer: its domain ("filius.de" for one that is not a domain name, as Java's setter keeps it). */
mailDomain?: string;
/** EmailServer: the accounts of the bean list. */
beanAccounts?: JavaEmailAccount[];
/**
* EmailServer: the accounts of /mailserver/konten.txt, which Java reads at
* every simulation start — null without the file (Java then starts with
* none).
*/
fileAccounts?: JavaEmailAccount[] | null;
/** EmailAnwendung: its folders (Java's empfangene, gesendete, erstellte Nachrichten). */
received?: JavaEmail[];
sent?: JavaEmail[];
drafts?: JavaEmail[];
addressBook?: JavaContact[];
/** EmailAnwendung: the account of /konten.txt, null without one. */
fileAccount?: JavaEmailAccount | null;
/** EmailAnwendung: the accounts of an older file's kontoListe, which FILIUS 2.14 ignores. */
legacyAccounts?: JavaEmailAccount[];
/** The application's JavaBean, for the e-mail layer. */
source: JavaObject;
}
export interface FlsSystemSoftware {
/**
* Switch: its SSID, undefined when the file has none (Java rolls one).
* Rechner, Notebook: the wireless network it joins, null for none.
*/
ssid?: string | null | undefined;
/** Switch: how long it keeps a learned MAC address, in milliseconds. */
retentionTime?: number;
/** Vermittlungsrechner. */
ripEnabled?: boolean;
/** Every node with an IP stack: rows of destination, mask, gateway, interface address — as the file has them. */
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;
/** Rechner, Notebook: the installed applications, in the file's order. */
applications?: FlsApplication[];
/**
* Rechner, Notebook: the installed applications FILIUS does not ship — a
* project's own, compiled into projekt/anwendungen — kept as their beans,
* to be written again as the file had them.
*/
ownApplications?: FlsOwnApplication[];
/**
* Every node with an IP stack: the root directory (named "root" by Java's
* constructor) as the file has it. A router's or home router's holds only
* what the file adds to the constructor's, see `templateFiles`.
*/
fileSystem?: FlsFileTree;
/**
* Vermittlungsrechner, Gateway: whether the files the constructor makes
* stay in front of the file's — false when the file sets a root of its own.
*/
templateFiles?: boolean;
}
type Warn = (message: string) => void;
const FIREWALL_CLASS = 'filius.software.firewall.Firewall';
const WEB_SERVER_CLASS = 'filius.software.www.WebServer';
export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass, warn: Warn): FlsSystemSoftware {
const system = device.properties.get('systemSoftware');
const software = isJavaObject(system) ? system : undefined;
const value = (property: string) => software?.properties.get(property);
const result: FlsSystemSoftware = {};
switch (javaClass) {
case 'Switch':
result.ssid = optionalString(value('SSID'), 'SSID', warn);
result.retentionTime = integer(value('retentionTime'), 300_000, 'retentionTime', warn);
break;
case 'Rechner':
case 'Notebook':
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
result.fileSystem = fileSystem(value('dateisystem'), warn);
[result.applications, result.ownApplications] = applications(value('installierteAnwendungen'), result.fileSystem, software, warn);
break;
case 'Vermittlungsrechner':
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
break;
case 'Gateway':
case 'Modem':
break;
}
if (javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway') {
result.fileSystem = fileSystem(value('dateisystem'), warn);
result.templateFiles = constructedRoot(value('dateisystem'));
}
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);
}
return result;
}
function manualRoutes(table: JavaValue | undefined, warn: Warn): (string | null)[][] {
if (table === undefined || table === null) return [];
if (!isJavaObject(table)) {
warn(`weiterleitungstabelle is ${describe(table)}, ignored`);
return [];
}
return rowsOfStrings(table.properties.get('manuelleTabelle'));
}
function modemAddress(value: string | null | undefined): string | null {
return value === undefined ? 'localhost' : value;
}
/**
* installierteAnwendungen: key → application. A class FILIUS does not ship —
* a project's own application, compiled into projekt/anwendungen — cannot
* run here; it is kept as its bean (beanCapture.ts), or left out with a
* warning where the bean cannot be written again.
*/
function applications(
map: JavaValue | undefined,
root: FlsFileTree,
software: JavaObject | undefined,
warn: Warn,
): [FlsApplication[], FlsOwnApplication[]] {
const result: FlsApplication[] = [];
const own: FlsOwnApplication[] = [];
for (const [key, application] of mapEntries(map)) {
if (!isJavaObject(application)) {
warn(`application ${describe(key)} is ${describe(application)}, left out`);
continue;
}
const javaClass = application.className;
if (!(FILIUS_APPLICATION_CLASSES as readonly string[]).includes(javaClass)) {
const captured = captureBean(application, software);
if ('bean' in captured) own.push({ key: typeof key === 'string' ? key : null, javaClass, bean: captured.bean });
else warn(`application ${javaClass}, a project's own, cannot be written again: ${captured.failure}; left out`);
continue;
}
const simple = simpleClassName(javaClass);
const at = (property: string) => application.properties.get(property);
const measured = (property: string) => javaDefault(simple, property);
const entry: FlsApplication = {
key: typeof key === 'string' ? key : null,
javaClass: javaClass as FlsApplicationClass,
source: application,
};
if (SERVER_APPLICATIONS.has(javaClass)) {
entry.isActive = boolean(at('aktiv'), measured('aktiv') === true, `${simple} aktiv`, warn);
entry.port = integer(at('port'), Number(measured('port')), `${simple} port`, warn);
}
switch (javaClass) {
case 'filius.software.clientserver.ClientBaustein': {
const target = optionalString(at('zielIPAdresse'), 'ClientBaustein zielIPAdresse', warn);
entry.targetAddress = target === undefined ? null : target;
entry.targetPort = integer(at('zielPort'), Number(measured('zielPort')), 'ClientBaustein zielPort', warn);
break;
}
case 'filius.software.dns.DNSServer':
entry.recursiveResolutionEnabled = boolean(at('recursiveResolutionEnabled'), false, 'DNSServer recursion', warn);
break;
case 'filius.software.www.WebServer':
entry.useVirtualHosts = boolean(at('useVHost'), measured('useVHost') === true, 'WebServer useVHost', warn);
break;
case 'filius.software.dateiaustausch.PeerToPeerAnwendung':
entry.maxPeers =
at('maxTeilnehmerZahl') === undefined ? null : integer(at('maxTeilnehmerZahl'), 0, 'Gnutella peers', warn);
break;
case 'filius.software.firewall.Firewall':
entry.firewall = firewallOf([application], true, warn);
break;
case 'filius.software.email.EmailServer': {
entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn);
const domain = optionalString(at('mailDomain'), 'EmailServer mailDomain', warn);
entry.mailDomain = typeof domain === 'string' && isDomainName(domain) ? domain : 'filius.de';
entry.beanAccounts = javaEmailAccounts(at('listeBenutzerkonten'), warn);
const file = javaFileContent(root, EMAIL_SERVER_FILE);
entry.fileAccounts = file === undefined ? null : readEmailServerFile(file, warn);
break;
}
case 'filius.software.email.EmailAnwendung': {
entry.received = javaEmails(at('empfangeneNachrichten'), warn);
entry.sent = javaEmails(at('gesendeteNachrichten'), warn);
entry.drafts = javaEmails(at('erstellteNachrichten'), warn);
entry.addressBook = javaContacts(at('adressbuch'), warn);
const file = javaFileContent(root, EMAIL_CLIENT_FILE);
entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn);
entry.legacyAccounts = javaLegacyEmailAccounts(at('kontoListe'), warn);
break;
}
}
result.push(entry);
}
return [result, own];
}
/**
* Dateisystem: its root is `arbeitsVerzeichnis` in files up to 1.10 and
* `root` after. The constructor's root is a node named "root"; a node the
* file makes with `new DefaultMutableTreeNode()` has no user object.
*/
function fileSystem(system: JavaValue | undefined, warn: Warn): FlsFileTree {
const root = fileSystemRoot(system);
if (root === undefined) return { kind: 'directory', name: 'root', children: [] };
return fileTreeNode(root, true, warn);
}
function fileSystemRoot(system: JavaValue | undefined): JavaValue | undefined {
const at = (property: string) => (isJavaObject(system) ? system.properties.get(property) : undefined);
return at('root') ?? at('arbeitsVerzeichnis');
}
/** Whether the root is the one the constructor made: none set, or reached through the getter. */
function constructedRoot(system: JavaValue | undefined): boolean {
if (system !== undefined && !(isJavaObject(system) && system.fromGetter)) return false;
const root = fileSystemRoot(system);
return root === undefined || (isJavaObject(root) && root.fromGetter);
}
function fileTreeNode(node: JavaValue, constructorRoot: boolean, warn: Warn): FlsFileTree {
if (!isJavaObject(node)) {
warn(`file system: a node is ${describe(node)}, left empty`);
return { kind: 'other', className: 'unknown', children: [] };
}
for (const call of node.calls) {
if (call.method !== 'add') warn(`file system: ${call.method} on a node is not read`);
}
const children = listItems(node).map((child) => fileTreeNode(child, false, warn));
const userObject = node.properties.has('userObject')
? node.properties.get('userObject')!
: node.fromGetter && constructorRoot
? 'root'
: (node.args[0] ?? null);
if (userObject === null || typeof userObject === 'string') return { kind: 'directory', name: userObject, children };
if (isJavaObject(userObject) && userObject.className === 'filius.software.system.Datei') {
// new Datei(name, typ, inhalt), then the setters.
const [name, type, content] = userObject.args.length === 3 ? userObject.args : [];
const text = (property: string, fromConstructor: JavaValue | undefined) => {
const value = userObject.properties.get(property) ?? fromConstructor ?? null;
if (typeof value === 'string' || value === null) return value;
warn(`file system: file ${property} is ${describe(value)}, ignored`);
return null;
};
return {
kind: 'file',
name: text('name', name),
type: text('dateiTyp', type),
content: text('dateiInhalt', content),
children,
};
}
return { kind: 'other', className: isJavaObject(userObject) ? userObject.className : typeof userObject, children };
}
/** 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 makes Java throw
* inside the loop — XMLDecoder swallows it, and the entries after it are
* lost. That error is not copied: such an entry alone is left out.
*/
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, left out`);
continue;
}
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. */
function firewallObjects(software: JavaObject | undefined): JavaObject[] {
const found: JavaObject[] = [];
const applications = new Map(mapEntries(software?.properties.get('installierteAnwendungen')));
const installed = applications.get(FIREWALL_CLASS);
if (isJavaObject(installed)) found.push(installed);
const webServer = applications.get(WEB_SERVER_CLASS);
if (isJavaObject(webServer)) {
for (const [, plugin] of mapEntries(webServer.properties.get('plugins'))) {
const pluginFirewall = isJavaObject(plugin) ? plugin.properties.get('firewall') : undefined;
if (isJavaObject(pluginFirewall) && !found.includes(pluginFirewall)) found.push(pluginFirewall);
}
}
return found;
}
/** The firewall as the router's constructor leaves it (off, drop), then the file's settings. */
function firewall(software: JavaObject | undefined, warn: Warn): FlsFirewall {
return firewallOf(firewallObjects(software), false, warn);
}
/**
* A firewall's settings from every object the file reaches it by. `activated`
* starts as the constructor leaves it: a router's is switched off, a PC's
* personal firewall (new Firewall()) is on.
*/
function firewallOf(objects: JavaObject[], activatedByDefault: boolean, warn: Warn): FlsFirewall {
const properties = new Map<string, JavaValue>();
for (const object of objects) {
for (const [property, value] of object.properties) {
if (properties.has(property) && properties.get(property) !== value) {
warn(`firewall: ${property} is set in two places, the later one is used`);
}
properties.set(property, value);
}
}
const at = (property: string) => properties.get(property);
// setAllowRelatedPackets is the old name of setFilterSYNSegmentsOnly.
const filterSyn = at('filterSYNSegmentsOnly') ?? at('allowRelatedPackets');
return {
activated: boolean(at('activated'), activatedByDefault, 'firewall activated', warn),
defaultPolicy: integer(at('defaultPolicy'), 0, 'firewall defaultPolicy', warn),
dropICMP: boolean(at('dropICMP'), false, 'firewall dropICMP', warn),
filterSYNSegmentsOnly: boolean(filterSyn, true, 'firewall filterSYNSegmentsOnly', warn),
filterUdp: boolean(at('filterUdp'), true, 'firewall filterUdp', warn),
rules: listItems(at('ruleset')).flatMap((rule) => (isJavaObject(rule) ? [firewallRule(rule, warn)] : [])),
};
}
/**
* FirewallRule: public fields set one by one (`getField(name).set`), or the
* seven-argument constructor. Defaults: all addresses empty, all ports, TCP,
* drop.
*/
function firewallRule(rule: JavaObject, warn: Warn): FlsFirewallRule {
const [srcIP, srcMask, destIP, destMask, port, protocol, action] = rule.args.length === 7 ? rule.args : [];
const field = (name: string, fromConstructor: JavaValue | undefined) => rule.properties.get(name) ?? fromConstructor;
const text = (name: string, fromConstructor: JavaValue | undefined) => {
const value = field(name, fromConstructor) ?? '';
if (typeof value === 'string' || value === null) return value;
warn(`firewall rule: ${name} is ${describe(value)}, used ""`);
return '';
};
return {
srcIP: text('srcIP', srcIP),
srcMask: text('srcMask', srcMask),
destIP: text('destIP', destIP),
destMask: text('destMask', destMask),
port: integer(field('port', port), -1, 'firewall rule port', warn),
protocol: integer(field('protocol', protocol), 6, 'firewall rule protocol', warn),
action: integer(field('action', action), 0, 'firewall rule action', warn),
};
}
function optionalString(value: JavaValue | undefined, name: string, warn: Warn): string | null | undefined {
if (value === undefined || value === null || typeof value === 'string') return value;
warn(`${name} is ${describe(value)}, ignored`);
return undefined;
}
function boolean(value: JavaValue | undefined, fallback: boolean, name: string, warn: Warn): boolean {
if (value === undefined) return fallback;
if (typeof value === 'boolean') return value;
warn(`${name} is ${describe(value)}, used ${fallback}`);
return fallback;
}
function integer(value: JavaValue | undefined, fallback: number, name: string, warn: Warn): number {
if (value === undefined) return fallback;
if (typeof value === 'number' && Number.isInteger(value)) return value;
warn(`${name} is ${describe(value)}, used ${fallback}`);
return fallback;
}
function describe(value: JavaValue): string {
if (isUnresolved(value)) return `unresolved (${value.reason})`;
if (isJavaObject(value)) return `an object of ${value.className}`;
return typeof value === 'bigint' ? `${value}` : JSON.stringify(value);
}