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
+2 -2
View File
@@ -49,8 +49,8 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` im Format von Javas Lesern, ohne ihre Fehler (leere Felder zählen, eine kurze Zeile fällt allein weg, Nachrichten mit leerem Text oder Betreff bleiben, `;` im Betreff bleibt, CRLF, beim Client die letzte vollständige Zeile); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft (13 benannte Java-Fehler) |
| `src/javaBeansWriter.ts` | — | neu: Schreiber für das `XMLDecoder`-Format, Gegenstück zu `javabeans.ts`; ausdrücklich typisierte Eingabe, geteilte Objekte über `id`/`idref`, was XML nicht fassen kann (und CR) als `<char code>`; von Java selbst geprüft (`BeansDump`) |
| `test/javaBeansWriter.test.ts`, `test/beans/` | — | neu: 11 Dateien des Schreibers, von Javas `XMLDecoder` gelesen und beschrieben; Erwartungen von Hand |
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); Kopf „Filius version: 2.14.0 (mdg-filius)“ |
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware und Dateisysteme (ohne Programme) wie beim Original (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel) |
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); installierte Programme nach den Dateien (Webserver, Tauschbörse und E-Mail-Server legen ihre Ordner nur an, wo sie fehlen; der DNS-Server liest `/dns/hosts` beim Installieren), je unter ihrem Schlüssel mit der Systemsoftware wie `installApp`, samt Einstellungen und persönlicher Firewall; Kopf „Filius version: 2.14.0 (mdg-filius)“ |
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware, Dateisysteme und Programme (ohne E-Mail-Konten, -Nachrichten und Adressbuch) wie beim Original (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel, jedes Filius-Programm mit Einstellungen abseits der Vorgabe samt persönlicher Firewall, ein von Java angelegter Ordner `/peer2peer`) |
| `src/importEmail.ts` | `Persistence/TopologyProjectStore.swift` (`registerJavaEmailConfigurations`, `assignedJavaEmailMessages`) | übertragen, Eingabe ist Javas Sicht: Server-Konten aus `/mailserver/konten.txt`, ohne Datei aus der Bean-Liste (Java verlöre sie); Client-Konto aus `/konten.txt`, ohne Datei aus der älteren `kontoListe` (Filius 2.14 liest sie nicht mehr); Konten und Nachrichten bleiben, wo Swift verwirft, was seine Prüfung nicht besteht; Vor- und Nachname getrennt; Empfangszeit nur aus der Datei (Swift nummeriert); gegen Swifts Modell geprüft |
| `test/importEmail.test.ts`, `test/email-*.swift.json` | `State/TopologyEmail.swift` (`decodeJavaServer`, `decodeJavaClient`) als Vergleich | neu: dieselben 43 Kontodateien mit Swifts Leser; wie Swift bis auf benannte Fälle, in denen Swift prüft und die ganze Datei verwirft, wo Java liest |
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
+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
+4 -4
View File
@@ -36,13 +36,13 @@ virtuellen Bildschirm statt „headless".
von Router und Heimrouter, DHCP-Client und -Server, die statische
NAT-Tabelle des Heimrouters, die Verbindung eines Modems und das
Dateisystem (Inhalte als Länge und SHA-256, damit die Referenz klein
bleibt; die Dateien von Router und Heimrouter erzeugt Javas Konstruktor aus
eigenen Vorlagen, sie stehen nicht in der Datei) und die installierten
Programme eines Rechners mit ihren Einstellungen (Klassen, die Filius
bleibt; bei Router und Heimrouter stehen vorn die Dateien, die Javas
Konstruktor aus eigenen Vorlagen erzeugt, dahinter die aus der Datei) und
die installierten Programme eines Rechners mit ihren Einstellungen (die
persönliche Firewall samt Regeln) (Klassen, die Filius
nicht mitbringt — eigene Anwendungen im Archiv unter `projekt/anwendungen`
— fehlen, weil das Werkzeug nur `konfiguration.xml` lädt); E-Mail: Konten des Servers aus der Bean-Liste und aus `/mailserver/konten.txt` (`kontenLaden`), Ordner und Adressbuch des Clients und sein Konto aus `/konten.txt` (`laden`); Kabel (Enden als Gerät/Schnittstelle),
Doku-Elemente.
Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise.
## Getter-Standardwerte
+10 -5
View File
@@ -309,15 +309,20 @@ public class FlsOracle {
settings.put("dropICMP", firewall.getDropICMP());
settings.put("filterSYNSegmentsOnly", firewall.getFilterSYNSegmentsOnly());
settings.put("filterUdp", firewall.getFilterUdp());
settings.put("rules", firewallRules(firewall));
json.put("firewall", settings);
}
return json;
}
/** A firewall's rules, each as source, masks, destination, port, protocol and action. */
static List<Object> firewallRules(Firewall firewall) {
List<Object> rules = new ArrayList<>();
for (FirewallRule rule : firewall.getRuleset()) {
rules.add(Arrays.asList(rule.srcIP, rule.srcMask, rule.destIP, rule.destMask, rule.port,
(int) rule.protocol, (int) rule.action));
}
settings.put("rules", rules);
json.put("firewall", settings);
}
return json;
return rules;
}
/**
@@ -388,7 +393,7 @@ public class FlsOracle {
json.put("dropICMP", firewall.getDropICMP());
json.put("filterSYNSegmentsOnly", firewall.getFilterSYNSegmentsOnly());
json.put("filterUdp", firewall.getFilterUdp());
json.put("rules", firewall.getRuleset().size());
json.put("rules", firewallRules(firewall));
}
result.add(json);
}