feat(fls): export the editor model's e-mail
exportProject.ts, step X4b, which completes the export: the e-mail beans from the model — the server's domain, accounts and mailboxes, the client's three folders and address book. The account files are written anew from the model's accounts, so that they say what the model says: /mailserver/konten.txt, which Java's server reads at every simulation start, and /konten.txt, the client's account. The format is FILIUS 2.14's (listeBenutzerkontenZuString, EmailKonto.toString) without its errors: empty fields where Java writes "null". Java's readers drop empty fields (StringTokenizer) and a trailing empty one (String.split); the files are written as a correct reader, this package's, reads them. Java escapes only a message's text; a character another field cannot carry (a separator, a line break) becomes a space there, with a warning, and the beans keep it. Checked by Java: every corpus file, imported, exported and written, loads with the e-mail programs of the original up to named departures: a string recipient read as an address (as the writer does), the sent folder's messages sent (the import's decision), the account files written anew with Java's types and without the line break older versions ended the client's with. In the synthetic model Java reads both account files back as the model means them, every escaped character of the text included. 17 mutation probes, each loaded by Java, were all caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -51,8 +51,8 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `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); 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`; 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 |
|
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; 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); 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); dazu ein künstliches Modell, das Java ebenfalls lädt |
|
||||||
| `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 |
|
||||||
|
|||||||
@@ -30,8 +30,20 @@
|
|||||||
// model keeps it), the echo server's port, the generic client's target, the
|
// model keeps it), the echo server's port, the generic client's target, the
|
||||||
// peer count, the personal firewall. A DNS server's records are in
|
// peer count, the personal firewall. A DNS server's records are in
|
||||||
// /dns/hosts, one of the files, and it forwards to the host's DNS server as
|
// /dns/hosts, one of the files, and it forwards to the host's DNS server as
|
||||||
// Java's does. E-mail accounts, messages and address books are a later
|
// Java's does.
|
||||||
// step. System software, files and programs
|
//
|
||||||
|
// E-mail: the server's domain and accounts with their mailboxes, the client's
|
||||||
|
// folders and address book, as Java's beans. The account files are written
|
||||||
|
// anew from the model's accounts, in the format Java writes them
|
||||||
|
// (EmailServer.listeBenutzerkontenZuString, EmailKonto.toString), so that
|
||||||
|
// they say what the model says: /mailserver/konten.txt, which Java's server
|
||||||
|
// reads at every simulation start, and /konten.txt, the client's account.
|
||||||
|
// Java's errors are not copied: a field is written empty where Java writes
|
||||||
|
// "null", and the format is written as Java's own readers should read it —
|
||||||
|
// they drop empty fields (StringTokenizer) and a trailing empty one
|
||||||
|
// (String.split), the readers of this package do not. A character a field
|
||||||
|
// cannot carry in the format (a separator, a line break) becomes a space,
|
||||||
|
// with a warning; the beans keep it. System software, files and programs
|
||||||
// follow in later steps.
|
// follow in later steps.
|
||||||
//
|
//
|
||||||
// What the model keeps is written as it keeps it, and so differs from the
|
// What the model keeps is written as it keeps it, and so differs from the
|
||||||
@@ -52,7 +64,10 @@
|
|||||||
// type their kind says, not the one the file named; a router's own files
|
// type their kind says, not the one the file named; a router's own files
|
||||||
// (a web server's www.conf) are not in the model;
|
// (a web server's www.conf) are not in the model;
|
||||||
// - a peer-to-peer application's peer count the file lacked (Java rolls
|
// - a peer-to-peer application's peer count the file lacked (Java rolls
|
||||||
// 3, 4 or 5) is the model's 3.
|
// 3, 4 or 5) is the model's 3;
|
||||||
|
// - e-mail as the model keeps it (importEmail.ts): an older file's client
|
||||||
|
// account in /konten.txt, where FILIUS 2.14 no longer reads it; account
|
||||||
|
// files written from the model's accounts.
|
||||||
//
|
//
|
||||||
// Checked by Java: every corpus file, imported, exported and written, loads in
|
// Checked by Java: every corpus file, imported, exported and written, loads in
|
||||||
// Java FILIUS to the topology of the original up to those departures.
|
// Java FILIUS to the topology of the original up to those departures.
|
||||||
@@ -71,6 +86,11 @@ import {
|
|||||||
type DhcpServerConfiguration,
|
type DhcpServerConfiguration,
|
||||||
type DnsServerConfiguration,
|
type DnsServerConfiguration,
|
||||||
type EchoServerConfiguration,
|
type EchoServerConfiguration,
|
||||||
|
type EmailAddress,
|
||||||
|
type EmailClientConfiguration,
|
||||||
|
type EmailContact,
|
||||||
|
type EmailMessage,
|
||||||
|
type EmailServerConfiguration,
|
||||||
type GnutellaConfiguration,
|
type GnutellaConfiguration,
|
||||||
type InstallableProgram,
|
type InstallableProgram,
|
||||||
type SimpleClientConfiguration,
|
type SimpleClientConfiguration,
|
||||||
@@ -91,6 +111,7 @@ import {
|
|||||||
type TopologyNode,
|
type TopologyNode,
|
||||||
type VirtualFileSystem,
|
type VirtualFileSystem,
|
||||||
} from '@mdg-filius/core';
|
} from '@mdg-filius/core';
|
||||||
|
import { EMAIL_CLIENT_FILE, EMAIL_SERVER_FILE, type JavaAddressEntry, type JavaContact, type JavaEmail } from './javaEmail.js';
|
||||||
import { PROGRAM_OF_CLASS } from './importProject.js';
|
import { PROGRAM_OF_CLASS } from './importProject.js';
|
||||||
import type { FlsApplicationClass, FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
|
import type { FlsApplicationClass, FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
|
||||||
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
|
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
|
||||||
@@ -118,6 +139,8 @@ export interface ProjectToExport {
|
|||||||
readonly dnsServerConfigurations: ReadonlyMap<Id, DnsServerConfiguration>;
|
readonly dnsServerConfigurations: ReadonlyMap<Id, DnsServerConfiguration>;
|
||||||
readonly echoServerConfigurations: ReadonlyMap<Id, EchoServerConfiguration>;
|
readonly echoServerConfigurations: ReadonlyMap<Id, EchoServerConfiguration>;
|
||||||
readonly simpleClientConfigurations: ReadonlyMap<Id, SimpleClientConfiguration>;
|
readonly simpleClientConfigurations: ReadonlyMap<Id, SimpleClientConfiguration>;
|
||||||
|
readonly emailClientConfigurations: ReadonlyMap<Id, EmailClientConfiguration>;
|
||||||
|
readonly emailServerConfigurations: ReadonlyMap<Id, EmailServerConfiguration>;
|
||||||
readonly documentationItems: readonly DocumentationItem[];
|
readonly documentationItems: readonly DocumentationItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +167,7 @@ export function exportProject(project: ProjectToExport): ExportedTopology {
|
|||||||
});
|
});
|
||||||
const indexOfNode = new Map(exported.map((node, index) => [node.id, index]));
|
const indexOfNode = new Map(exported.map((node, index) => [node.id, index]));
|
||||||
const servingModems = firstOfEachPair(exported, project);
|
const servingModems = firstOfEachPair(exported, project);
|
||||||
const nodes = exported.map((node) => exportNode(node, project, servingModems));
|
const nodes = exported.map((node) => exportNode(node, project, servingModems, warnings));
|
||||||
|
|
||||||
const links: FlsLink[] = [];
|
const links: FlsLink[] = [];
|
||||||
project.graph.links.forEach((link, index) => {
|
project.graph.links.forEach((link, index) => {
|
||||||
@@ -172,7 +195,7 @@ export function exportProject(project: ProjectToExport): ExportedTopology {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportNode(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>): NodeToWrite {
|
function exportNode(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>, warnings: string[]): NodeToWrite {
|
||||||
const javaClass = CLASS_OF_KIND[node.kind as Exclude<NodeKind, 'unsupported'>];
|
const javaClass = CLASS_OF_KIND[node.kind as Exclude<NodeKind, 'unsupported'>];
|
||||||
const interfaces = exportInterfaces(node, project);
|
const interfaces = exportInterfaces(node, project);
|
||||||
return {
|
return {
|
||||||
@@ -184,7 +207,7 @@ function exportNode(node: TopologyNode, project: ProjectToExport, servingModems:
|
|||||||
useMacAsName: showsMac(node.hostLabelPolicy),
|
useMacAsName: showsMac(node.hostLabelPolicy),
|
||||||
interfaces,
|
interfaces,
|
||||||
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
|
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
|
||||||
system: exportSystem(node, project, servingModems),
|
system: exportSystem(node, project, servingModems, (message) => warnings.push(`node '${node.displayName}': ${message}`)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +234,9 @@ function modemPort(pairIdentifier: string | undefined): number {
|
|||||||
return port >= 1 && port <= 65_535 ? port : DEFAULT_MODEM_PORT;
|
return port >= 1 && port <= 65_535 ? port : DEFAULT_MODEM_PORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportSystem(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>): SystemToWrite {
|
type Warn = (message: string) => void;
|
||||||
|
|
||||||
|
function exportSystem(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>, warn: Warn): SystemToWrite {
|
||||||
const id = node.id;
|
const id = node.id;
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case 'networkSwitch': {
|
case 'networkSwitch': {
|
||||||
@@ -246,12 +271,12 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
|
|||||||
case 'notebook': {
|
case 'notebook': {
|
||||||
const ssid = project.hostWirelessConfigurations.get(id)?.ssid ?? '';
|
const ssid = project.hostWirelessConfigurations.get(id)?.ssid ?? '';
|
||||||
const files = project.virtualFileSystems.get(id);
|
const files = project.virtualFileSystems.get(id);
|
||||||
return {
|
const root: FlsFileTree = { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') };
|
||||||
...system,
|
const client = project.installedPrograms.get(id)?.has('emailClient') === true ? project.emailClientConfigurations.get(id) : undefined;
|
||||||
ssid: ssid === '' ? null : ssid,
|
const server = project.installedPrograms.get(id)?.has('emailServer') === true ? project.emailServerConfigurations.get(id) : undefined;
|
||||||
fileSystem: { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') },
|
if (client?.account != null) putFile(root, EMAIL_CLIENT_FILE, 'text/txt', clientAccountFile(client, warn));
|
||||||
applications: exportPrograms(id, project),
|
if (server !== undefined) putFile(root, EMAIL_SERVER_FILE, 'txt', serverAccountsFile(server, warn));
|
||||||
};
|
return { ...system, ssid: ssid === '' ? null : ssid, fileSystem: root, applications: exportPrograms(id, project) };
|
||||||
}
|
}
|
||||||
case 'router':
|
case 'router':
|
||||||
return {
|
return {
|
||||||
@@ -310,14 +335,159 @@ function exportPrograms(id: Id, project: ProjectToExport): ApplicationToWrite[]
|
|||||||
return { ...application, maxPeers: project.gnutellaConfigurations.get(id)?.maximumKnownPeers ?? 3 };
|
return { ...application, maxPeers: project.gnutellaConfigurations.get(id)?.maximumKnownPeers ?? 3 };
|
||||||
case 'firewall':
|
case 'firewall':
|
||||||
return { ...application, firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? JAVA_PERSONAL_FIREWALL_DEFAULTS) };
|
return { ...application, firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? JAVA_PERSONAL_FIREWALL_DEFAULTS) };
|
||||||
case 'emailServer':
|
case 'emailServer': {
|
||||||
return { ...application, isActive };
|
const server = project.emailServerConfigurations.get(id);
|
||||||
|
return {
|
||||||
|
...application,
|
||||||
|
isActive,
|
||||||
|
mailDomain: server?.domain ?? 'filius.de',
|
||||||
|
beanAccounts: (server?.accounts ?? []).map((account) => ({
|
||||||
|
username: account.username,
|
||||||
|
password: account.password,
|
||||||
|
lastName: account.lastName,
|
||||||
|
firstName: account.firstName,
|
||||||
|
emailAddress: null,
|
||||||
|
pop3Server: null,
|
||||||
|
pop3Port: null,
|
||||||
|
smtpServer: null,
|
||||||
|
smtpPort: null,
|
||||||
|
messages: account.mailbox.map(javaEmail),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'emailClient': {
|
||||||
|
const client = project.emailClientConfigurations.get(id);
|
||||||
|
return {
|
||||||
|
...application,
|
||||||
|
received: (client?.inbox ?? []).map(javaEmail),
|
||||||
|
sent: (client?.sent ?? []).map(javaEmail),
|
||||||
|
drafts: (client?.drafts ?? []).map(javaEmail),
|
||||||
|
addressBook: (client?.addressBook ?? []).map(javaContact),
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return application;
|
return application;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function javaAddress(address: EmailAddress): JavaAddressEntry {
|
||||||
|
return { name: address.name, mailAddress: address.mailAddress };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A message as Java's Email; its received time as the digits Java keeps, none as Java's "". */
|
||||||
|
function javaEmail(message: EmailMessage): JavaEmail {
|
||||||
|
return {
|
||||||
|
sender: javaAddress(message.from),
|
||||||
|
to: message.to.map(javaAddress),
|
||||||
|
cc: message.cc.map(javaAddress),
|
||||||
|
bcc: message.bcc.map(javaAddress),
|
||||||
|
subject: message.subject,
|
||||||
|
text: message.body,
|
||||||
|
dateReceived: message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
||||||
|
isNew: message.isNew,
|
||||||
|
isDeleted: message.isMarkedForDeletion,
|
||||||
|
isSent: message.isSent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function javaContact(contact: EmailContact): JavaContact {
|
||||||
|
return { ...contact };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A field of an account file: each character it cannot carry becomes a space, with a warning. */
|
||||||
|
function field(value: string, forbidden: RegExp, what: string, warn: Warn): string {
|
||||||
|
if (!forbidden.test(value)) return value;
|
||||||
|
warn(`${what} holds a character Java's account file cannot carry, written there with a space`);
|
||||||
|
return value.replace(new RegExp(forbidden.source, 'g'), ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EmailKonto.toString: nine fields; a field may hold no ';' and no line break. */
|
||||||
|
function clientAccountFile(client: EmailClientConfiguration, warn: Warn): string {
|
||||||
|
const account = client.account!;
|
||||||
|
const text = (value: string, what: string) => field(value, /[;\r\n]/, `${EMAIL_CLIENT_FILE}: ${what}`, warn);
|
||||||
|
return [
|
||||||
|
text(account.pop3Host, 'POP3 server'),
|
||||||
|
text(account.smtpHost, 'SMTP server'),
|
||||||
|
String(account.pop3Port),
|
||||||
|
String(account.smtpPort),
|
||||||
|
text(account.username, 'user name'),
|
||||||
|
text(account.password, 'password'),
|
||||||
|
text(account.lastName, 'last name'),
|
||||||
|
text(account.firstName, 'first name'),
|
||||||
|
text(account.email, 'address'),
|
||||||
|
].join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailServer.listeBenutzerkontenZuString: per account a line of user,
|
||||||
|
* domain, password, last and first name, then its messages, each "#" and
|
||||||
|
* seven fields joined by "$" — the text with Java's escapes
|
||||||
|
* (replaceSpecialChar, then line breaks as "|"), the others unescaped.
|
||||||
|
*/
|
||||||
|
function serverAccountsFile(server: EmailServerConfiguration, warn: Warn): string {
|
||||||
|
const where = (what: string) => `${EMAIL_SERVER_FILE}: ${what}`;
|
||||||
|
const plain = (value: string, what: string) => field(value, /[;#$\r\n]/, where(what), warn);
|
||||||
|
const address = (entry: EmailAddress, what: string) => {
|
||||||
|
const name = entry.name === null ? null : field(entry.name, /[;#$,<>\r\n]/, where(what), warn);
|
||||||
|
const mail = entry.mailAddress === null ? null : field(entry.mailAddress, /[;#$,<>\r\n]/, where(what), warn);
|
||||||
|
if (name === null) return mail === null ? '' : `<${mail}>`;
|
||||||
|
return mail === null ? name : `${name} <${mail}>`;
|
||||||
|
};
|
||||||
|
const addresses = (entries: readonly EmailAddress[], what: string) => entries.map((entry) => address(entry, what)).join(',');
|
||||||
|
const escapedText = (text: string) =>
|
||||||
|
text.replace(/\$/g, '&&00036&&').replace(/;/g, '&&00059&&').replace(/\|/g, '&&00124&&').replace(/#/g, '&&00035&&').replace(/\n/g, '|');
|
||||||
|
return server.accounts
|
||||||
|
.map((account) => {
|
||||||
|
const messages = account.mailbox
|
||||||
|
.map(
|
||||||
|
(message) =>
|
||||||
|
'#' +
|
||||||
|
[
|
||||||
|
address(message.from, 'a sender'),
|
||||||
|
addresses(message.to, 'a recipient'),
|
||||||
|
addresses(message.cc, 'a recipient'),
|
||||||
|
addresses(message.bcc, 'a recipient'),
|
||||||
|
message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
||||||
|
plain(message.subject, 'a subject'),
|
||||||
|
field(escapedText(message.body), /\r$/, where('a text'), warn),
|
||||||
|
].join('$'),
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
return [
|
||||||
|
plain(account.username, 'a user name'),
|
||||||
|
server.domain,
|
||||||
|
plain(account.password, 'a password'),
|
||||||
|
plain(account.lastName, 'a last name'),
|
||||||
|
plain(account.firstName, 'a first name'),
|
||||||
|
messages,
|
||||||
|
].join(';') + '\n';
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file at an absolute path of the root's tree, replacing one of the same
|
||||||
|
* name in any case (Java's holeDatei finds it so); directories are made.
|
||||||
|
*/
|
||||||
|
function putFile(root: FlsFileTree, path: string, type: string, content: string): void {
|
||||||
|
const names = path.split('/').filter((name) => name !== '');
|
||||||
|
let directory = root;
|
||||||
|
for (const name of names.slice(0, -1)) {
|
||||||
|
let next = directory.children.find((child) => child.kind === 'directory' && child.name?.toLowerCase() === name);
|
||||||
|
if (next === undefined) {
|
||||||
|
next = { kind: 'directory', name, children: [] };
|
||||||
|
directory.children.push(next);
|
||||||
|
}
|
||||||
|
directory = next;
|
||||||
|
}
|
||||||
|
const name = names.at(-1)!;
|
||||||
|
const file: FlsFileTree = { kind: 'file', name, type, content, children: [] };
|
||||||
|
const index = directory.children.findIndex((child) => child.kind === 'file' && child.name?.toLowerCase() === name);
|
||||||
|
if (index < 0) directory.children.push(file);
|
||||||
|
else directory.children[index] = file;
|
||||||
|
}
|
||||||
|
|
||||||
/** The text types Java's importer reads as text, named by a file's extension. */
|
/** The text types Java's importer reads as text, named by a file's extension. */
|
||||||
const TEXT_EXTENSIONS = new Set(['txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
|
const TEXT_EXTENSIONS = new Set(['txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
firewallConfiguration,
|
firewallConfiguration,
|
||||||
firewallRule,
|
firewallRule,
|
||||||
stableGeneratedMacAddress,
|
stableGeneratedMacAddress,
|
||||||
|
type EmailMessage,
|
||||||
type Id,
|
type Id,
|
||||||
type InstallableProgram,
|
type InstallableProgram,
|
||||||
type TopologyNode,
|
type TopologyNode,
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
readFlsArchive,
|
readFlsArchive,
|
||||||
readTopology,
|
readTopology,
|
||||||
writeFlsArchive,
|
writeFlsArchive,
|
||||||
|
type FlsFileTree,
|
||||||
type ProjectToExport,
|
type ProjectToExport,
|
||||||
} from '../src/index.js';
|
} from '../src/index.js';
|
||||||
import { writeTopologyXml } from '../src/topologyWriter.js';
|
import { writeTopologyXml } from '../src/topologyWriter.js';
|
||||||
@@ -69,21 +71,8 @@ const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), '
|
|||||||
const exportedPath = join(here, 'exported.java.json');
|
const exportedPath = join(here, 'exported.java.json');
|
||||||
const exported = JSON.parse(readFileSync(exportedPath, 'utf8')) as Record<string, JavaFile>;
|
const exported = JSON.parse(readFileSync(exportedPath, 'utf8')) as Record<string, JavaFile>;
|
||||||
|
|
||||||
/**
|
/** The system software with its files in any order. */
|
||||||
* The system software without what a later step exports — the e-mail
|
function systemOf(settings: Record<string, unknown>) {
|
||||||
* programs' domain, bean accounts, folders and address book; files in any
|
|
||||||
* order.
|
|
||||||
*/
|
|
||||||
function systemOf(system: Record<string, unknown>) {
|
|
||||||
const settings =
|
|
||||||
system.applications === undefined
|
|
||||||
? system
|
|
||||||
: {
|
|
||||||
...system,
|
|
||||||
applications: (system.applications as Record<string, unknown>[]).map(
|
|
||||||
({ mailDomain: _domain, beanAccounts: _accounts, received: _received, sent: _sent, drafts: _drafts, addressBook: _book, ...rest }) => rest,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return settings.fileSystem === undefined || settings.fileSystem === null
|
return settings.fileSystem === undefined || settings.fileSystem === null
|
||||||
? settings
|
? settings
|
||||||
: { ...settings, fileSystem: sortedTree(settings.fileSystem as JavaTree) };
|
: { ...settings, fileSystem: sortedTree(settings.fileSystem as JavaTree) };
|
||||||
@@ -127,7 +116,16 @@ function topologyOf(view: JavaFile) {
|
|||||||
* below), not the one the file named — Java names them unevenly itself
|
* below), not the one the file named — Java names them unevenly itself
|
||||||
* (txt, text/txt, html, none);
|
* (txt, text/txt, html, none);
|
||||||
* - fileOrder: directories first, then by name (compared regardless of
|
* - fileOrder: directories first, then by name (compared regardless of
|
||||||
* order here).
|
* order here);
|
||||||
|
* - recipientsAsStrings: a recipient an older file keeps as a string is the
|
||||||
|
* address Java's AddressEntry(String) reads from it (the oracle reports
|
||||||
|
* it next to the string), as the import reads it — Java puts the string
|
||||||
|
* into its typed list unchecked and its e-mail window fails on it;
|
||||||
|
* - sentFolder: the messages of the client's sent folder are sent ones;
|
||||||
|
* - accountFiles: /konten.txt and /mailserver/konten.txt are written anew
|
||||||
|
* from the model's accounts as FILIUS 2.14 writes them — with its types
|
||||||
|
* (text/txt, txt), the client's line without the line break older
|
||||||
|
* versions ended it with.
|
||||||
*/
|
*/
|
||||||
const DEPARTURES = {
|
const DEPARTURES = {
|
||||||
generatedMac(view: JavaFile, macs: (string | undefined)[][]): JavaFile {
|
generatedMac(view: JavaFile, macs: (string | undefined)[][]): JavaFile {
|
||||||
@@ -174,6 +172,38 @@ const DEPARTURES = {
|
|||||||
: { ...node.system, fileSystem: retype(node.system.fileSystem as JavaTree) },
|
: { ...node.system, fileSystem: retype(node.system.fileSystem as JavaTree) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
recipientsAsStrings(view: JavaFile): JavaFile {
|
||||||
|
return withRecipientsRead(view) as JavaFile;
|
||||||
|
},
|
||||||
|
sentFolder(view: JavaFile): JavaFile {
|
||||||
|
return mapSystems(view, (node) =>
|
||||||
|
node.system.applications === undefined
|
||||||
|
? node.system
|
||||||
|
: {
|
||||||
|
...node.system,
|
||||||
|
applications: (node.system.applications as Record<string, unknown>[]).map((application) =>
|
||||||
|
application.sent === undefined
|
||||||
|
? application
|
||||||
|
: { ...application, sent: (application.sent as Record<string, unknown>[]).map((message) => ({ ...message, versendet: true })) },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
accountFiles(view: JavaFile, contents: (Map<string, string> | undefined)[]): JavaFile {
|
||||||
|
/** The tree at a path ("" for the root), with each account file as the export writes it. */
|
||||||
|
const rewrite = (tree: JavaTree, path: string, files: Map<string, string>): JavaTree => {
|
||||||
|
const children = tree.children.map((child) => rewrite(child, `${path}/${String(child.file ?? child.directory)}`, files));
|
||||||
|
const content = 'file' in tree ? files.get(path) : undefined;
|
||||||
|
if (content === undefined) return { ...tree, children };
|
||||||
|
const client = path.toLowerCase() === '/konten.txt';
|
||||||
|
return { ...tree, ...javaFile(tree.file as string, client ? 'text/txt' : 'txt', client ? content.replace(/\n$/, '') : content), children };
|
||||||
|
};
|
||||||
|
return mapSystems(view, (node, index) => {
|
||||||
|
const tree = node.system.fileSystem as JavaTree | null | undefined;
|
||||||
|
const files = contents[index];
|
||||||
|
return tree === undefined || tree === null || files === undefined ? node.system : { ...node.system, fileSystem: rewrite(tree, '', files) };
|
||||||
|
});
|
||||||
|
},
|
||||||
documentation(view: JavaFile): JavaFile {
|
documentation(view: JavaFile): JavaFile {
|
||||||
const items = (view.documentation as Record<string, unknown>[]).map((item) => {
|
const items = (view.documentation as Record<string, unknown>[]).map((item) => {
|
||||||
const [x, y, width, height] = item.bounds as number[];
|
const [x, y, width, height] = item.bounds as number[];
|
||||||
@@ -188,7 +218,7 @@ const DEPARTURES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type JavaTree = { file?: string; type?: string | null; children: JavaTree[] } & Record<string, unknown>;
|
type JavaTree = { file?: string; directory?: string | null; type?: string | null; children: JavaTree[] } & Record<string, unknown>;
|
||||||
|
|
||||||
const IMAGE_TYPES = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'image']);
|
const IMAGE_TYPES = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'image']);
|
||||||
const TEXT_TYPES = new Set(['', 'text', 'txt', 'text/txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
|
const TEXT_TYPES = new Set(['', 'text', 'txt', 'text/txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
|
||||||
@@ -227,6 +257,42 @@ function fileTrees(view: JavaFile, sorted: boolean): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contents of /konten.txt (with the client's account) and
|
||||||
|
* /mailserver/konten.txt (with the server) in a host's files, as the file
|
||||||
|
* has them: the ones the export writes anew.
|
||||||
|
*/
|
||||||
|
function accountFileContents(root: FlsFileTree | undefined, project: ReturnType<typeof importProject>, id: Id): Map<string, string> | undefined {
|
||||||
|
if (root === undefined) return undefined;
|
||||||
|
const result = new Map<string, string>();
|
||||||
|
const programs = project.installedPrograms.get(id);
|
||||||
|
const child = (tree: FlsFileTree, name: string) => tree.children.find((each) => each.kind !== 'other' && each.name?.toLowerCase() === name);
|
||||||
|
const client = child(root, 'konten.txt');
|
||||||
|
if (programs?.has('emailClient') && project.emailClientConfigurations.get(id)?.account != null && client?.kind === 'file') {
|
||||||
|
result.set(`/${client.name}`, client.content ?? '');
|
||||||
|
}
|
||||||
|
const directory = child(root, 'mailserver');
|
||||||
|
const server = directory?.kind === 'directory' ? child(directory, 'konten.txt') : undefined;
|
||||||
|
if (programs?.has('emailServer') && directory?.kind === 'directory' && server?.kind === 'file') {
|
||||||
|
result.set(`/${directory.name}/${server.name}`, server.content ?? '');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Each recipient as Java reads its string, none null. */
|
||||||
|
function withRecipientsRead(value: unknown, key = ''): unknown {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (key === 'empfaenger' || key === 'cc' || key === 'bcc') {
|
||||||
|
return value.filter((entry) => entry !== null).map((entry) => (entry[0] === 'string' ? entry[2] : entry));
|
||||||
|
}
|
||||||
|
return value.map((item) => withRecipientsRead(item));
|
||||||
|
}
|
||||||
|
if (value !== null && typeof value === 'object') {
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, withRecipientsRead(item, name)]));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function mapSystems(view: JavaFile, system: (node: JavaNode, index: number) => Record<string, unknown>): JavaFile {
|
function mapSystems(view: JavaFile, system: (node: JavaNode, index: number) => Record<string, unknown>): JavaFile {
|
||||||
return { ...view, nodes: view.nodes.map((node, index) => ({ ...node, system: system(node, index) })) };
|
return { ...view, nodes: view.nodes.map((node, index) => ({ ...node, system: system(node, index) })) };
|
||||||
}
|
}
|
||||||
@@ -249,6 +315,9 @@ const departed = {
|
|||||||
routerFiles: new Set<string>(),
|
routerFiles: new Set<string>(),
|
||||||
fileTypes: new Set<string>(),
|
fileTypes: new Set<string>(),
|
||||||
fileOrder: new Set<string>(),
|
fileOrder: new Set<string>(),
|
||||||
|
recipientsAsStrings: new Set<string>(),
|
||||||
|
sentFolder: new Set<string>(),
|
||||||
|
accountFiles: new Set<string>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('the editor model exported, as Java loads it', () => {
|
describe('the editor model exported, as Java loads it', () => {
|
||||||
@@ -278,6 +347,8 @@ describe('the editor model exported, as Java loads it', () => {
|
|||||||
const source = original[name]!;
|
const source = original[name]!;
|
||||||
const nodeIds = project.graph.nodes.map((node) => node.id);
|
const nodeIds = project.graph.nodes.map((node) => node.id);
|
||||||
const pairs = nodeIds.map((id) => project.remoteLinkConfigurations.get(id)?.pairIdentifier);
|
const pairs = nodeIds.map((id) => project.remoteLinkConfigurations.get(id)?.pairIdentifier);
|
||||||
|
// The account files of each host the export writes anew, by path, as the file has them.
|
||||||
|
const accountFiles = topology.nodes.map((node, index) => accountFileContents(node.system.fileSystem, project, nodeIds[index]!));
|
||||||
// What each router's file adds to its constructor's files.
|
// What each router's file adds to its constructor's files.
|
||||||
const added = topology.nodes.map((node) => (node.system.templateFiles === true ? (node.system.fileSystem?.children.length ?? 0) : 0));
|
const added = topology.nodes.map((node) => (node.system.templateFiles === true ? (node.system.fileSystem?.children.length ?? 0) : 0));
|
||||||
const steps: [keyof typeof departed, (view: JavaFile) => JavaFile][] = [
|
const steps: [keyof typeof departed, (view: JavaFile) => JavaFile][] = [
|
||||||
@@ -286,6 +357,9 @@ describe('the editor model exported, as Java loads it', () => {
|
|||||||
['modemRoles', (view) => DEPARTURES.modemRoles(view, pairs)],
|
['modemRoles', (view) => DEPARTURES.modemRoles(view, pairs)],
|
||||||
['routerFiles', (view) => DEPARTURES.routerFiles(view, added)],
|
['routerFiles', (view) => DEPARTURES.routerFiles(view, added)],
|
||||||
['fileTypes', (view) => DEPARTURES.fileTypes(view)],
|
['fileTypes', (view) => DEPARTURES.fileTypes(view)],
|
||||||
|
['recipientsAsStrings', (view) => DEPARTURES.recipientsAsStrings(view)],
|
||||||
|
['sentFolder', (view) => DEPARTURES.sentFolder(view)],
|
||||||
|
['accountFiles', (view) => DEPARTURES.accountFiles(view, accountFiles)],
|
||||||
['documentation', (view) => DEPARTURES.documentation(view)],
|
['documentation', (view) => DEPARTURES.documentation(view)],
|
||||||
];
|
];
|
||||||
let expected = source;
|
let expected = source;
|
||||||
@@ -335,12 +409,58 @@ function emptyProject(): ProjectToExport {
|
|||||||
dnsServerConfigurations: new Map(),
|
dnsServerConfigurations: new Map(),
|
||||||
echoServerConfigurations: new Map(),
|
echoServerConfigurations: new Map(),
|
||||||
simpleClientConfigurations: new Map(),
|
simpleClientConfigurations: new Map(),
|
||||||
|
emailClientConfigurations: new Map(),
|
||||||
|
emailServerConfigurations: new Map(),
|
||||||
documentationItems: [],
|
documentationItems: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = (id: string) => ({ id, label: id, isOccupied: false });
|
const port = (id: string) => ({ id, label: id, isOccupied: false });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message with every character Java escapes in a text, a separator in its
|
||||||
|
* subject (a space in the account file), a sender with name and address,
|
||||||
|
* recipients without a name.
|
||||||
|
*/
|
||||||
|
const SYNTHETIC_MAIL: EmailMessage = {
|
||||||
|
id: 1,
|
||||||
|
from: { name: 'Anna', mailAddress: 'anna@schule.de' },
|
||||||
|
to: [
|
||||||
|
{ name: null, mailAddress: 'ben@schule.de' },
|
||||||
|
{ name: 'Carla', mailAddress: 'carla@schule.de' },
|
||||||
|
],
|
||||||
|
cc: [],
|
||||||
|
bcc: [{ name: null, mailAddress: 'eva@schule.de' }],
|
||||||
|
subject: 'Hallo; Welt',
|
||||||
|
body: 'a$b;c|d#e\nzweite Zeile',
|
||||||
|
receivedAtMilliseconds: 1_727_690_000_000,
|
||||||
|
isNew: false,
|
||||||
|
isMarkedForDeletion: true,
|
||||||
|
isSent: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** SYNTHETIC_MAIL as the oracle describes it after Java loaded it. */
|
||||||
|
function javaMail(changes: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
absender: ['Anna', 'anna@schule.de'],
|
||||||
|
empfaenger: [[null, 'ben@schule.de'], ['Carla', 'carla@schule.de']],
|
||||||
|
cc: [],
|
||||||
|
bcc: [[null, 'eva@schule.de']],
|
||||||
|
betreff: 'Hallo; Welt',
|
||||||
|
text: 'a$b;c|d#e\nzweite Zeile',
|
||||||
|
dateReceived: '1727690000000',
|
||||||
|
neu: false,
|
||||||
|
delete: true,
|
||||||
|
versendet: false,
|
||||||
|
...changes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVER_ACCOUNTS_FILE =
|
||||||
|
'anna;schule.de;geheim;Muster;Anna;#Anna <anna@schule.de>$<ben@schule.de>,Carla <carla@schule.de>$$<eva@schule.de>$1727690000000$Hallo Welt$' +
|
||||||
|
'a&&00036&&b&&00059&&c&&00124&&d&&00035&&e|zweite Zeile\n';
|
||||||
|
const CLIENT_ACCOUNT_FILE = '10.0.0.1;10.0.0.1;110;25;anna;geheim;Muster;Anna;anna@schule.de';
|
||||||
|
|
||||||
/** Bytes of a length: (index * 37 + 11) mod 256. */
|
/** Bytes of a length: (index * 37 + 11) mod 256. */
|
||||||
function bytesOf(length: number): Uint8Array {
|
function bytesOf(length: number): Uint8Array {
|
||||||
return Uint8Array.from({ length }, (_, index) => (index * 37 + 11) % 256);
|
return Uint8Array.from({ length }, (_, index) => (index * 37 + 11) % 256);
|
||||||
@@ -484,11 +604,49 @@ const SYNTHETIC: ProjectToExport = {
|
|||||||
'fileExplorer',
|
'fileExplorer',
|
||||||
'imageViewer',
|
'imageViewer',
|
||||||
'webBrowser',
|
'webBrowser',
|
||||||
|
'emailServer',
|
||||||
|
'emailClient',
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
['nb', new Set<InstallableProgram>(['gnutella', 'simpleClient', 'firewall'])],
|
['nb', new Set<InstallableProgram>(['gnutella', 'simpleClient', 'firewall'])],
|
||||||
]),
|
]),
|
||||||
activePrograms: new Map([['pc', new Set<InstallableProgram>(['webServer', 'dnsServer', 'echoServer'])]]),
|
emailServerConfigurations: new Map([
|
||||||
|
[
|
||||||
|
'pc',
|
||||||
|
{
|
||||||
|
domain: 'schule.de',
|
||||||
|
pop3Port: 110,
|
||||||
|
accounts: [{ username: 'anna', password: 'geheim', firstName: 'Anna', lastName: 'Muster', mailbox: [SYNTHETIC_MAIL] }],
|
||||||
|
nextMessageID: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
emailClientConfigurations: new Map([
|
||||||
|
[
|
||||||
|
'pc',
|
||||||
|
{
|
||||||
|
account: {
|
||||||
|
pop3Host: '10.0.0.1',
|
||||||
|
pop3Port: 110,
|
||||||
|
smtpHost: '10.0.0.1',
|
||||||
|
smtpPort: 25,
|
||||||
|
username: 'anna',
|
||||||
|
password: 'geheim',
|
||||||
|
firstName: 'Anna',
|
||||||
|
lastName: 'Muster',
|
||||||
|
email: 'anna@schule.de',
|
||||||
|
},
|
||||||
|
inbox: [SYNTHETIC_MAIL],
|
||||||
|
sent: [{ ...SYNTHETIC_MAIL, id: 2, isSent: true }],
|
||||||
|
drafts: [{ ...SYNTHETIC_MAIL, id: 3, subject: '', body: '', to: [] }],
|
||||||
|
addressBook: [
|
||||||
|
{ name: 'Muster', firstName: 'Anna', street: 'Weg 1', houseNumber: 12, postalCode: 20095, city: 'Hamburg', email: 'anna@schule.de', phone: '040' },
|
||||||
|
],
|
||||||
|
nextMessageID: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
activePrograms: new Map([['pc', new Set<InstallableProgram>(['webServer', 'dnsServer', 'echoServer', 'emailServer'])]]),
|
||||||
webServerConfigurations: new Map([['pc', { port: 8080, documentRoot: '/webserver', virtualHosts: true }]]),
|
webServerConfigurations: new Map([['pc', { port: 8080, documentRoot: '/webserver', virtualHosts: true }]]),
|
||||||
dnsServerConfigurations: new Map([['pc', { recursiveResolutionEnabled: true, records: [] }]]),
|
dnsServerConfigurations: new Map([['pc', { recursiveResolutionEnabled: true, records: [] }]]),
|
||||||
echoServerConfigurations: new Map([['pc', { port: 7777 }]]),
|
echoServerConfigurations: new Map([['pc', { port: 7777 }]]),
|
||||||
@@ -542,7 +700,19 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
|||||||
expect(java?.sha256, 'exported.java.json is not about this output: UPDATE_EXPORTED=1, then npm run oracle:java').toBe(
|
expect(java?.sha256, 'exported.java.json is not about this output: UPDATE_EXPORTED=1, then npm run oracle:java').toBe(
|
||||||
createHash('sha256').update(archive).digest('hex'),
|
createHash('sha256').update(archive).digest('hex'),
|
||||||
);
|
);
|
||||||
expect(warnings).toEqual([]);
|
expect(warnings).toEqual(["node 'PC': /mailserver/konten.txt: a subject holds a character Java's account file cannot carry, written there with a space"]);
|
||||||
|
const serverAccount = (nachrichten: unknown[]) => ({
|
||||||
|
benutzername: 'anna',
|
||||||
|
passwort: 'geheim',
|
||||||
|
nachname: 'Muster',
|
||||||
|
vorname: 'Anna',
|
||||||
|
emailAdresse: null,
|
||||||
|
pop3server: null,
|
||||||
|
pop3port: null,
|
||||||
|
smtpserver: null,
|
||||||
|
smtpport: null,
|
||||||
|
nachrichten,
|
||||||
|
});
|
||||||
const card = (ip: string, mask: string, mac: string, gateway: string, dns: string, wireless = false) => ({ ip, mask, mac, gateway, dns, wireless });
|
const card = (ip: string, mask: string, mac: string, gateway: string, dns: string, wireless = false) => ({ ip, mask, mac, gateway, dns, wireless });
|
||||||
const modem = (name: string, x: number, mode: number, port: number) => ({
|
const modem = (name: string, x: number, mode: number, port: number) => ({
|
||||||
class: 'Modem',
|
class: 'Modem',
|
||||||
@@ -582,6 +752,8 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
|||||||
javaFile('roh', '', javaBase64(bytesOf(3))),
|
javaFile('roh', '', javaBase64(bytesOf(3))),
|
||||||
{ directory: 'webserver', children: [javaFile('index.html', 'html', '<p>own</p>')] },
|
{ directory: 'webserver', children: [javaFile('index.html', 'html', '<p>own</p>')] },
|
||||||
{ directory: 'peer2peer', children: [] },
|
{ directory: 'peer2peer', children: [] },
|
||||||
|
javaFile('konten.txt', 'text/txt', CLIENT_ACCOUNT_FILE),
|
||||||
|
{ directory: 'mailserver', children: [javaFile('konten.txt', 'txt', SERVER_ACCOUNTS_FILE)] },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
// In the order of Java's key.
|
// In the order of Java's key.
|
||||||
@@ -590,6 +762,36 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
|||||||
{ key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', aktiv: true, port: 7777 },
|
{ key: 'filius.software.clientserver.ServerBaustein', class: 'filius.software.clientserver.ServerBaustein', aktiv: true, port: 7777 },
|
||||||
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', maxTeilnehmerZahl: 7 },
|
{ key: 'filius.software.dateiaustausch.PeerToPeerAnwendung', class: 'filius.software.dateiaustausch.PeerToPeerAnwendung', 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', aktiv: true, port: 53, recursiveResolutionEnabled: true },
|
||||||
|
{
|
||||||
|
key: 'filius.software.email.EmailAnwendung',
|
||||||
|
class: 'filius.software.email.EmailAnwendung',
|
||||||
|
received: [javaMail()],
|
||||||
|
sent: [javaMail({ versendet: true })],
|
||||||
|
drafts: [javaMail({ betreff: '', text: '', empfaenger: [] })],
|
||||||
|
addressBook: [['Muster', 'Anna', 'Weg 1', 12, 20095, 'Hamburg', 'anna@schule.de', '040']],
|
||||||
|
// /konten.txt as Java's laden() reads it.
|
||||||
|
fileAccount: {
|
||||||
|
benutzername: 'anna',
|
||||||
|
passwort: 'geheim',
|
||||||
|
nachname: 'Muster',
|
||||||
|
vorname: 'Anna',
|
||||||
|
emailAdresse: 'anna@schule.de',
|
||||||
|
pop3server: '10.0.0.1',
|
||||||
|
pop3port: '110',
|
||||||
|
smtpserver: '10.0.0.1',
|
||||||
|
smtpport: '25',
|
||||||
|
nachrichten: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'filius.software.email.EmailServer',
|
||||||
|
class: 'filius.software.email.EmailServer',
|
||||||
|
aktiv: true,
|
||||||
|
mailDomain: 'schule.de',
|
||||||
|
beanAccounts: [serverAccount([javaMail()])],
|
||||||
|
// /mailserver/konten.txt as Java's kontenLaden() reads it: the subject with a space, a message's flags as new.
|
||||||
|
fileAccounts: [serverAccount([javaMail({ betreff: 'Hallo Welt', neu: true, delete: false })])],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'filius.software.firewall.Firewall',
|
key: 'filius.software.firewall.Firewall',
|
||||||
class: 'filius.software.firewall.Firewall',
|
class: 'filius.software.firewall.Firewall',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user