feat(fls): write the installed applications of a .fls, loaded by Java as the original

topologyWriter.ts puts each installed application of a PC or notebook under
its key and gives it the system software, as installApp does, with its
settings: a server's switch and port, a client's target, DNS recursion,
virtual hosts, the peer count, the personal firewall. The applications
follow the files, as in Java's own files: the web server, the peer-to-peer
application and the e-mail server make their directories only where the
files lack them, and the DNS server reads /dns/hosts when it is installed.
E-mail accounts, messages and address books are the next step.

The Java oracle now reports a personal firewall's rules in full, not their
number (the corpus has none; its result is byte-equal).

Checked by Java against Java: every corpus file, read and written again,
loads with the same applications as the original. The synthetic topology
adds every FILIUS application with settings away from its defaults, a
personal firewall with a rule, an own index.html (Java makes no second) and
a missing /peer2peer directory, which Java makes at the end. 12 mutation
probes, among them files and applications in reverse order, each loaded by
Java, were all caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 08:36:00 +00:00
co-authored by Claude Opus 5.5
parent 84ea76e880
commit ccec734143
7 changed files with 1861 additions and 142 deletions
+55 -6
View File
@@ -14,7 +14,13 @@
// which holds the same object (its key among the installed applications
// differs: Firewall on a router, NatGateway on a home router). Files are
// added to the constructor's root, as XMLEncoder adds them — on a router
// after the files its constructor makes. Applications are a later layer.
// after the files its constructor makes. The installed applications of a PC
// or notebook follow the files, as in Java's own files: the constructors of
// the web server, the peer-to-peer application and the e-mail server make
// their directories only where the files lack them, and the DNS server reads
// /dns/hosts when it is installed. Each is put under its key and given the
// system software, as installApp does. E-mail accounts, messages and address
// books are a later layer.
//
// Checked by Java: every corpus file, read and written again, loads in Java
// FILIUS to the same topology and system software (topologyWriter.test.ts,
@@ -32,14 +38,19 @@ import {
type BeanValue,
short,
} from './javaBeansWriter.js';
import type { FlsFileTree, FlsFirewall, FlsSystemSoftware } from './systemSoftware.js';
import type { FlsApplication, FlsFileTree, FlsFirewall, FlsSystemSoftware } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNode, FlsNodeClass } from './topology.js';
/** The version line of the files this writer makes: the format of FILIUS 2.14. */
export const WRITTEN_VERSION_LINE = 'Filius version: 2.14.0 (mdg-filius)';
/** The system software's settings and files; its applications are a later layer. */
export type SystemToWrite = Omit<FlsSystemSoftware, 'applications'>;
/** An installed application; e-mail accounts, messages and address books are a later layer. */
export type ApplicationToWrite = Omit<FlsApplication, 'source'>;
/** The system software's settings, files and applications. */
export type SystemToWrite = Omit<FlsSystemSoftware, 'applications'> & {
readonly applications?: readonly ApplicationToWrite[];
};
export type NodeToWrite = Pick<
FlsNode,
@@ -140,7 +151,14 @@ function device(node: NodeToWrite, ports: BeanValue[]): BeanObject {
),
);
}
statements.push({ get: 'systemSoftware', statements: systemStatements(node.system, node.javaClass) });
// Named for the applications, which are given it.
const system = (node.system.applications?.length ?? 0) > 0 ? reference('SystemSoftware') : undefined;
const systemStatementList = systemStatements(node.system, node.javaClass, system);
statements.push(
system === undefined
? { get: 'systemSoftware', statements: systemStatementList }
: { get: 'systemSoftware', statements: systemStatementList, as: system },
);
return object(`filius.hardware.knoten.${node.javaClass}`, statements);
}
@@ -150,7 +168,7 @@ const WEB_SERVER_CLASS = 'filius.software.www.WebServer';
* The settings the file has, each set on the constructor's system software.
* A switch's SSID the file lacked stays unwritten, so Java rolls one as it did.
*/
function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass): BeanStatement[] {
function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass, self: BeanReference | undefined): BeanStatement[] {
const statements: BeanStatement[] = [];
if (system.firewall !== undefined) {
statements.push({
@@ -167,6 +185,15 @@ function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass): BeanS
});
}
if (system.fileSystem !== undefined) statements.push(fileSystemStatement(system.fileSystem, system.templateFiles !== false));
if (self !== undefined && system.applications !== undefined) {
statements.push({
get: 'installierteAnwendungen',
statements: system.applications.map((application) => ({
call: 'put',
args: [application.key, object(application.javaClass, applicationStatements(application, self))],
})),
});
}
if (system.dhcpServer !== undefined) {
const server = system.dhcpServer;
statements.push({
@@ -236,6 +263,28 @@ function userObject(tree: FlsFileTree): BeanValue {
return tree.kind === 'directory' ? tree.name : null;
}
/**
* An application's settings, after the system software it runs on. A
* peer-to-peer application's peer count the file lacked stays unwritten, so
* Java rolls one as it did.
*/
function applicationStatements(application: ApplicationToWrite, system: BeanReference): BeanStatement[] {
const statements: BeanStatement[] = [{ set: 'systemSoftware', value: system }];
if (application.isActive !== undefined) statements.push({ set: 'aktiv', value: application.isActive });
if (application.port !== undefined) statements.push({ set: 'port', value: int(application.port) });
if (application.targetAddress !== undefined) statements.push({ set: 'zielIPAdresse', value: application.targetAddress });
if (application.targetPort !== undefined) statements.push({ set: 'zielPort', value: int(application.targetPort) });
if (application.recursiveResolutionEnabled !== undefined) {
statements.push({ set: 'recursiveResolutionEnabled', value: application.recursiveResolutionEnabled });
}
if (application.useVirtualHosts !== undefined) statements.push({ set: 'useVHost', value: application.useVirtualHosts });
if (application.maxPeers !== undefined && application.maxPeers !== null) {
statements.push({ set: 'maxTeilnehmerZahl', value: int(application.maxPeers) });
}
if (application.firewall !== undefined) statements.push(...firewallStatements(application.firewall));
return statements;
}
/** A LinkedList of String arrays: routes, static NAT. */
function rows(table: readonly (readonly (string | null)[])[]): BeanObject {
return listOf(table.map((row) => ({ kind: 'array', componentClass: 'java.lang.String', items: [...row] })));
+9 -1
View File
@@ -91,7 +91,15 @@ function javaApplications(applications: FlsApplication[]): unknown[] {
dropICMP: application.firewall.dropICMP,
filterSYNSegmentsOnly: application.firewall.filterSYNSegmentsOnly,
filterUdp: application.firewall.filterUdp,
rules: application.firewall.rules.length,
rules: application.firewall.rules.map((rule) => [
rule.srcIP,
rule.srcMask,
rule.destIP,
rule.destMask,
rule.port,
rule.protocol,
rule.action,
]),
}),
...(application.mailDomain === undefined
? {}
+73 -5
View File
@@ -1,8 +1,8 @@
// Every corpus file, read and written again: Java FILIUS loads the written
// file (tools/java-oracle, written.java.json) to the same topology as the
// original (corpus.java.json) — devices, names, positions, interfaces,
// system software and files (without applications), cables and
// documentation. Java against Java: no mapping of this package stands
// system software, files and applications (without e-mail accounts,
// messages and address books), cables and documentation. Java against Java: no mapping of this package stands
// between the two.
//
// UPDATE_WRITTEN=1 writes the files to test/written/ (not checked in); the
@@ -43,10 +43,14 @@ type JavaFile = { success: boolean; nodes: JavaNode[]; cables: unknown[]; docume
const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, JavaFile>;
const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record<string, JavaFile>;
/** The system software without its applications, which are a later layer. */
/**
* The system software without what a later layer writes: the e-mail
* programs' domain, accounts, messages and address book.
*/
function systemOf(system: Record<string, unknown>) {
const { applications: _applications, ...settings } = system;
return settings;
if (system.applications === undefined) return system;
const applications = (system.applications as Record<string, unknown>[]).map(({ mailDomain: _domain, beanAccounts: _accounts, received: _received, sent: _sent, drafts: _drafts, addressBook: _book, ...rest }) => rest);
return { ...system, applications };
}
/**
@@ -136,8 +140,45 @@ const SYNTHETIC: TopologyToWrite = {
],
},
{ kind: 'file', name: 'untyped', type: null, content: null, children: [] },
{ kind: 'directory', name: 'webserver', children: [{ kind: 'file', name: 'index.html', type: 'html', content: '<p>own</p>', children: [] }] },
{ kind: 'directory', name: 'dns', children: [{ kind: 'file', name: 'hosts', type: null, content: 'a.de. A 3600 10.0.0.1\n', children: [] }] },
],
},
applications: [
{
key: 'filius.software.firewall.Firewall',
javaClass: 'filius.software.firewall.Firewall',
firewall: {
activated: false,
defaultPolicy: 1,
dropICMP: true,
filterSYNSegmentsOnly: false,
filterUdp: false,
rules: [{ srcIP: '10.0.0.0', srcMask: '255.0.0.0', destIP: '', destMask: '', port: 22, protocol: 6, action: 1 }],
},
},
{ key: 'filius.software.www.WebServer', javaClass: 'filius.software.www.WebServer', isActive: true, port: 8080, useVirtualHosts: true },
{
key: 'filius.software.dns.DNSServer',
javaClass: 'filius.software.dns.DNSServer',
isActive: true,
port: 5353,
recursiveResolutionEnabled: true,
},
{ key: 'filius.software.clientserver.ServerBaustein', javaClass: 'filius.software.clientserver.ServerBaustein', isActive: true, port: 7777 },
{
key: 'filius.software.clientserver.ClientBaustein',
javaClass: 'filius.software.clientserver.ClientBaustein',
targetAddress: '10.0.0.9',
targetPort: 7777,
},
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', javaClass: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxPeers: 7 },
{ key: 'filius.software.lokal.Terminal', javaClass: 'filius.software.lokal.Terminal' },
{ key: 'filius.software.lokal.TextEditor', javaClass: 'filius.software.lokal.TextEditor' },
{ key: 'filius.software.lokal.FileExplorer', javaClass: 'filius.software.lokal.FileExplorer' },
{ key: 'filius.software.lokal.ImageViewer', javaClass: 'filius.software.lokal.ImageViewer' },
{ key: 'filius.software.www.WebBrowser', javaClass: 'filius.software.www.WebBrowser' },
],
},
},
{
@@ -298,8 +339,35 @@ describe('a topology beyond the corpus, as Java loads it', () => {
children: [
{ directory: 'docs', children: [javaFile('a.txt', 'text', FILE_CONTENT), { directory: null, children: [] }] },
javaFile('untyped', null, null),
{ directory: 'webserver', children: [javaFile('index.html', 'html', '<p>own</p>')] },
{ directory: 'dns', children: [javaFile('hosts', null, 'a.de. A 3600 10.0.0.1\n')] },
// Made by the peer-to-peer application, whose directory the files lack.
{ directory: 'peer2peer', children: [] },
],
},
// In the order of Java's key.
applications: [
{ key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', zielIPAdresse: '10.0.0.9', zielPort: 7777 },
{ key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', aktiv: true, port: 7777 },
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 7 },
{ key: 'filius.software.dns.DNSServer', class: 'filius.software.dns.DNSServer', aktiv: true, port: 5353, recursiveResolutionEnabled: true },
{
key: 'filius.software.firewall.Firewall',
class: 'filius.software.firewall.Firewall',
activated: false,
defaultPolicy: 1,
dropICMP: true,
filterSYNSegmentsOnly: false,
filterUdp: false,
rules: [['10.0.0.0', '255.0.0.0', '', '', 22, 6, 1]],
},
{ key: 'filius.software.lokal.FileExplorer', class: 'filius.software.lokal.FileExplorer' },
{ key: 'filius.software.lokal.ImageViewer', class: 'filius.software.lokal.ImageViewer' },
{ key: 'filius.software.lokal.Terminal', class: 'filius.software.lokal.Terminal' },
{ key: 'filius.software.lokal.TextEditor', class: 'filius.software.lokal.TextEditor' },
{ key: 'filius.software.www.WebBrowser', class: 'filius.software.www.WebBrowser' },
{ key: 'filius.software.www.WebServer', class: 'filius.software.www.WebServer', aktiv: true, port: 8080, useVHost: true },
],
},
},
{
File diff suppressed because it is too large Load Diff