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
+55 -1
View File
@@ -12,12 +12,14 @@ import {
decodeJavaBeans,
readFlsArchive,
readTopology,
type FlsApplication,
type FlsDhcpServer,
type FlsFileTree,
type FlsModem,
type FlsNodeClass,
type FlsTopology,
} from '../src/index.js';
import { EXPECTED_WARNINGS } from './expectedWarnings.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>;
@@ -63,6 +65,35 @@ function javaFileTree(tree: FlsFileTree): unknown {
}
}
/** FlsOracle.applications' shape: Java's property names, sorted by key. */
function javaApplications(applications: FlsApplication[]): unknown[] {
return [...applications]
.sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1))
.map((application) => ({
key: application.key,
class: application.javaClass,
...(application.isActive === undefined ? {} : { aktiv: application.isActive, port: application.port }),
...(application.targetPort === undefined
? {}
: { zielIPAdresse: application.targetAddress, zielPort: application.targetPort }),
...(application.recursiveResolutionEnabled === undefined
? {}
: { recursiveResolutionEnabled: application.recursiveResolutionEnabled }),
...(application.useVirtualHosts === undefined ? {} : { useVHost: application.useVirtualHosts }),
...(application.maxPeers === undefined ? {} : { maxTeilnehmerZahl: application.maxPeers }),
...(application.firewall === undefined
? {}
: {
activated: application.firewall.activated,
defaultPolicy: application.firewall.defaultPolicy,
dropICMP: application.firewall.dropICMP,
filterSYNSegmentsOnly: application.firewall.filterSYNSegmentsOnly,
filterUdp: application.firewall.filterUdp,
rules: application.firewall.rules.length,
}),
}));
}
/** Java's property names, as FlsOracle.java writes them. */
function javaDhcpServer(server: FlsDhcpServer) {
return {
@@ -106,6 +137,7 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
}),
...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }),
...(node.system.fileSystem === undefined ? {} : { fileSystem: javaFileTree(node.system.fileSystem) }),
...(node.system.applications === undefined ? {} : { applications: javaApplications(node.system.applications) }),
...(node.system.firewall === undefined
? {}
: {
@@ -164,7 +196,7 @@ describe('corpus: the topology as Java loads it', () => {
const expected = javaView[name] as { nodes: { bounds: number[] | null }[] };
expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(withoutTemplateFiles(expected));
expect(topology.warnings).toEqual([]);
expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []);
});
}
});
@@ -368,6 +400,28 @@ describe('readTopology rules', () => {
expect(warnings).toEqual(['node 2: file system: insert on a node is not read']);
});
it("gives installed applications Java's measured defaults for what the file leaves out", () => {
const put = (className: string, body = '') =>
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
${put('filius.software.www.WebServer')}
${put('filius.software.clientserver.ClientBaustein')}
${put('filius.software.dateiaustausch.PeerToPeerAnwendung')}
${put('filius.software.firewall.Firewall')}
${put('filius.software.dns.DNSServer', '<void property="aktiv"><boolean>true</boolean></void><void property="port"><int>5353</int></void>')}
${put('filius.software.lokal.Terminal')}
</void></void></object>`)}</object>`);
const byClass = new Map(nodes[0]!.system.applications!.map((application) => [application.javaClass, application]));
expect(byClass.get('filius.software.www.WebServer')).toMatchObject({ isActive: false, port: 80, useVirtualHosts: false });
expect(byClass.get('filius.software.clientserver.ClientBaustein')).toMatchObject({ targetAddress: null, targetPort: 55_555 });
expect(byClass.get('filius.software.dateiaustausch.PeerToPeerAnwendung')).toMatchObject({ maxPeers: 4 });
expect(byClass.get('filius.software.firewall.Firewall')?.firewall).toMatchObject({ activated: true, defaultPolicy: 0 });
expect(byClass.get('filius.software.dns.DNSServer')).toMatchObject({ isActive: true, port: 5353, recursiveResolutionEnabled: false });
expect(Object.keys(byClass.get('filius.software.lokal.Terminal')!).sort()).toEqual(['javaClass', 'key', 'source']);
});
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"/>')}