feat(fls): installed applications as Java loads them

readSystemSoftware lists a PC's or notebook's installed applications with
the settings the model needs: server applications switched on and their
port, the generic client's target, DNS recursion, web server virtual
hosts, Gnutella's peer limit and the personal firewall (on by default,
unlike a router's). Defaults the file leaves out are measured in Java.
All 23 corpus files match Java.

Two examples install an application of their own (Accumulator,
Broadcast), compiled into projekt/anwendungen; it is left out with a
warning, as this reader cannot run Java classes. It must survive a save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 06:05:20 +00:00
co-authored by Claude Opus 5.5
parent be3b332655
commit c48d15def4
12 changed files with 1438 additions and 9 deletions
+127 -3
View File
@@ -27,7 +27,16 @@
// not know Java's constructors and sees a placeholder per path; they are
// merged here.
import { isJavaObject, isUnresolved, listItems, mapEntries, type JavaObject, type JavaValue } from './javabeans.js';
import {
isJavaObject,
isUnresolved,
javaDefault,
listItems,
mapEntries,
simpleClassName,
type JavaObject,
type JavaValue,
} from './javabeans.js';
import type { FlsNodeClass } from './topology.js';
export interface FlsFirewallRule {
@@ -79,6 +88,55 @@ export type 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 installed application with the settings the model reads; e-mail contents are a later layer. */
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. */
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). */
maxPeers?: number;
/** Firewall: the personal firewall. */
firewall?: FlsFirewall;
/** 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).
@@ -101,6 +159,8 @@ export interface FlsSystemSoftware {
staticNAT?: (string | null)[][];
/** Modem. */
modem?: FlsModem;
/** Rechner, Notebook: the installed applications, in the file's order. */
applications?: FlsApplication[];
/** Rechner, Notebook: the root directory (named "root" by Java's constructor). */
fileSystem?: FlsFileTree;
}
@@ -125,6 +185,7 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
case 'Notebook':
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
result.fileSystem = fileSystem(value('dateisystem'), warn);
result.applications = applications(value('installierteAnwendungen'), warn);
break;
case 'Vermittlungsrechner':
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
@@ -165,6 +226,60 @@ 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 — is left
* out: this reader cannot run Java classes.
*/
function applications(map: JavaValue | undefined, warn: Warn): FlsApplication[] {
const result: FlsApplication[] = [];
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)) {
warn(`application ${javaClass} is not part of FILIUS (a project's own application), 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 = integer(at('maxTeilnehmerZahl'), Number(measured('maxTeilnehmerZahl')), 'Gnutella peers', warn);
break;
case 'filius.software.firewall.Firewall':
entry.firewall = firewallOf([application], true, warn);
break;
}
result.push(entry);
}
return result;
}
/**
* 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
@@ -282,8 +397,17 @@ function firewallObjects(software: JavaObject | undefined): JavaObject[] {
/** 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 firewallObjects(software)) {
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`);
@@ -295,7 +419,7 @@ function firewall(software: JavaObject | undefined, warn: Warn): FlsFirewall {
// setAllowRelatedPackets is the old name of setFilterSYNSegmentsOnly.
const filterSyn = at('filterSYNSegmentsOnly') ?? at('allowRelatedPackets');
return {
activated: boolean(at('activated'), false, 'firewall activated', warn),
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),