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:
2026-09-30 09:25:22 +00:00
co-authored by Claude Opus 5.5
parent 95daa64276
commit 8ab8850b20
4 changed files with 1264 additions and 88 deletions
+185 -15
View File
@@ -30,8 +30,20 @@
// 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
// /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
// step. System software, files and programs
// Java's does.
//
// 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.
//
// 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
// (a web server's www.conf) are not in the model;
// - 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
// Java FILIUS to the topology of the original up to those departures.
@@ -71,6 +86,11 @@ import {
type DhcpServerConfiguration,
type DnsServerConfiguration,
type EchoServerConfiguration,
type EmailAddress,
type EmailClientConfiguration,
type EmailContact,
type EmailMessage,
type EmailServerConfiguration,
type GnutellaConfiguration,
type InstallableProgram,
type SimpleClientConfiguration,
@@ -91,6 +111,7 @@ import {
type TopologyNode,
type VirtualFileSystem,
} 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 type { FlsApplicationClass, FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
@@ -118,6 +139,8 @@ export interface ProjectToExport {
readonly dnsServerConfigurations: ReadonlyMap<Id, DnsServerConfiguration>;
readonly echoServerConfigurations: ReadonlyMap<Id, EchoServerConfiguration>;
readonly simpleClientConfigurations: ReadonlyMap<Id, SimpleClientConfiguration>;
readonly emailClientConfigurations: ReadonlyMap<Id, EmailClientConfiguration>;
readonly emailServerConfigurations: ReadonlyMap<Id, EmailServerConfiguration>;
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 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[] = [];
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 interfaces = exportInterfaces(node, project);
return {
@@ -184,7 +207,7 @@ function exportNode(node: TopologyNode, project: ProjectToExport, servingModems:
useMacAsName: showsMac(node.hostLabelPolicy),
interfaces,
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;
}
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;
switch (node.kind) {
case 'networkSwitch': {
@@ -246,12 +271,12 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
case 'notebook': {
const ssid = project.hostWirelessConfigurations.get(id)?.ssid ?? '';
const files = project.virtualFileSystems.get(id);
return {
...system,
ssid: ssid === '' ? null : ssid,
fileSystem: { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') },
applications: exportPrograms(id, project),
};
const root: FlsFileTree = { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') };
const client = project.installedPrograms.get(id)?.has('emailClient') === true ? project.emailClientConfigurations.get(id) : undefined;
const server = project.installedPrograms.get(id)?.has('emailServer') === true ? project.emailServerConfigurations.get(id) : undefined;
if (client?.account != null) putFile(root, EMAIL_CLIENT_FILE, 'text/txt', clientAccountFile(client, warn));
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':
return {
@@ -310,14 +335,159 @@ function exportPrograms(id: Id, project: ProjectToExport): ApplicationToWrite[]
return { ...application, maxPeers: project.gnutellaConfigurations.get(id)?.maximumKnownPeers ?? 3 };
case 'firewall':
return { ...application, firewall: exportFirewall(project.firewallConfigurationsByNodeId.get(id) ?? JAVA_PERSONAL_FIREWALL_DEFAULTS) };
case 'emailServer':
return { ...application, isActive };
case 'emailServer': {
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:
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. */
const TEXT_EXTENSIONS = new Set(['txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
+221 -19
View File
@@ -18,6 +18,7 @@ import {
firewallConfiguration,
firewallRule,
stableGeneratedMacAddress,
type EmailMessage,
type Id,
type InstallableProgram,
type TopologyNode,
@@ -32,6 +33,7 @@ import {
readFlsArchive,
readTopology,
writeFlsArchive,
type FlsFileTree,
type ProjectToExport,
} from '../src/index.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 exported = JSON.parse(readFileSync(exportedPath, 'utf8')) as Record<string, JavaFile>;
/**
* The system software without what a later step exports — the e-mail
* 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,
),
};
/** The system software with its files in any order. */
function systemOf(settings: Record<string, unknown>) {
return settings.fileSystem === undefined || settings.fileSystem === null
? settings
: { ...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
* (txt, text/txt, html, none);
* - 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 = {
generatedMac(view: JavaFile, macs: (string | undefined)[][]): JavaFile {
@@ -174,6 +172,38 @@ const DEPARTURES = {
: { ...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 {
const items = (view.documentation as Record<string, unknown>[]).map((item) => {
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 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 {
return { ...view, nodes: view.nodes.map((node, index) => ({ ...node, system: system(node, index) })) };
}
@@ -249,6 +315,9 @@ const departed = {
routerFiles: new Set<string>(),
fileTypes: 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', () => {
@@ -278,6 +347,8 @@ describe('the editor model exported, as Java loads it', () => {
const source = original[name]!;
const nodeIds = project.graph.nodes.map((node) => node.id);
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.
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][] = [
@@ -286,6 +357,9 @@ describe('the editor model exported, as Java loads it', () => {
['modemRoles', (view) => DEPARTURES.modemRoles(view, pairs)],
['routerFiles', (view) => DEPARTURES.routerFiles(view, added)],
['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)],
];
let expected = source;
@@ -335,12 +409,58 @@ function emptyProject(): ProjectToExport {
dnsServerConfigurations: new Map(),
echoServerConfigurations: new Map(),
simpleClientConfigurations: new Map(),
emailClientConfigurations: new Map(),
emailServerConfigurations: new Map(),
documentationItems: [],
};
}
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. */
function bytesOf(length: number): Uint8Array {
return Uint8Array.from({ length }, (_, index) => (index * 37 + 11) % 256);
@@ -484,11 +604,49 @@ const SYNTHETIC: ProjectToExport = {
'fileExplorer',
'imageViewer',
'webBrowser',
'emailServer',
'emailClient',
]),
],
['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 }]]),
dnsServerConfigurations: new Map([['pc', { recursiveResolutionEnabled: true, records: [] }]]),
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(
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 modem = (name: string, x: number, mode: number, port: number) => ({
class: 'Modem',
@@ -582,6 +752,8 @@ describe('a model beyond the corpus, as Java loads it', () => {
javaFile('roh', '', javaBase64(bytesOf(3))),
{ directory: 'webserver', children: [javaFile('index.html', 'html', '<p>own</p>')] },
{ 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.
@@ -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.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.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',
class: 'filius.software.firewall.Firewall',
File diff suppressed because it is too large Load Diff