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

topologyWriter.ts adds each node's file tree to the root its constructor
makes, as XMLEncoder does (arbeitsVerzeichnis, files as new Datei(name,
typ, inhalt)); a root of its own is set anew.

The reader now reads the files of routers and home routers too: their
constructor makes files from templates (the web interface), and the file
adds to them (a web server's www.conf). systemSoftware.ts keeps what the
file adds, and templateFiles says whether the constructor's files stay in
front (false when the file sets a root of its own). The topology test finds
our part at the end of Java's tree and the template in front, the same for
every router of a class across the corpus.

Checked by Java against Java: every corpus file, read and written again,
loads with the same file systems as the original. The synthetic topology
adds a nested tree with a renamed root, a nameless directory, a file
without type, content with CR, CRLF and beyond-BMP characters, router files
after the template and a home router with a root of its own. 9 mutation
probes, 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:27:41 +00:00
co-authored by Claude Opus 5.5
parent facad5843e
commit 84ea76e880
6 changed files with 1755 additions and 123 deletions
+3 -3
View File
@@ -39,7 +39,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet; gegen Swifts importiertes Modell auf 13 Dateien geprüft | | `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, begründete Abweichungen; im Dateisystem ausgenommen nur Swifts versteckte Zustandsdateien), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | | `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, begründete Abweichungen; im Dateisystem ausgenommen nur Swifts versteckte Zustandsdateien), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) |
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem (bei Router und Heimrouter das, was die Datei zu den Vorlagen des Konstruktors hinzufügt, oder eine eigene Wurzel, die sie ersetzt), installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen | | `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen |
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter, die Zeichen-Enden eines Kabels | | `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter, die Zeichen-Enden eines Kabels |
@@ -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/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`) | | `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 | | `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); Kopf „Filius version: 2.14.0 (mdg-filius)“ | | `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 und Systemsoftware (ohne Dateien und 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) | | `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/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 | | `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 | | `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 | | `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 |
+33 -8
View File
@@ -11,11 +11,12 @@
// table (port forwarding) and a modem's connection settings. // table (port forwarding) and a modem's connection settings.
// Java counterparts as well: filius.software.dhcp.DHCPServer, ModemFirmware. // Java counterparts as well: filius.software.dhcp.DHCPServer, ModemFirmware.
// //
// And the file system of a PC or notebook: Java's tree of // And the file system: Java's tree of DefaultMutableTreeNodes, a
// DefaultMutableTreeNodes, a directory's user object its name, a file's a // directory's user object its name, a file's a filius.software.system.Datei.
// filius.software.system.Datei. Routers and home routers have files too, but // A PC's or notebook's constructor leaves the root empty. A router's or home
// Java's constructor makes them from its own templates (the start page of // router's makes files from its own templates (the start page of the
// the firewall's web interface); they are not in the file. // 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 // Checked against Java (corpus.java.json, `system`). Applications are a
// later layer. // later layer.
@@ -198,8 +199,17 @@ export interface FlsSystemSoftware {
modem?: FlsModem; modem?: FlsModem;
/** Rechner, Notebook: the installed applications, in the file's order. */ /** Rechner, Notebook: the installed applications, in the file's order. */
applications?: FlsApplication[]; applications?: FlsApplication[];
/** Rechner, Notebook: the root directory (named "root" by Java's constructor). */ /**
* 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; 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; type Warn = (message: string) => void;
@@ -231,6 +241,10 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
case 'Modem': case 'Modem':
break; break;
} }
if (javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway') {
result.fileSystem = fileSystem(value('dateisystem'), warn);
result.templateFiles = constructedRoot(value('dateisystem'));
}
if (javaClass !== 'Switch' && javaClass !== 'Modem') { if (javaClass !== 'Switch' && javaClass !== 'Modem') {
result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), warn); result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), warn);
result.dhcpClient = boolean(value('DHCPKonfiguration'), false, 'DHCPKonfiguration', warn); result.dhcpClient = boolean(value('DHCPKonfiguration'), false, 'DHCPKonfiguration', warn);
@@ -343,12 +357,23 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
* file makes with `new DefaultMutableTreeNode()` has no user object. * file makes with `new DefaultMutableTreeNode()` has no user object.
*/ */
function fileSystem(system: JavaValue | undefined, warn: Warn): FlsFileTree { function fileSystem(system: JavaValue | undefined, warn: Warn): FlsFileTree {
const at = (property: string) => (isJavaObject(system) ? system.properties.get(property) : undefined); const root = fileSystemRoot(system);
const root = at('root') ?? at('arbeitsVerzeichnis');
if (root === undefined) return { kind: 'directory', name: 'root', children: [] }; if (root === undefined) return { kind: 'directory', name: 'root', children: [] };
return fileTreeNode(root, true, warn); 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 { function fileTreeNode(node: JavaValue, constructorRoot: boolean, warn: Warn): FlsFileTree {
if (!isJavaObject(node)) { if (!isJavaObject(node)) {
warn(`file system: a node is ${describe(node)}, left empty`); warn(`file system: a node is ${describe(node)}, left empty`);
+36 -5
View File
@@ -12,8 +12,9 @@
// are set through getters. A router's or home router's firewall is reached // are set through getters. A router's or home router's firewall is reached
// the way XMLEncoder reaches it: through the web server's `konfig` plug-in, // the way XMLEncoder reaches it: through the web server's `konfig` plug-in,
// which holds the same object (its key among the installed applications // which holds the same object (its key among the installed applications
// differs: Firewall on a router, NatGateway on a home router). Files and // differs: Firewall on a router, NatGateway on a home router). Files are
// applications are later layers. // added to the constructor's root, as XMLEncoder adds them — on a router
// after the files its constructor makes. Applications are a later layer.
// //
// Checked by Java: every corpus file, read and written again, loads in Java // Checked by Java: every corpus file, read and written again, loads in Java
// FILIUS to the same topology and system software (topologyWriter.test.ts, // FILIUS to the same topology and system software (topologyWriter.test.ts,
@@ -31,14 +32,14 @@ import {
type BeanValue, type BeanValue,
short, short,
} from './javaBeansWriter.js'; } from './javaBeansWriter.js';
import type { FlsFirewall, FlsSystemSoftware } from './systemSoftware.js'; import type { FlsFileTree, FlsFirewall, FlsSystemSoftware } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNode, FlsNodeClass } from './topology.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. */ /** 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)'; export const WRITTEN_VERSION_LINE = 'Filius version: 2.14.0 (mdg-filius)';
/** The system software's settings; its files and applications are later layers. */ /** The system software's settings and files; its applications are a later layer. */
export type SystemToWrite = Omit<FlsSystemSoftware, 'fileSystem' | 'applications'>; export type SystemToWrite = Omit<FlsSystemSoftware, 'applications'>;
export type NodeToWrite = Pick< export type NodeToWrite = Pick<
FlsNode, FlsNode,
@@ -165,6 +166,7 @@ function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass): BeanS
], ],
}); });
} }
if (system.fileSystem !== undefined) statements.push(fileSystemStatement(system.fileSystem, system.templateFiles !== false));
if (system.dhcpServer !== undefined) { if (system.dhcpServer !== undefined) {
const server = system.dhcpServer; const server = system.dhcpServer;
statements.push({ statements.push({
@@ -205,6 +207,35 @@ function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass): BeanS
return statements; return statements;
} }
/**
* The file system: the root's children added to the constructor's root
* (named "root", empty on a PC or notebook), or a root of its own that
* replaces it with the files a router's constructor makes. `arbeitsVerzeichnis`
* is the property Java writes; `root` names the same field.
*/
function fileSystemStatement(root: FlsFileTree, constructedRoot: boolean): BeanStatement {
if (!constructedRoot) return { get: 'dateisystem', statements: [{ set: 'arbeitsVerzeichnis', value: treeNode(root) }] };
const statements: BeanStatement[] = [];
if (!(root.kind === 'directory' && root.name === 'root')) statements.push({ set: 'userObject', value: userObject(root) });
statements.push(...childStatements(root));
return { get: 'dateisystem', statements: [{ get: 'arbeitsVerzeichnis', statements }] };
}
function childStatements(tree: FlsFileTree): BeanStatement[] {
return tree.children.map((child) => ({ call: 'add', args: [treeNode(child)] }));
}
/** A DefaultMutableTreeNode; one of another kind than directory or file is written without a user object. */
function treeNode(tree: FlsFileTree): BeanObject {
return object('javax.swing.tree.DefaultMutableTreeNode', childStatements(tree), tree.kind === 'other' ? [] : [userObject(tree)]);
}
/** A directory's name, or a file as `new Datei(name, typ, inhalt)`. */
function userObject(tree: FlsFileTree): BeanValue {
if (tree.kind === 'file') return object('filius.software.system.Datei', [], [tree.name, tree.type, tree.content]);
return tree.kind === 'directory' ? tree.name : null;
}
/** A LinkedList of String arrays: routes, static NAT. */ /** A LinkedList of String arrays: routes, static NAT. */
function rows(table: readonly (readonly (string | null)[])[]): BeanObject { function rows(table: readonly (readonly (string | null)[])[]): BeanObject {
return listOf(table.map((row) => ({ kind: 'array', componentClass: 'java.lang.String', items: [...row] }))); return listOf(table.map((row) => ({ kind: 'array', componentClass: 'java.lang.String', items: [...row] })));
+56 -9
View File
@@ -168,6 +168,7 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
}), }),
...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }), ...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }),
...(node.system.fileSystem === undefined ? {} : { fileSystem: javaFileTree(node.system.fileSystem) }), ...(node.system.fileSystem === undefined ? {} : { fileSystem: javaFileTree(node.system.fileSystem) }),
...('templateFiles' in node.system ? { templateFiles: undefined } : {}),
...(node.system.applications === undefined ? {} : { applications: javaApplications(node.system.applications) }), ...(node.system.applications === undefined ? {} : { applications: javaApplications(node.system.applications) }),
...(node.system.firewall === undefined ...(node.system.firewall === undefined
? {} ? {}
@@ -202,19 +203,33 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
}; };
} }
type JavaTree = { children: unknown[] };
/** /**
* Java's view without the file systems of routers and home routers: their * The files a router's or home router's constructor makes, as Java loaded
* files come from Java's constructor (templates of the firewall's web * each one: they come first in its root, what the file adds follows. Each
* interface), not from the .fls file, and the reader does not claim them. * router's are cut off here and collected, and must be the same for every
* router of a class across the corpus.
*/ */
function withoutTemplateFiles(view: unknown): unknown { const templateChildren = new Map<string, Set<string>>();
function withoutTemplateFiles(view: unknown, topology: FlsTopology): unknown {
const { nodes, ...rest } = view as { nodes: { class: string; system: Record<string, unknown> }[] }; const { nodes, ...rest } = view as { nodes: { class: string; system: Record<string, unknown> }[] };
return { return {
...rest, ...rest,
nodes: nodes.map((node) => { nodes: nodes.map((node, index) => {
if (node.class !== 'Vermittlungsrechner' && node.class !== 'Gateway') return node; const ours = topology.nodes[index]?.system;
const { fileSystem: _templateFiles, ...system } = node.system; if ((node.class !== 'Vermittlungsrechner' && node.class !== 'Gateway') || ours?.templateFiles !== true) return node;
return { ...node, system }; const tree = node.system.fileSystem as JavaTree;
const added = ours.fileSystem?.children.length ?? 0;
const template = tree.children.slice(0, tree.children.length - added);
const seen = templateChildren.get(node.class) ?? new Set<string>();
seen.add(JSON.stringify(template));
templateChildren.set(node.class, seen);
return {
...node,
system: { ...node.system, fileSystem: { ...tree, children: tree.children.slice(tree.children.length - added) } },
};
}), }),
}; };
} }
@@ -227,10 +242,18 @@ describe('corpus: the topology as Java loads it', () => {
const topology = readTopology(values); const topology = readTopology(values);
const expected = javaView[name] as { nodes: { bounds: number[] | null }[] }; const expected = javaView[name] as { nodes: { bounds: number[] | null }[] };
expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(withoutTemplateFiles(expected)); expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(withoutTemplateFiles(expected, topology));
expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []); expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []);
}); });
} }
it("a router's constructor makes the same files in every corpus file", () => {
for (const javaClass of ['Vermittlungsrechner', 'Gateway']) {
const templates = [...(templateChildren.get(javaClass) ?? [])].map((text) => JSON.parse(text) as { directory?: string }[]);
expect(templates, javaClass).toHaveLength(1);
expect(templates[0]!.map((child) => child.directory), javaClass).toEqual(['webserver']);
}
});
}); });
// ── Rules the corpus does not exercise ───────────────────────────────────── // ── Rules the corpus does not exercise ─────────────────────────────────────
@@ -433,6 +456,30 @@ describe('readTopology rules', () => {
expect(warnings).toEqual(['node 2: file system: insert on a node is not read']); expect(warnings).toEqual(['node 2: file system: insert on a node is not read']);
}); });
it("reads what a router's file adds to the constructor's files, or a root of its own", () => {
const folder = (name: string) =>
`<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>${name}</string></object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Vermittlungsrechner"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Gateway"><void property="systemSoftware"><void property="dateisystem">
<void property="arbeitsVerzeichnis">${folder('www.conf')}</void>
</void></void></object>`)}
${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner"><void property="systemSoftware"><void property="dateisystem">
<void property="arbeitsVerzeichnis"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>root</string>${folder('own')}</object></void>
</void></void></object>`)}
${nodeItem(`<object class="filius.hardware.knoten.Gateway"><void property="systemSoftware">
<void property="dateisystem"><object class="filius.software.system.Dateisystem"/></void>
</void></object>`)}</object>`);
const directory = (name: string, children: FlsFileTree[] = []): FlsFileTree => ({ kind: 'directory', name, children });
expect(nodes.map((node) => [node.system.fileSystem, node.system.templateFiles])).toEqual([
[directory('root'), true],
[directory('root', [directory('www.conf')]), true],
[directory('root', [directory('own')]), false],
[directory('root'), false],
]);
});
it("gives installed applications Java's measured defaults for what the file leaves out", () => { it("gives installed applications Java's measured defaults for what the file leaves out", () => {
const put = (className: string, body = '') => const put = (className: string, body = '') =>
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`; `<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
+89 -3
View File
@@ -1,7 +1,7 @@
// Every corpus file, read and written again: Java FILIUS loads the written // 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 // file (tools/java-oracle, written.java.json) to the same topology as the
// original (corpus.java.json) — devices, names, positions, interfaces, // original (corpus.java.json) — devices, names, positions, interfaces,
// system software (without files and applications), cables and // system software and files (without applications), cables and
// documentation. Java against Java: no mapping of this package stands // documentation. Java against Java: no mapping of this package stands
// between the two. // between the two.
// //
@@ -43,9 +43,9 @@ 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 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>; const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record<string, JavaFile>;
/** The system software without its files and applications, which are later layers. */ /** The system software without its applications, which are a later layer. */
function systemOf(system: Record<string, unknown>) { function systemOf(system: Record<string, unknown>) {
const { fileSystem: _files, applications: _applications, ...settings } = system; const { applications: _applications, ...settings } = system;
return settings; return settings;
} }
@@ -89,6 +89,9 @@ describe('the topology written again, as Java loads it', () => {
} }
}); });
/** Text a file's content may hold: markup, CR and CRLF, a tab, beyond the BMP. */
const FILE_CONTENT = '<p>a & b</p>\r\nline\rend\ttab Grüße 😀\n';
/** /**
* What the corpus does not have: a name set to null, useMACAsName, fewer * What the corpus does not have: a name set to null, useMACAsName, fewer
* ports than the constructor makes, more interfaces; a PC's SSID, routes and * ports than the constructor makes, more interfaces; a PC's SSID, routes and
@@ -120,6 +123,21 @@ const SYNTHETIC: TopologyToWrite = {
useOwnSettings: true, useOwnSettings: true,
staticAssignedAddresses: [{ mac: 'AA:BB:CC:DD:EE:77', ip: '10.0.0.77' }], staticAssignedAddresses: [{ mac: 'AA:BB:CC:DD:EE:77', ip: '10.0.0.77' }],
}, },
fileSystem: {
kind: 'directory',
name: '/',
children: [
{
kind: 'directory',
name: 'docs',
children: [
{ kind: 'file', name: 'a.txt', type: 'text', content: FILE_CONTENT, children: [] },
{ kind: 'directory', name: null, children: [] },
],
},
{ kind: 'file', name: 'untyped', type: null, content: null, children: [] },
],
},
}, },
}, },
{ {
@@ -150,6 +168,8 @@ const SYNTHETIC: TopologyToWrite = {
portCount: 3, portCount: 3,
system: { system: {
ripEnabled: true, ripEnabled: true,
fileSystem: { kind: 'directory', name: 'root', children: [{ kind: 'directory', name: 'www.conf', children: [] }] },
templateFiles: true,
firewall: { firewall: {
activated: true, activated: true,
defaultPolicy: 1, defaultPolicy: 1,
@@ -176,6 +196,26 @@ const SYNTHETIC: TopologyToWrite = {
portCount: 1, portCount: 1,
system: { modem: { mode: 1, port: 4242, ipAddress: '192.168.1.5' } }, system: { modem: { mode: 1, port: 4242, ipAddress: '192.168.1.5' } },
}, },
{
javaClass: 'Gateway',
name: 'G',
x: 400,
y: 20,
useIpAsName: false,
useMacAsName: false,
interfaces: ['4', '5'].map((octet) => ({
ip: `10.0.${octet}.1`,
mask: '255.255.255.0',
mac: `AA:BB:CC:DD:EE:1${octet}`,
gateway: '',
dns: '',
})),
portCount: 2,
system: {
fileSystem: { kind: 'directory', name: 'own', children: [{ kind: 'file', name: 'x', type: 'text', content: 'y', children: [] }] },
templateFiles: false,
},
},
], ],
links: [ links: [
{ ends: [{ node: 0, port: 0 }, { node: 1, port: 2 }], wireless: false, panel: [0, 1] }, { ends: [{ node: 0, port: 0 }, { node: 1, port: 2 }], wireless: false, panel: [0, 1] },
@@ -195,6 +235,28 @@ const JAVA_DHCP_DEFAULTS = {
staticAssignedAddresses: [], staticAssignedAddresses: [],
}; };
/** A file as the oracle describes it: its content as UTF-16 length and SHA-256 of its UTF-8 bytes. */
function javaFile(name: string, type: string | null, content: string | null) {
return {
file: name,
type,
length: content === null ? null : content.length,
sha256: content === null ? null : createHash('sha256').update(content, 'utf8').digest('hex'),
children: [],
};
}
/** The files a router's constructor makes, as Java loaded them from the corpus. */
function routerTemplate(): unknown[] {
const router = Object.values(original)
.flatMap((file) => file.nodes ?? [])
.find((node) => node.class === 'Vermittlungsrechner');
return ((router!.system.fileSystem as { children: unknown[] }).children).slice(0, 1);
}
/** A router's firewall as its constructor leaves it. */
const JAVA_ROUTER_FIREWALL = { activated: false, defaultPolicy: 0, dropICMP: false, filterSYNSegmentsOnly: true, filterUdp: true, rules: [] };
describe('a topology beyond the corpus, as Java loads it', () => { describe('a topology beyond the corpus, as Java loads it', () => {
it('keeps what the corpus never sets', () => { it('keeps what the corpus never sets', () => {
const name = 'synthetic/synthetic.fls'; const name = 'synthetic/synthetic.fls';
@@ -231,6 +293,13 @@ describe('a topology beyond the corpus, as Java loads it', () => {
ownSettings: true, ownSettings: true,
staticAssignedAddresses: ['AA:BB:CC:DD:EE:77 10.0.0.77'], staticAssignedAddresses: ['AA:BB:CC:DD:EE:77 10.0.0.77'],
}, },
fileSystem: {
directory: '/',
children: [
{ directory: 'docs', children: [javaFile('a.txt', 'text', FILE_CONTENT), { directory: null, children: [] }] },
javaFile('untyped', null, null),
],
},
}, },
}, },
{ {
@@ -251,6 +320,7 @@ describe('a topology beyond the corpus, as Java loads it', () => {
ports: ['1', '2', '3'].map(plainInterface), ports: ['1', '2', '3'].map(plainInterface),
system: { system: {
ripEnabled: true, ripEnabled: true,
fileSystem: { directory: 'root', children: [...routerTemplate(), { directory: 'www.conf', children: [] }] },
manualRoutes: [], manualRoutes: [],
dhcpClient: false, dhcpClient: false,
dhcpServer: JAVA_DHCP_DEFAULTS, dhcpServer: JAVA_DHCP_DEFAULTS,
@@ -278,6 +348,22 @@ describe('a topology beyond the corpus, as Java loads it', () => {
ports: [{}], ports: [{}],
system: { modem: { mode: 1, port: 4242, ipAdresse: '192.168.1.5' } }, system: { modem: { mode: 1, port: 4242, ipAdresse: '192.168.1.5' } },
}, },
{
class: 'Gateway',
name: 'G',
position: [400, 20],
useIpAsName: undefined,
useMacAsName: undefined,
ports: ['4', '5'].map(plainInterface),
system: {
fileSystem: { directory: 'own', children: [javaFile('x', 'text', 'y')] },
manualRoutes: [],
dhcpClient: false,
dhcpServer: JAVA_DHCP_DEFAULTS,
staticNAT: [],
firewall: JAVA_ROUTER_FIREWALL,
},
},
], ],
cables: [ cables: [
{ ends: [[0, 0], [1, 2]], wireless: false, panel: [0, 1] }, { ends: [[0, 0], [1, 2]], wireless: false, panel: [0, 1] },
File diff suppressed because it is too large Load Diff