feat(fls): read and write a project's own applications as their beans

A project's own applications (classes under projekt/anwendungen, installed
on a host) are no longer left out. The decoder now keeps the Java type of
every primitive — at setters, constructor and call arguments, also through
shared ids like Integer0 — and which properties an object's own getter
made. beanCapture.ts turns a decoded bean back into what the writer writes,
with the node's system software as a mark the writer replaces by the
system software it writes; what the format cannot say is refused, not
guessed (an unresolved value, a Field, a factory-made object, an object a
getter reaches elsewhere in the file). The reader keeps own applications as
captured beans; the corpus no longer warns.

The capture first took a setter with another object's getter result for an
own getter; the round-trip test found it. The Java oracle did not show
whether an application runs on its node's system software, so a mutation
writing the mark instead survived; it now reports onItsSystem (true for all
192 applications of the corpus).

Checked by Java: from the files written again, FILIUS loads Accumulator and
Broadcast installed, switched on and on their node's system software, as
in the original. The round-trip test covers every primitive, null, a
class, an array, shared objects, getter chains, constructed elements, map
reads and static constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 09:43:16 +00:00
co-authored by Claude Opus 5.5
parent 95c5b7d97b
commit 3548652687
18 changed files with 1444 additions and 404 deletions
+4 -3
View File
@@ -36,10 +36,10 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| --- | --- | --- | | --- | --- | --- |
| `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen | | `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen |
| `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene |
| `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; merkt sich den Java-Typ jedes Primitivs an Settern, Konstruktor- und Aufrufargumenten und welche Eigenschaften ein eigener Getter angelegt hat (für `beanCapture.ts`) |
| `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, WLAN eines Rechners eingeschaltet nach Javas Funk-Merkmal der Karte (auch ohne SSID; Swift: jede SSID); der Webserver behält Javas Schalter für virtuelle Hosts (Swift: aus); 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, WLAN eines Rechners eingeschaltet nach Javas Funk-Merkmal der Karte (auch ohne SSID; Swift: jede SSID); der Webserver behält Javas Schalter für virtuelle Hosts (Swift: aus); 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 (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/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 (eigene Anwendungen eines Projekts als erfasste Bean, `beanCapture.ts`); `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, die Funk-Eigenschaft einer Netzwerkkarte (`wireless`, bis 30.09.2026 übersehen) | | `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, die Funk-Eigenschaft einer Netzwerkkarte (`wireless`, bis 30.09.2026 übersehen) |
@@ -47,9 +47,10 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt; Java lädt seit M8 über den vollständigen Ladeweg, eigene Anwendungen (`test/ownApplications.ts`) sind bis B2 ausgenommen | | `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt; Java lädt seit M8 über den vollständigen Ladeweg, eigene Anwendungen (`test/ownApplications.ts`) sind bis B2 ausgenommen |
| `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java | | `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java |
| `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/beanCapture.ts` | — | neu: eine decodierte Bean zurück in die Form des Schreibers, mit Javas Zahlentypen; der Verweis auf das Betriebssystem des Knotens als Marke; was das Format nicht sagen kann (Unaufgelöstes, `Field`, Fabrikmethode, ein anderswo per Getter erreichtes Objekt), wird abgelehnt, nicht geraten; Rundlauf-Tests in `test/beanCapture.test.ts` |
| `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); 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; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; 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); eigene Anwendungen eines Projekts, wie die Datei sie hatte; 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; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; 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 samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (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, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) | | `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 samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (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, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) |
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; `saveProjectArchive` schreibt eine ganze `.fls` samt der übrigen Archiveinträge (Gegenstück: `openProjectArchive` in `importProject.ts`, das sie in `archiveEntries` des Modells legt); Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente; X2 Systemsoftware — SSIDs, Lernzeit, Routen, RIP, DHCP-Client und -Server, Portweiterleitung, Firewall (Namen des Kerns als Javas Zahlen), Modems aus den Paaren des Modells (Port aus dem Paarnamen, der erste eines Paars dient, Ziel `localhost`); X3 Dateien von Rechner und Notebook — Typ aus der Inhaltsart (Text nach Endung, sonst `text`; Bild und Binärdaten nach Medientyp), Bilder und Binärdaten in Base64 wie Filius' `Base64.encodeBytes` (Umbruch nach 76 Zeichen, an 19 Längen mit Java gemessen); X4a Programme — je Programm die Filius-Klasse unter ihrem Namen als Schlüssel, eingeschaltete Server, Ports, virtuelle Hosts, DNS-Rekursion, Ziel des Clients (leer als `null` wie Javas Konstruktor), Teilnehmerzahl, persönliche Firewall; X4b E-Mail — Beans des Servers (Domain, Konten, Postfächer) und des Clients (drei Ordner, Adressbuch), `/konten.txt` und `/mailserver/konten.txt` aus den Konten des Modells neu geschrieben im Format von Filius 2.14 (`EmailKonto.toString`, `listeBenutzerkontenZuString`), ohne Javas Fehler (leere Felder statt "null"); ein Zeichen, das ein Feld im Format nicht tragen kann, wird dort zum Leerzeichen, mit Warnung | | `src/exportProject.ts` | Gegenstück zu `importProject.ts`; `saveProjectArchive` schreibt eine ganze `.fls` samt der übrigen Archiveinträge (Gegenstück: `openProjectArchive` in `importProject.ts`, das sie in `archiveEntries` des Modells legt); Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente; X2 Systemsoftware — SSIDs, Lernzeit, Routen, RIP, DHCP-Client und -Server, Portweiterleitung, Firewall (Namen des Kerns als Javas Zahlen), Modems aus den Paaren des Modells (Port aus dem Paarnamen, der erste eines Paars dient, Ziel `localhost`); X3 Dateien von Rechner und Notebook — Typ aus der Inhaltsart (Text nach Endung, sonst `text`; Bild und Binärdaten nach Medientyp), Bilder und Binärdaten in Base64 wie Filius' `Base64.encodeBytes` (Umbruch nach 76 Zeichen, an 19 Längen mit Java gemessen); X4a Programme — je Programm die Filius-Klasse unter ihrem Namen als Schlüssel, eingeschaltete Server, Ports, virtuelle Hosts, DNS-Rekursion, Ziel des Clients (leer als `null` wie Javas Konstruktor), Teilnehmerzahl, persönliche Firewall; X4b E-Mail — Beans des Servers (Domain, Konten, Postfächer) und des Clients (drei Ordner, Adressbuch), `/konten.txt` und `/mailserver/konten.txt` aus den Konten des Modells neu geschrieben im Format von Filius 2.14 (`EmailKonto.toString`, `listeBenutzerkontenZuString`), ohne Javas Fehler (leere Felder statt "null"); ein Zeichen, das ein Feld im Format nicht tragen kann, wird dort zum Leerzeichen, mit Warnung |
| `test/exportProject.test.ts`, `test/exported.java.json` | — | neu: alle 23 Korpus-Dateien importiert, exportiert, geschrieben und von Java geladen — Topologie wie beim Original bis auf benannte Abweichungen (abgeleiteter MAC und abgeleitete Switch-SSID statt gewürfelter, Modem-Rollen nach Reihenfolge im Paar, Doku in Zeichenreihenfolge mit Text, Schrift und Mindestgröße des Modells, Dateien in der Ordnung und mit dem Typ des Modells, keine eigenen Dateien von Routern, Empfänger als Zeichenkette gelesen, Nachrichten des Gesendet-Ordners versendet, Konto-Dateien neu geschrieben wie Filius 2.14); die übrigen Archiveinträge (eigene Anwendungen unter `projekt/anwendungen`) kommen byte-gleich zurück; dazu ein künstliches Modell, das Java ebenfalls lädt | | `test/exportProject.test.ts`, `test/exported.java.json` | — | neu: alle 23 Korpus-Dateien importiert, exportiert, geschrieben und von Java geladen — Topologie wie beim Original bis auf benannte Abweichungen (abgeleiteter MAC und abgeleitete Switch-SSID statt gewürfelter, Modem-Rollen nach Reihenfolge im Paar, Doku in Zeichenreihenfolge mit Text, Schrift und Mindestgröße des Modells, Dateien in der Ordnung und mit dem Typ des Modells, keine eigenen Dateien von Routern, Empfänger als Zeichenkette gelesen, Nachrichten des Gesendet-Ordners versendet, Konto-Dateien neu geschrieben wie Filius 2.14); die übrigen Archiveinträge (eigene Anwendungen unter `projekt/anwendungen`) kommen byte-gleich zurück; dazu ein künstliches Modell, das Java ebenfalls lädt |
+294 -102
View File
File diff suppressed because it is too large Load Diff
+142
View File
@@ -0,0 +1,142 @@
// A decoded JavaBean (javabeans.ts) turned back into what javaBeansWriter.ts
// writes, so that a bean this package does not understand — a project's own
// application — is written again as the file had it. Java's primitive types
// come from the decoder (propertyKinds, argKinds, a call's kinds); a number
// without one was an Integer or a Double. The node's system software, which
// the bean refers to, is marked (SYSTEM_SOFTWARE_CLASS) and put back by the
// writer as the system software of the node it writes.
//
// What the writer's format cannot say is refused, never guessed: a value the
// decoder left unresolved, a Field, an object a static factory made, and an
// object the bean reached by a getter elsewhere in the file (another part of
// the node, whose identity a copy would lose). Objects the bean shares with
// itself stay shared.
import { isJavaObject, isUnresolved, type JavaArray, type JavaObject, type JavaValue, type PrimitiveKind } from './javabeans.js';
import type { BeanObject, BeanStatement, BeanValue } from './javaBeansWriter.js';
/** The class name of the mark where a captured bean refers to its node's system software. */
export const SYSTEM_SOFTWARE_CLASS = '#systemSoftware';
export type CaptureResult = { readonly bean: BeanObject } | { readonly failure: string };
class CaptureFailure extends Error {}
/**
* The bean `object` as the writer writes it; `systemSoftware` is the node's
* system software as the decoder has it.
*/
export function captureBean(object: JavaObject, systemSoftware: JavaValue | undefined): CaptureResult {
const captured = new Map<JavaObject, BeanObject>();
const value = (item: JavaValue, kind: PrimitiveKind | undefined): BeanValue => {
if (item === null || typeof item === 'boolean') return item;
if (typeof item === 'string') return kind === 'char' ? { kind: 'char', value: item } : item;
if (typeof item === 'number' || typeof item === 'bigint') return number(item, kind);
if (isJavaObject(item)) {
if (item === systemSoftware) return { kind: 'object', className: SYSTEM_SOFTWARE_CLASS };
if (item.fromGetter) throw new CaptureFailure(`it refers to ${item.className}, which a getter reaches elsewhere in the file`);
return bean(item);
}
if (isUnresolved(item)) throw new CaptureFailure(`a value is unresolved (${item.reason})`);
switch (item.kind) {
case 'array':
return { kind: 'array', componentClass: item.componentClass, items: (item as JavaArray).items.map((each) => value(each, undefined)) };
case 'class':
return { kind: 'class', name: item.name };
default:
throw new CaptureFailure(`it holds a Field (${item.className}.${item.name})`);
}
};
const bean = (item: JavaObject): BeanObject => {
const known = captured.get(item);
if (known !== undefined) return known;
if (item.factoryMethod !== undefined) throw new CaptureFailure(`${item.className}.${item.factoryMethod} makes an object this writer cannot`);
const statements: BeanStatement[] = [];
const result: BeanObject =
item.staticField === undefined
? { kind: 'object', className: item.className, args: item.args.map((arg, index) => value(arg, item.argKinds[index])), statements }
: { kind: 'object', className: item.className, staticField: item.staticField, statements };
captured.set(item, result);
statements.push(...statementsOf(item));
return result;
};
/** Setters and getters with what follows on their results; the constructor's elements; the calls but reads. */
const statementsOf = (item: JavaObject): BeanStatement[] => {
const statements: BeanStatement[] = [];
for (const [property, each] of item.properties) {
if (item.propertiesFromGetters.has(property) && isJavaObject(each)) {
statements.push({ get: property, statements: statementsOf(each) });
} else {
statements.push({ set: property, value: value(each, item.propertyKinds.get(property)) });
}
}
for (const [index, element] of item.presetElements) {
statements.push({ getIndex: index, statements: isJavaObject(element) ? statementsOf(element) : [] });
}
for (const [key, entry] of item.presetEntries) {
statements.push({ call: 'get', args: [key], statements: isJavaObject(entry) ? statementsOf(entry) : [] });
}
for (const call of item.calls) {
// A read (get(i), get(key)) is the preset element or entry above, or a value set before.
if (call.method === 'get' && call.args.length === 1) continue;
statements.push({ call: call.method, args: call.args.map((arg, index) => value(arg, call.kinds?.[index])) });
}
return statements;
};
try {
return { bean: bean(object) };
} catch (error) {
if (error instanceof CaptureFailure) return { failure: error.message };
throw error;
}
}
function number(item: number | bigint, kind: PrimitiveKind | undefined): BeanValue {
switch (kind) {
case 'byte':
case 'short':
case 'int':
return { kind, value: Number(item) };
case 'long':
return { kind: 'long', value: item };
case 'float':
case 'double':
return { kind, value: Number(item) };
case 'char':
return { kind: 'char', value: String.fromCharCode(Number(item)) };
default:
// An Integer, or a Double where the number has a fraction.
return typeof item === 'number' && !Number.isInteger(item) ? { kind: 'double', value: item } : { kind: 'int', value: Number(item) };
}
}
/** The bean with each mark of the system software replaced by `systemSoftware`. */
export function withSystemSoftware(value: BeanValue, systemSoftware: BeanValue): BeanValue {
const seen = new Map<BeanObject, BeanObject>();
const replace = (item: BeanValue): BeanValue => {
if (item === null || typeof item !== 'object') return item;
if (item.kind === 'array') return { ...item, items: item.items.map(replace) };
if (item.kind !== 'object') return item;
if (item.className === SYSTEM_SOFTWARE_CLASS) return systemSoftware;
return object(item);
};
const statement = (each: BeanStatement): BeanStatement => {
if ('set' in each || 'setIndex' in each) return { ...each, value: replace(each.value) };
if ('call' in each) return { ...each, args: each.args.map(replace), ...(each.statements === undefined ? {} : { statements: each.statements.map(statement) }) };
return { ...each, statements: each.statements.map(statement) };
};
const object = (item: BeanObject): BeanObject => {
const known = seen.get(item);
if (known !== undefined) return known;
const statements: BeanStatement[] = [];
const result: BeanObject = { ...item, ...(item.args === undefined ? {} : { args: item.args.map(replace) }), statements };
seen.set(item, result);
statements.push(...(item.statements ?? []).map(statement));
return result;
};
return replace(value);
}
+76 -10
View File
@@ -45,8 +45,18 @@ export interface JavaObject {
readonly factoryMethod: string | undefined; readonly factoryMethod: string | undefined;
/** Set through setters, `Field.set` or created lazily by a getter. */ /** Set through setters, `Field.set` or created lazily by a getter. */
readonly properties: Map<string, JavaValue>; readonly properties: Map<string, JavaValue>;
/** Method calls in order (`add`, `put`, …), with their arguments. */ /** The Java type of each property set to a primitive (`int`, `long`, …): numbers alone do not say. */
readonly calls: { method: string; args: JavaValue[] }[]; readonly propertyKinds: Map<string, PrimitiveKind>;
/**
* The properties whose value this object's own getter made (a placeholder
* the file's statements then changed) — not set to an object from
* elsewhere.
*/
readonly propertiesFromGetters: Set<string>;
/** The Java types of the constructor's arguments, where they are primitives. */
readonly argKinds: (PrimitiveKind | undefined)[];
/** Method calls in order (`add`, `put`, …), with their arguments and those arguments' primitive types. */
readonly calls: { method: string; args: JavaValue[]; kinds?: (PrimitiveKind | undefined)[] }[];
/** /**
* Elements the object already held before the file's statements — a list * Elements the object already held before the file's statements — a list
* its constructor filled, reached by `<void index="i">` on a getter's * its constructor filled, reached by `<void index="i">` on a getter's
@@ -62,6 +72,23 @@ export interface JavaObject {
readonly fromGetter: boolean; readonly fromGetter: boolean;
} }
/** The primitive types XMLEncoder writes as elements of their own. */
export type PrimitiveKind = 'boolean' | 'byte' | 'char' | 'short' | 'int' | 'long' | 'float' | 'double';
const PRIMITIVE_TAGS = new Set<string>(['boolean', 'byte', 'char', 'short', 'int', 'long', 'float', 'double']);
/** A wrapper's id prefix, as XMLEncoder names a shared value (Integer0), and its primitive. */
const KIND_OF_ID_PREFIX: Readonly<Record<string, PrimitiveKind>> = {
Boolean: 'boolean',
Byte: 'byte',
Character: 'char',
Short: 'short',
Integer: 'int',
Long: 'long',
Float: 'float',
Double: 'double',
};
export interface JavaArray { export interface JavaArray {
readonly kind: 'array'; readonly kind: 'array';
readonly componentClass: string; readonly componentClass: string;
@@ -262,6 +289,9 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
staticField: fields.staticField, staticField: fields.staticField,
factoryMethod: fields.factoryMethod, factoryMethod: fields.factoryMethod,
properties: new Map(), properties: new Map(),
propertyKinds: new Map(),
propertiesFromGetters: new Set(),
argKinds: [],
calls: [], calls: [],
presetElements: new Map(), presetElements: new Map(),
presetEntries: new Map(), presetEntries: new Map(),
@@ -271,6 +301,15 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
class Decoder { class Decoder {
private readonly ids = new Map<string, JavaValue>(); private readonly ids = new Map<string, JavaValue>();
/** The primitive type of each id that names a primitive value. */
private readonly idKinds = new Map<string, PrimitiveKind>();
/** The primitive type an argument element stands for, if any. */
private kindOf(element: Element): PrimitiveKind | undefined {
if (PRIMITIVE_TAGS.has(element.name)) return element.name as PrimitiveKind;
const idref = element.attributes.idref;
return idref === undefined ? undefined : this.idKinds.get(idref);
}
topLevel(java: Element): JavaValue[] { topLevel(java: Element): JavaValue[] {
return java.children.map((child) => this.expression(child)); return java.children.map((child) => this.expression(child));
@@ -332,6 +371,10 @@ class Decoder {
private primitive(element: Element, value: JavaValue): JavaValue { private primitive(element: Element, value: JavaValue): JavaValue {
this.store(element.attributes.id, value); this.store(element.attributes.id, value);
const id = element.attributes.id;
// An element of its own (<int>), or a constant named by its wrapper (Integer0).
const kind = PRIMITIVE_TAGS.has(element.name) ? (element.name as PrimitiveKind) : KIND_OF_ID_PREFIX[id?.replace(/\d+$/, '') ?? ''];
if (id !== undefined && kind !== undefined) this.idKinds.set(id, kind);
return value; return value;
} }
@@ -382,7 +425,10 @@ class Decoder {
factoryMethod: attributes.method, factoryMethod: attributes.method,
}); });
this.store(attributes.id, object); this.store(attributes.id, object);
for (const arg of args) object.args.push(this.expression(arg)); for (const arg of args) {
object.args.push(this.expression(arg));
object.argKinds.push(this.kindOf(arg));
}
this.applyStatements( this.applyStatements(
element.children.filter((child) => child.name === 'void'), element.children.filter((child) => child.name === 'void'),
object, object,
@@ -400,12 +446,14 @@ class Decoder {
private statement(element: Element, target: JavaValue | undefined): JavaValue { private statement(element: Element, target: JavaValue | undefined): JavaValue {
const attributes = element.attributes; const attributes = element.attributes;
const args: JavaValue[] = []; const args: JavaValue[] = [];
const kinds: (PrimitiveKind | undefined)[] = [];
const nested: Element[] = []; const nested: Element[] = [];
for (const child of element.children) { for (const child of element.children) {
if (child.name === 'void') { if (child.name === 'void') {
nested.push(child); nested.push(child);
} else { } else {
args.push(this.expression(child)); args.push(this.expression(child));
kinds.push(this.kindOf(child));
} }
} }
@@ -416,17 +464,23 @@ class Decoder {
value = value =
args.length === 0 args.length === 0
? this.getProperty(target, attributes.property, attributes.id) ? this.getProperty(target, attributes.property, attributes.id)
: this.setProperty(target, attributes.property, args[0]!); : this.setProperty(target, attributes.property, args[0]!, kinds[0]);
} else if (attributes.index !== undefined) { } else if (attributes.index !== undefined) {
const index = Number.parseInt(attributes.index, 10); const index = Number.parseInt(attributes.index, 10);
value = args.length === 0 ? this.getIndex(target, index, attributes.id) : this.setIndex(target, index, args[0]!); value = args.length === 0 ? this.getIndex(target, index, attributes.id) : this.setIndex(target, index, args[0]!);
} else if (attributes.method !== undefined) { } else if (attributes.method !== undefined) {
value = this.call(target, attributes.method, args, attributes.id); value = this.call(target, attributes.method, args, attributes.id, kinds);
} else if (attributes.idref !== undefined) { } else if (attributes.idref !== undefined) {
value = this.ids.get(attributes.idref) ?? null; value = this.ids.get(attributes.idref) ?? null;
} }
this.store(attributes.id, value); this.store(attributes.id, value);
// A getter's primitive result, shared by id: its wrapper names the type.
const prefix = attributes.id?.replace(/\d+$/, '');
const kind = prefix === undefined ? undefined : KIND_OF_ID_PREFIX[prefix];
if (kind !== undefined && (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean')) {
this.idKinds.set(attributes.id!, kind);
}
this.applyStatements(nested, value); this.applyStatements(nested, value);
return value; return value;
} }
@@ -439,10 +493,16 @@ class Decoder {
return newObject({ className, id, args, factoryMethod: method }); return newObject({ className, id, args, factoryMethod: method });
} }
private call(target: JavaValue | undefined, method: string, args: JavaValue[], id: string | undefined): JavaValue { private call(
target: JavaValue | undefined,
method: string,
args: JavaValue[],
id: string | undefined,
kinds: (PrimitiveKind | undefined)[] = [],
): JavaValue {
if (typeof target === 'object' && target !== null && (target as JavaField).kind === 'field') { if (typeof target === 'object' && target !== null && (target as JavaField).kind === 'field') {
const field = target as JavaField; const field = target as JavaField;
if (method === 'set' && args.length === 2) return this.setProperty(args[0]!, field.name, args[1]!); if (method === 'set' && args.length === 2) return this.setProperty(args[0]!, field.name, args[1]!, kinds[1]);
if (method === 'get' && args.length === 1) return this.getProperty(args[0]!, field.name, id); if (method === 'get' && args.length === 1) return this.getProperty(args[0]!, field.name, id);
} }
if (args.length === 0) { if (args.length === 0) {
@@ -450,15 +510,20 @@ class Decoder {
if (property !== undefined) return this.getProperty(target, property, id); if (property !== undefined) return this.getProperty(target, property, id);
} }
if (isJavaObject(target)) { if (isJavaObject(target)) {
target.calls.push({ method, args }); target.calls.push(kinds.some((kind) => kind !== undefined) ? { method, args, kinds } : { method, args });
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id); if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
if (method === 'get' && args.length === 1 && typeof args[0] === 'string') return this.getEntry(target, args[0], id); if (method === 'get' && args.length === 1 && typeof args[0] === 'string') return this.getEntry(target, args[0], id);
} }
return null; return null;
} }
private setProperty(target: JavaValue | undefined, property: string, value: JavaValue): JavaValue { private setProperty(target: JavaValue | undefined, property: string, value: JavaValue, kind?: PrimitiveKind): JavaValue {
if (isJavaObject(target)) target.properties.set(property, value); if (isJavaObject(target)) {
target.properties.set(property, value);
target.propertiesFromGetters.delete(property);
if (kind === undefined) target.propertyKinds.delete(property);
else target.propertyKinds.set(property, kind);
}
return null; return null;
} }
@@ -482,6 +547,7 @@ class Decoder {
} }
const placeholder = newObject({ className: prefix ?? 'unknown', id, fromGetter: true }); const placeholder = newObject({ className: prefix ?? 'unknown', id, fromGetter: true });
target.properties.set(property, placeholder); target.properties.set(property, placeholder);
target.propertiesFromGetters.add(property);
return placeholder; return placeholder;
} }
+30 -6
View File
@@ -28,7 +28,9 @@
// not know Java's constructors and sees a placeholder per path; they are // not know Java's constructors and sees a placeholder per path; they are
// merged here. // merged here.
import { captureBean } from './beanCapture.js';
import { isDomainName } from './dnsHosts.js'; import { isDomainName } from './dnsHosts.js';
import type { BeanObject } from './javaBeansWriter.js';
import { import {
EMAIL_CLIENT_FILE, EMAIL_CLIENT_FILE,
EMAIL_SERVER_FILE, EMAIL_SERVER_FILE,
@@ -130,6 +132,13 @@ const SERVER_APPLICATIONS = new Set<string>([
'filius.software.www.WebServer', 'filius.software.www.WebServer',
]); ]);
/** An application of the project's own: its key, class and bean, which refers to its node's system software by a mark. */
export interface FlsOwnApplication {
key: string | null;
javaClass: string;
bean: BeanObject;
}
/** An installed application with the settings the model reads. */ /** An installed application with the settings the model reads. */
export interface FlsApplication { export interface FlsApplication {
/** The key in installierteAnwendungen — the class name in every corpus file. */ /** The key in installierteAnwendungen — the class name in every corpus file. */
@@ -199,6 +208,12 @@ 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 installed applications FILIUS does not ship — a
* project's own, compiled into projekt/anwendungen — kept as their beans,
* to be written again as the file had them.
*/
ownApplications?: FlsOwnApplication[];
/** /**
* Every node with an IP stack: the root directory (named "root" by Java's * 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 * constructor) as the file has it. A router's or home router's holds only
@@ -232,7 +247,7 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
case 'Notebook': case 'Notebook':
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null; result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
result.fileSystem = fileSystem(value('dateisystem'), warn); result.fileSystem = fileSystem(value('dateisystem'), warn);
result.applications = applications(value('installierteAnwendungen'), result.fileSystem, warn); [result.applications, result.ownApplications] = applications(value('installierteAnwendungen'), result.fileSystem, software, warn);
break; break;
case 'Vermittlungsrechner': case 'Vermittlungsrechner':
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn); result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
@@ -279,11 +294,18 @@ function modemAddress(value: string | null | undefined): string | null {
/** /**
* installierteAnwendungen: key → application. A class FILIUS does not ship — * installierteAnwendungen: key → application. A class FILIUS does not ship —
* a project's own application, compiled into projekt/anwendungen — is left * a project's own application, compiled into projekt/anwendungen — cannot
* out: this reader cannot run Java classes. * run here; it is kept as its bean (beanCapture.ts), or left out with a
* warning where the bean cannot be written again.
*/ */
function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn): FlsApplication[] { function applications(
map: JavaValue | undefined,
root: FlsFileTree,
software: JavaObject | undefined,
warn: Warn,
): [FlsApplication[], FlsOwnApplication[]] {
const result: FlsApplication[] = []; const result: FlsApplication[] = [];
const own: FlsOwnApplication[] = [];
for (const [key, application] of mapEntries(map)) { for (const [key, application] of mapEntries(map)) {
if (!isJavaObject(application)) { if (!isJavaObject(application)) {
warn(`application ${describe(key)} is ${describe(application)}, left out`); warn(`application ${describe(key)} is ${describe(application)}, left out`);
@@ -291,7 +313,9 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
} }
const javaClass = application.className; const javaClass = application.className;
if (!(FILIUS_APPLICATION_CLASSES as readonly string[]).includes(javaClass)) { if (!(FILIUS_APPLICATION_CLASSES as readonly string[]).includes(javaClass)) {
warn(`application ${javaClass} is not part of FILIUS (a project's own application), left out`); const captured = captureBean(application, software);
if ('bean' in captured) own.push({ key: typeof key === 'string' ? key : null, javaClass, bean: captured.bean });
else warn(`application ${javaClass}, a project's own, cannot be written again: ${captured.failure}; left out`);
continue; continue;
} }
const simple = simpleClassName(javaClass); const simple = simpleClassName(javaClass);
@@ -348,7 +372,7 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
} }
result.push(entry); result.push(entry);
} }
return result; return [result, own];
} }
/** /**
+14 -4
View File
@@ -19,7 +19,8 @@
// the web server, the peer-to-peer application and the e-mail server make // 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 // 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 // /dns/hosts when it is installed. Each is put under its key and given the
// system software, as installApp does. // system software, as installApp does. A project's own application is
// written as the file had it (beanCapture.ts).
// //
// E-mail: the server's domain and bean accounts with their mailboxes, the // E-mail: the server's domain and bean accounts with their mailboxes, the
// client's three folders and address book. The client's account is no bean // client's three folders and address book. The client's account is no bean
@@ -33,6 +34,7 @@
// FILIUS to the same topology and system software (topologyWriter.test.ts, // FILIUS to the same topology and system software (topologyWriter.test.ts,
// tools/java-oracle). // tools/java-oracle).
import { withSystemSoftware } from './beanCapture.js';
import { import {
encodeJavaBeans, encodeJavaBeans,
int, int,
@@ -165,7 +167,8 @@ function device(node: NodeToWrite, ports: BeanValue[]): BeanObject {
); );
} }
// Named for the applications, which are given it. // Named for the applications, which are given it.
const system = (node.system.applications?.length ?? 0) > 0 ? reference('SystemSoftware') : undefined; const applicationCount = (node.system.applications?.length ?? 0) + (node.system.ownApplications?.length ?? 0);
const system = applicationCount > 0 ? reference('SystemSoftware') : undefined;
const systemStatementList = systemStatements(node.system, node.javaClass, system); const systemStatementList = systemStatements(node.system, node.javaClass, system);
statements.push( statements.push(
system === undefined system === undefined
@@ -198,13 +201,20 @@ function systemStatements(system: SystemToWrite, javaClass: FlsNodeClass, self:
}); });
} }
if (system.fileSystem !== undefined) statements.push(fileSystemStatement(system.fileSystem, system.templateFiles !== false)); if (system.fileSystem !== undefined) statements.push(fileSystemStatement(system.fileSystem, system.templateFiles !== false));
if (self !== undefined && system.applications !== undefined) { if (self !== undefined) {
statements.push({ statements.push({
get: 'installierteAnwendungen', get: 'installierteAnwendungen',
statements: system.applications.map((application) => ({ statements: [
...(system.applications ?? []).map((application): BeanStatement => ({
call: 'put', call: 'put',
args: [application.key, object(application.javaClass, applicationStatements(application, self))], args: [application.key, object(application.javaClass, applicationStatements(application, self))],
})), })),
// A project's own: its bean as the file had it, on this node's system software.
...(system.ownApplications ?? []).map((application): BeanStatement => ({
call: 'put',
args: [application.key, withSystemSoftware(application.bean, self)],
})),
],
}); });
} }
if (system.dhcpServer !== undefined) { if (system.dhcpServer !== undefined) {
+118
View File
@@ -0,0 +1,118 @@
// A decoded bean written again (beanCapture.ts): decoded, captured, written
// and decoded once more, it holds the same values with the same Java types —
// so XMLDecoder calls the same setters with the same arguments. Java's own
// check is in topologyWriter.test.ts and exportProject.test.ts: the corpus's
// own applications, loaded by FILIUS from the files written again.
import { describe, expect, it } from 'vitest';
import { captureBean, SYSTEM_SOFTWARE_CLASS, withSystemSoftware } from '../src/beanCapture.js';
import { decodeJavaBeans, isJavaObject, type JavaObject, type JavaValue } from '../src/javabeans.js';
import { encodeJavaBeans, object, reference, type BeanObject } from '../src/javaBeansWriter.js';
function doc(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>\n<java version="17" class="java.beans.XMLDecoder">\n${body}\n</java>\n`;
}
function bean(value: JavaValue | undefined): JavaObject {
if (!isJavaObject(value)) throw new Error(`not an object: ${JSON.stringify(value)}`);
return value;
}
/** What the decoder keeps of an object, by value: class, arguments, properties and calls with their types. */
function shape(value: JavaValue, seen = new Set<JavaObject>()): unknown {
if (!isJavaObject(value)) return typeof value === 'bigint' ? `${value}n` : value;
if (seen.has(value)) return { shared: value.className };
seen.add(value);
return {
className: value.className,
staticField: value.staticField,
fromGetter: value.fromGetter,
args: value.args.map((arg) => shape(arg, seen)),
argKinds: value.argKinds,
properties: [...value.properties].map(([name, each]) => [name, shape(each, seen), value.propertyKinds.get(name)]),
presetElements: [...value.presetElements].map(([index, each]) => [index, shape(each, seen)]),
presetEntries: [...value.presetEntries].map(([key, each]) => [key, shape(each, seen)]),
calls: value.calls.filter((call) => call.method !== 'get').map((call) => [call.method, call.args.map((arg) => shape(arg, seen)), call.kinds]),
};
}
/** The application of a file: the second value, the first being the system software it refers to. */
const APPLICATION = doc(`<object class="filius.software.system.Betriebssystem" id="Betriebssystem0"/>
<object class="example.OwnApplication">
<short>3</short><char>x</char>
<void property="systemSoftware"><object idref="Betriebssystem0"/></void>
<void property="aktiv"><boolean>true</boolean></void>
<void property="count"><long>9007199254740993</long></void>
<void property="small"><byte>-2</byte></void>
<void property="ratio"><float>0.5</float></void>
<void property="exact"><double>0.1</double></void>
<void property="letter"><char>y</char></void>
<void property="port"><int>55555</int></void>
<void property="name"><string>Thread-7</string></void>
<void property="nothing"><null/></void>
<void property="type"><class>java.lang.String</class></void>
<void property="names"><array class="java.lang.String" length="2"><void index="0"><string>a</string></void><void index="1"><string>b</string></void></array></void>
<void property="first"><object class="java.util.ArrayList" id="ArrayList0"><void method="add"><int>1</int></void></object></void>
<void property="second"><object idref="ArrayList0"/></void>
<void property="alignment"><object class="javax.swing.SwingConstants" field="CENTER"/></void>
<void property="settings"><void property="depth"><int>4</int></void></void>
<void property="list"><void index="0"><void property="label"><string>constructed</string></void></void></void>
<void property="table"><void method="put"><string>k</string><long>5</long></void></void>
</object>`);
describe('captureBean', () => {
it('writes a bean again with the same values and Java types', () => {
const [system, application] = decodeJavaBeans(APPLICATION);
const captured = captureBean(bean(application), system);
if (!('bean' in captured)) throw new Error(captured.failure);
const written = withSystemSoftware(captured.bean, object('filius.software.system.Betriebssystem', [], []));
const [again] = decodeJavaBeans(encodeJavaBeans([written]));
const { properties: _original, ...rest } = shape(application!) as { properties: [string, unknown, unknown][] };
const { properties: _again, ...restAgain } = shape(again!) as { properties: [string, unknown, unknown][] };
expect(restAgain).toEqual(rest);
// The system software is another object now, the one it was given. A
// static constant the decoder answers with its measured value comes back
// as that value, SwingConstants.CENTER as the int it is.
const withoutSystem = (properties: [string, unknown, unknown][]) => properties.filter(([name]) => name !== 'systemSoftware');
const expected = withoutSystem((shape(application!) as { properties: [string, unknown, unknown][] }).properties).map(
([name, value, kind]) => (name === 'alignment' ? [name, value, 'int'] : [name, value, kind]),
);
expect(withoutSystem((shape(again!) as { properties: [string, unknown, unknown][] }).properties)).toEqual(expected);
expect(bean(bean(again).properties.get('systemSoftware')).className).toBe('filius.software.system.Betriebssystem');
// Shared within the bean stays shared.
expect(bean(again).properties.get('first')).toBe(bean(again).properties.get('second'));
});
it("marks the node's system software and puts the given one in its place", () => {
const [system, application] = decodeJavaBeans(APPLICATION);
const captured = captureBean(bean(application), system) as { bean: BeanObject };
const mark = captured.bean.statements!.find((statement) => 'set' in statement && statement.set === 'systemSoftware');
expect(mark).toEqual({ set: 'systemSoftware', value: { kind: 'object', className: SYSTEM_SOFTWARE_CLASS } });
const self = reference('SystemSoftware');
const replaced = withSystemSoftware(captured.bean, self) as BeanObject;
expect(replaced.statements!.find((statement) => 'set' in statement && statement.set === 'systemSoftware')).toEqual({ set: 'systemSoftware', value: self });
// The captured bean itself is left as it was.
expect(captured.bean.statements!).toContainEqual(mark);
});
it('refuses what it cannot write again, and says why', () => {
/** The last value of the file captured, the first being the system software. */
const failure = (body: string) => {
const values = decodeJavaBeans(doc(`<object class="filius.software.system.Betriebssystem" id="Betriebssystem0"/>${body}`));
return captureBean(bean(values.at(-1)), values[0]);
};
expect(failure(`<object class="example.A"><void property="x"><object idref="Missing0"/></void></object>`)).toEqual({
failure: 'a value is unresolved (idref Missing0 is not defined)',
});
expect(
failure(`<object class="example.A"><void property="x"><object class="example.Factory" method="create"/></void></object>`),
).toEqual({ failure: 'example.Factory.create makes an object this writer cannot' });
// A folder of the file system, reached elsewhere by a getter: a copy would not be that folder.
expect(
failure(`<object class="example.B"><void id="DefaultMutableTreeNode0" property="root"><void property="x"><int>1</int></void></void></object>
<object class="example.A"><void property="folder"><object idref="DefaultMutableTreeNode0"/></void></object>`),
).toEqual({ failure: 'it refers to DefaultMutableTreeNode, which a getter reaches elsewhere in the file' });
});
});
+4 -12
View File
@@ -1,16 +1,8 @@
// Warnings the corpus is expected to produce, shared by the corpus tests. // Warnings the corpus is expected to produce, shared by the corpus tests.
/** /**
* Two examples install an application of their own, compiled into the * None since the reader keeps a project's own applications as their beans
* archive (projekt/anwendungen). Java loads it from there; this reader * (beanCapture.ts); until then the two examples that install one warned
* cannot run it — and the Java oracle, reading only konfiguration.xml, does * that it was left out.
* not see it either.
*/ */
export const EXPECTED_WARNINGS: Record<string, string[]> = { export const EXPECTED_WARNINGS: Record<string, string[]> = {};
'java-examples/anwendung_komplett.fls': [
"node 1: application filius.software.clientserver.Accumulator is not part of FILIUS (a project's own application), left out",
],
'java-examples/multicast_server.fls': [
"node 1: application filius.software.clientserver.Broadcast is not part of FILIUS (a project's own application), left out",
],
};
+16 -16
View File
@@ -765,13 +765,13 @@ describe('a model beyond the corpus, as Java loads it', () => {
}), }),
// In the order of Java's key. // In the order of Java's key.
applications: [ applications: [
{ key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', zielIPAdresse: '10.0.0.9', zielPort: 7777 }, { key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', onItsSystem: true, zielIPAdresse: '10.0.0.9', zielPort: 7777 },
{ key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', aktiv: true, port: 7777 }, { key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', onItsSystem: true, aktiv: true, port: 7777 },
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 7 }, { key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', onItsSystem: true, maxTeilnehmerZahl: 7 },
{ key: 'filius.software.dns.DNSServer', class: 'filius.software.dns.DNSServer', aktiv: true, port: 53, recursiveResolutionEnabled: true }, { key: 'filius.software.dns.DNSServer', class: 'filius.software.dns.DNSServer', onItsSystem: true, aktiv: true, port: 53, recursiveResolutionEnabled: true },
{ {
key: 'filius.software.email.EmailAnwendung', key: 'filius.software.email.EmailAnwendung',
class: 'filius.software.email.EmailAnwendung', class: 'filius.software.email.EmailAnwendung', onItsSystem: true,
received: [javaMail()], received: [javaMail()],
sent: [javaMail({ versendet: true })], sent: [javaMail({ versendet: true })],
drafts: [javaMail({ betreff: '', text: '', empfaenger: [] })], drafts: [javaMail({ betreff: '', text: '', empfaenger: [] })],
@@ -792,7 +792,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
}, },
{ {
key: 'filius.software.email.EmailServer', key: 'filius.software.email.EmailServer',
class: 'filius.software.email.EmailServer', class: 'filius.software.email.EmailServer', onItsSystem: true,
aktiv: true, aktiv: true,
mailDomain: 'schule.de', mailDomain: 'schule.de',
beanAccounts: [serverAccount([javaMail()])], beanAccounts: [serverAccount([javaMail()])],
@@ -801,7 +801,7 @@ describe('a model beyond the corpus, as Java loads it', () => {
}, },
{ {
key: 'filius.software.firewall.Firewall', key: 'filius.software.firewall.Firewall',
class: 'filius.software.firewall.Firewall', class: 'filius.software.firewall.Firewall', onItsSystem: true,
activated: false, activated: false,
defaultPolicy: 1, defaultPolicy: 1,
dropICMP: false, dropICMP: false,
@@ -809,12 +809,12 @@ describe('a model beyond the corpus, as Java loads it', () => {
filterUdp: true, filterUdp: true,
rules: [['10.0.0.0', '255.0.0.0', '', '', 22, 6, 1]], 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.FileExplorer', class: 'filius.software.lokal.FileExplorer', onItsSystem: true },
{ key: 'filius.software.lokal.ImageViewer', class: 'filius.software.lokal.ImageViewer' }, { key: 'filius.software.lokal.ImageViewer', class: 'filius.software.lokal.ImageViewer', onItsSystem: true },
{ key: 'filius.software.lokal.Terminal', class: 'filius.software.lokal.Terminal' }, { key: 'filius.software.lokal.Terminal', class: 'filius.software.lokal.Terminal', onItsSystem: true },
{ key: 'filius.software.lokal.TextEditor', class: 'filius.software.lokal.TextEditor' }, { key: 'filius.software.lokal.TextEditor', class: 'filius.software.lokal.TextEditor', onItsSystem: true },
{ key: 'filius.software.www.WebBrowser', class: 'filius.software.www.WebBrowser' }, { key: 'filius.software.www.WebBrowser', class: 'filius.software.www.WebBrowser', onItsSystem: true },
{ key: 'filius.software.www.WebServer', class: 'filius.software.www.WebServer', aktiv: true, port: 8080, useVHost: true }, { key: 'filius.software.www.WebServer', class: 'filius.software.www.WebServer', onItsSystem: true, aktiv: true, port: 8080, useVHost: true },
], ],
}, },
}, },
@@ -843,11 +843,11 @@ describe('a model beyond the corpus, as Java loads it', () => {
fileSystem: { directory: 'root', children: [{ directory: 'peer2peer', children: [] }] }, fileSystem: { directory: 'root', children: [{ directory: 'peer2peer', children: [] }] },
applications: [ applications: [
// Without a setting: no target (null), the model's 3 peers, Java's personal firewall. // Without a setting: no target (null), the model's 3 peers, Java's personal firewall.
{ key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', zielIPAdresse: null, zielPort: 55_555 }, { key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', onItsSystem: true, zielIPAdresse: null, zielPort: 55_555 },
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 3 }, { key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', onItsSystem: true, maxTeilnehmerZahl: 3 },
{ {
key: 'filius.software.firewall.Firewall', key: 'filius.software.firewall.Firewall',
class: 'filius.software.firewall.Firewall', class: 'filius.software.firewall.Firewall', onItsSystem: true,
activated: true, activated: true,
defaultPolicy: 0, defaultPolicy: 0,
dropICMP: false, dropICMP: false,
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -525,7 +525,7 @@ function flsNode(javaClass: FlsNode['javaClass'], fields: Partial<FlsNode> = {})
portCount: 0, portCount: 0,
system: {}, system: {},
source: { kind: 'object', className: javaClass, id: undefined, args: [], staticField: undefined, source: { kind: 'object', className: javaClass, id: undefined, args: [], staticField: undefined,
factoryMethod: undefined, properties: new Map(), calls: [], presetElements: new Map(), presetEntries: new Map(), fromGetter: false }, factoryMethod: undefined, properties: new Map(), propertyKinds: new Map(), propertiesFromGetters: new Set(), argKinds: [], calls: [], presetElements: new Map(), presetEntries: new Map(), fromGetter: false },
...fields, ...fields,
}; };
} }
+22
View File
@@ -92,6 +92,28 @@ describe('XMLDecoder rules', () => {
expect(object(rectangle).properties.get('y')).toBe(true); expect(object(rectangle).properties.get('y')).toBe(true);
}); });
it('keeps the Java type of each primitive a setter, a constructor or a call gets', () => {
// A shared getter result, as in the corpus: a button's measured icon text gap.
const [, bean] = decodeJavaBeans(
doc(`<object class="filius.gui.netzwerksicht.JSidebarButton"><void id="Integer0" property="iconTextGap"/></object>
<object class="example.Bean"><short>3</short><char>x</char>
<void property="count"><long>7</long></void>
<void property="ratio"><float>0.5</float></void>
<void property="gap"><object idref="Integer0"/></void>
<void property="shared"><byte id="Byte0">2</byte></void>
<void property="again"><object idref="Byte0"/></void>
<void property="text"><string>7</string></void>
<void property="count2"><int>1</int></void>
<void property="count2"><string>no longer a number</string></void>
<void method="put"><string>key</string><double>1.0</double></void>
</object>`),
);
expect(object(bean).argKinds).toEqual(['short', 'char']);
expect(Object.fromEntries(object(bean).propertyKinds)).toEqual({ count: 'long', ratio: 'float', gap: 'int', shared: 'byte', again: 'byte' });
expect(object(bean).calls).toEqual([{ method: 'put', args: ['key', 1], kinds: [undefined, 'double'] }]);
});
it('says "unresolved" for a shared getter value it has no measurement for', () => { it('says "unresolved" for a shared getter value it has no measurement for', () => {
const [, holder] = decodeJavaBeans( const [, holder] = decodeJavaBeans(
doc(`<object class="com.example.Unknown"><void id="Integer0" property="size"/></object> doc(`<object class="com.example.Unknown"><void id="Integer0" property="size"/></object>
+27 -3
View File
@@ -1,7 +1,7 @@
// A project's own applications (classes under projekt/anwendungen): Java // A project's own applications (classes under projekt/anwendungen): Java
// installs them when it loads the whole archive (tools/java-oracle, laden); // installs them when it loads the whole archive (tools/java-oracle, laden).
// this package reads, writes and exports them in a later step (B2). Until // The reader keeps them as beans, without knowing what Java makes of them;
// then the tests compare Java's views without them. // its test compares them by key and class.
import { FILIUS_APPLICATION_CLASSES } from '../src/index.js'; import { FILIUS_APPLICATION_CLASSES } from '../src/index.js';
@@ -32,6 +32,30 @@ export function withoutOwnApplications<T>(view: T): T {
}; };
} }
/** Java's view with each own application as its key, class and whether it runs on its node's system software. */
export function withOwnApplicationsByClass<T>(view: T): T {
const { nodes } = view as View;
if (nodes === undefined) return view;
return {
...view,
nodes: nodes.map((node) =>
node.system.applications === undefined
? node
: {
...node,
system: {
...node.system,
applications: (node.system.applications as { key: string; class: string }[]).map((application) =>
(FILIUS_APPLICATION_CLASSES as readonly string[]).includes(application.class)
? application
: { key: application.key, class: application.class, onItsSystem: (application as { onItsSystem?: boolean }).onItsSystem },
),
},
},
),
};
}
/** Whether Java's view has an own application installed. */ /** Whether Java's view has an own application installed. */
export function hasOwnApplications(view: unknown): boolean { export function hasOwnApplications(view: unknown): boolean {
return JSON.stringify(withoutOwnApplications(view)) !== JSON.stringify(view); return JSON.stringify(withoutOwnApplications(view)) !== JSON.stringify(view);
+25 -3
View File
@@ -21,7 +21,9 @@ import {
type JavaEmailAccount, type JavaEmailAccount,
} from '../src/index.js'; } from '../src/index.js';
import { EXPECTED_WARNINGS } from './expectedWarnings.js'; import { EXPECTED_WARNINGS } from './expectedWarnings.js';
import { hasOwnApplications, OWN_APPLICATION_FILES, withoutOwnApplications } from './ownApplications.js'; import { SYSTEM_SOFTWARE_CLASS } from '../src/beanCapture.js';
import type { BeanValue } from '../src/javaBeansWriter.js';
import { hasOwnApplications, OWN_APPLICATION_FILES, withOwnApplicationsByClass } from './ownApplications.js';
import { javaEmail, javaEmailAccount } from './javaEmailNames.js'; import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus'); const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
@@ -69,12 +71,18 @@ function javaFileTree(tree: FlsFileTree): unknown {
} }
/** FlsOracle.applications' shape: Java's property names, sorted by key. */ /** FlsOracle.applications' shape: Java's property names, sorted by key. */
function isSystemSoftwareMark(value: BeanValue): boolean {
return value !== null && typeof value === 'object' && value.kind === 'object' && value.className === SYSTEM_SOFTWARE_CLASS;
}
function javaApplications(applications: FlsApplication[]): unknown[] { function javaApplications(applications: FlsApplication[]): unknown[] {
return [...applications] return [...applications]
.sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1)) .sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1))
.map((application) => ({ .map((application) => ({
key: application.key, key: application.key,
class: application.javaClass, class: application.javaClass,
// The reader's applications are its node's: each file gives them its system software.
onItsSystem: true,
...(application.isActive === undefined ? {} : { aktiv: application.isActive, port: application.port }), ...(application.isActive === undefined ? {} : { aktiv: application.isActive, port: application.port }),
...(application.targetPort === undefined ...(application.targetPort === undefined
? {} ? {}
@@ -178,7 +186,21 @@ 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 } : {}), ...('templateFiles' in node.system ? { templateFiles: undefined } : {}),
...(node.system.applications === undefined ? {} : { applications: javaApplications(node.system.applications) }), ...(node.system.applications === undefined
? {}
: {
applications: [
...(javaApplications(node.system.applications) as { key: string | null }[]),
...(node.system.ownApplications ?? []).map((application) => ({
key: application.key,
class: application.javaClass,
onItsSystem: application.bean.statements?.some(
(statement) => 'set' in statement && statement.set === 'systemSoftware' && isSystemSoftwareMark(statement.value),
),
})),
].sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1)),
}),
...('ownApplications' in node.system ? { ownApplications: undefined } : {}),
...(node.system.firewall === undefined ...(node.system.firewall === undefined
? {} ? {}
: { : {
@@ -252,7 +274,7 @@ describe('corpus: the topology as Java loads it', () => {
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( expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(
withoutTemplateFiles(withoutOwnApplications(expected), topology), withoutTemplateFiles(withOwnApplicationsByClass(expected), topology),
); );
expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []); expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []);
}); });
+18 -18
View File
@@ -18,7 +18,6 @@ import { describe, expect, it } from 'vitest';
import { decodeJavaBeans, readFlsArchive, readTopology, writeFlsArchive } from '../src/index.js'; import { decodeJavaBeans, readFlsArchive, readTopology, writeFlsArchive } from '../src/index.js';
import type { JavaEmail } from '../src/javaEmail.js'; import type { JavaEmail } from '../src/javaEmail.js';
import { writeTopologyXml, type TopologyToWrite } from '../src/topologyWriter.js'; import { writeTopologyXml, type TopologyToWrite } from '../src/topologyWriter.js';
import { withoutOwnApplications } from './ownApplications.js';
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
const corpusRoot = join(here, '../../../corpus'); const corpusRoot = join(here, '../../../corpus');
@@ -94,8 +93,10 @@ describe('the topology written again, as Java loads it', () => {
for (const file of flsFiles(corpusRoot)) { for (const file of flsFiles(corpusRoot)) {
const name = relative(corpusRoot, file); const name = relative(corpusRoot, file);
it(name, () => { it(name, () => {
const topology = readTopology(decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml)); const contents = readFlsArchive(new Uint8Array(readFileSync(file)));
const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(topology))); const topology = readTopology(decodeJavaBeans(contents.configurationXml));
// With the archive's other entries: the classes of the project's own applications.
const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(topology)), contents.supplementalEntries);
if (process.env.UPDATE_WRITTEN === '1') { if (process.env.UPDATE_WRITTEN === '1') {
mkdirSync(dirname(join(writtenRoot, name)), { recursive: true }); mkdirSync(dirname(join(writtenRoot, name)), { recursive: true });
writeFileSync(join(writtenRoot, name), archive); writeFileSync(join(writtenRoot, name), archive);
@@ -104,8 +105,7 @@ describe('the topology written again, as Java loads it', () => {
expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe( expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe(
createHash('sha256').update(archive).digest('hex'), createHash('sha256').update(archive).digest('hex'),
); );
// The written file has no archive entries: Java installs no own application (B2). expect(topologyOf(java!)).toEqual(topologyOf(withRecipientsRead(original[name]!) as JavaFile));
expect(topologyOf(java!)).toEqual(topologyOf(withRecipientsRead(withoutOwnApplications(original[name]!)) as JavaFile));
}); });
} }
@@ -448,13 +448,13 @@ describe('a topology beyond the corpus, as Java loads it', () => {
}, },
// In the order of Java's key. // In the order of Java's key.
applications: [ applications: [
{ key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', zielIPAdresse: '10.0.0.9', zielPort: 7777 }, { key: 'filius.software.clientserver.ClientBaustein', class: 'filius.software.clientserver.ClientBaustein', onItsSystem: true, zielIPAdresse: '10.0.0.9', zielPort: 7777 },
{ key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', aktiv: true, port: 7777 }, { key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', onItsSystem: true, aktiv: true, port: 7777 },
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 7 }, { key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', onItsSystem: true, maxTeilnehmerZahl: 7 },
{ key: 'filius.software.dns.DNSServer', class: 'filius.software.dns.DNSServer', aktiv: true, port: 5353, recursiveResolutionEnabled: true }, { key: 'filius.software.dns.DNSServer', class: 'filius.software.dns.DNSServer', onItsSystem: true, aktiv: true, port: 5353, recursiveResolutionEnabled: true },
{ {
key: 'filius.software.email.EmailAnwendung', key: 'filius.software.email.EmailAnwendung',
class: 'filius.software.email.EmailAnwendung', class: 'filius.software.email.EmailAnwendung', onItsSystem: true,
received: [javaMail()], received: [javaMail()],
sent: [javaMail({ versendet: true, neu: false })], sent: [javaMail({ versendet: true, neu: false })],
drafts: [javaMail({ betreff: null, text: null, dateReceived: null, empfaenger: [] })], drafts: [javaMail({ betreff: null, text: null, dateReceived: null, empfaenger: [] })],
@@ -464,7 +464,7 @@ describe('a topology beyond the corpus, as Java loads it', () => {
}, },
{ {
key: 'filius.software.email.EmailServer', key: 'filius.software.email.EmailServer',
class: 'filius.software.email.EmailServer', class: 'filius.software.email.EmailServer', onItsSystem: true,
aktiv: true, aktiv: true,
mailDomain: 'schule.de', mailDomain: 'schule.de',
beanAccounts: [ beanAccounts: [
@@ -486,7 +486,7 @@ describe('a topology beyond the corpus, as Java loads it', () => {
}, },
{ {
key: 'filius.software.firewall.Firewall', key: 'filius.software.firewall.Firewall',
class: 'filius.software.firewall.Firewall', class: 'filius.software.firewall.Firewall', onItsSystem: true,
activated: false, activated: false,
defaultPolicy: 1, defaultPolicy: 1,
dropICMP: true, dropICMP: true,
@@ -494,12 +494,12 @@ describe('a topology beyond the corpus, as Java loads it', () => {
filterUdp: false, filterUdp: false,
rules: [['10.0.0.0', '255.0.0.0', '', '', 22, 6, 1]], 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.FileExplorer', class: 'filius.software.lokal.FileExplorer', onItsSystem: true },
{ key: 'filius.software.lokal.ImageViewer', class: 'filius.software.lokal.ImageViewer' }, { key: 'filius.software.lokal.ImageViewer', class: 'filius.software.lokal.ImageViewer', onItsSystem: true },
{ key: 'filius.software.lokal.Terminal', class: 'filius.software.lokal.Terminal' }, { key: 'filius.software.lokal.Terminal', class: 'filius.software.lokal.Terminal', onItsSystem: true },
{ key: 'filius.software.lokal.TextEditor', class: 'filius.software.lokal.TextEditor' }, { key: 'filius.software.lokal.TextEditor', class: 'filius.software.lokal.TextEditor', onItsSystem: true },
{ key: 'filius.software.www.WebBrowser', class: 'filius.software.www.WebBrowser' }, { key: 'filius.software.www.WebBrowser', class: 'filius.software.www.WebBrowser', onItsSystem: true },
{ key: 'filius.software.www.WebServer', class: 'filius.software.www.WebServer', aktiv: true, port: 8080, useVHost: true }, { key: 'filius.software.www.WebServer', class: 'filius.software.www.WebServer', onItsSystem: true, aktiv: true, port: 8080, useVHost: true },
], ],
}, },
}, },
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -48,7 +48,8 @@ virtuellen Bildschirm statt „headless".
bleibt; bei Router und Heimrouter stehen vorn die Dateien, die Javas bleibt; bei Router und Heimrouter stehen vorn die Dateien, die Javas
Konstruktor aus eigenen Vorlagen erzeugt, dahinter die aus der Datei) und Konstruktor aus eigenen Vorlagen erzeugt, dahinter die aus der Datei) und
die installierten Programme eines Rechners mit ihren Einstellungen (die die installierten Programme eines Rechners mit ihren Einstellungen (die
persönliche Firewall samt Regeln) (Klassen, die Filius persönliche Firewall samt Regeln; je Programm `onItsSystem`: ob es am
Betriebssystem seines Knotens hängt) (Klassen, die Filius
nicht mitbringt — eigene Anwendungen im Archiv unter `projekt/anwendungen` 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`); ein Empfänger, den eine ältere Datei als Zeichenkette hält, samt dem, was Javas `AddressEntry(String)` daraus macht; Kabel (Enden als Gerät/Schnittstelle), — 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`); ein Empfänger, den eine ältere Datei als Zeichenkette hält, samt dem, was Javas `AddressEntry(String)` daraus macht; Kabel (Enden als Gerät/Schnittstelle),
Doku-Elemente. Doku-Elemente.
+2
View File
@@ -360,6 +360,8 @@ public class FlsOracle {
Map<String, Object> json = new LinkedHashMap<>(); Map<String, Object> json = new LinkedHashMap<>();
json.put("key", entry.getKey()); json.put("key", entry.getKey());
json.put("class", application == null ? null : application.getClass().getName()); json.put("class", application == null ? null : application.getClass().getName());
// Whether it runs on the system software of its own node.
json.put("onItsSystem", application != null && application.getSystemSoftware() == system);
if (application instanceof ServerAnwendung server) { if (application instanceof ServerAnwendung server) {
json.put("aktiv", server.isAktiv()); json.put("aktiv", server.isAktiv());
json.put("port", server.getPort()); json.put("port", server.getPort());