feat(fls): write the e-mail programs of a .fls, loaded by Java as the original

topologyWriter.ts writes the e-mail server's domain and bean accounts with
their mailboxes, and the e-mail client's three folders and address book.
The client's account is no bean (FILIUS 2.14 has no kontoListe property):
it lives in /konten.txt and is written with the files.

A Java error is not copied: older files keep recipients as strings, which
Java puts into its typed lists unchecked, and its e-mail window fails on
them. The writer writes the AddressEntry Java's own AddressEntry(String)
makes of each; a null recipient is left out. The Java oracle now reports
that reading next to each such string, and our addressEntry matches it on
all 9 in the corpus. The test names the departure (RECIPIENTS_AS_STRINGS)
and checks that it touches man-in-the-middle.fls alone.

Checked by Java against Java: every corpus file, read and written again,
loads with the same e-mail programs as the original, up to that departure.
The synthetic topology adds an e-mail server and client with an account, a
mailbox, three folders, an address book and recipients of every form. 15
mutation probes, each loaded by Java, were all caught. The writer now
writes everything the reader reads of Java's view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 08:44:34 +00:00
co-authored by Claude Opus 5.5
parent ccec734143
commit 34d3788cd2
9 changed files with 1083 additions and 58 deletions
+140 -11
View File
@@ -1,8 +1,9 @@
// Every corpus file, read and written again: Java FILIUS loads the written
// file (tools/java-oracle, written.java.json) to the same topology as the
// original (corpus.java.json) — devices, names, positions, interfaces,
// system software, files and applications (without e-mail accounts,
// messages and address books), cables and documentation. Java against Java: no mapping of this package stands
// system software, files, applications with their e-mail accounts,
// messages and address books, cables and documentation — up to one Java
// error the writer does not copy (RECIPIENTS_AS_STRINGS). Java against Java: no mapping of this package stands
// between the two.
//
// UPDATE_WRITTEN=1 writes the files to test/written/ (not checked in); the
@@ -15,6 +16,7 @@ import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { decodeJavaBeans, readFlsArchive, readTopology, writeFlsArchive } from '../src/index.js';
import type { JavaEmail } from '../src/javaEmail.js';
import { writeTopologyXml, type TopologyToWrite } from '../src/topologyWriter.js';
const here = dirname(fileURLToPath(import.meta.url));
@@ -44,13 +46,26 @@ const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), '
const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record<string, JavaFile>;
/**
* The system software without what a later layer writes: the e-mail
* programs' domain, accounts, messages and address book.
* The Java error the writer does not copy: older files keep e-mail
* recipients as strings, which Java puts into its typed lists unchecked (its
* e-mail window then fails on them). The writer writes the AddressEntry
* Java's own AddressEntry(String) makes of each — the oracle reports it
* next to the string — and leaves a null recipient out.
*/
function systemOf(system: Record<string, unknown>) {
if (system.applications === undefined) return system;
const applications = (system.applications as Record<string, unknown>[]).map(({ mailDomain: _domain, beanAccounts: _accounts, received: _received, sent: _sent, drafts: _drafts, addressBook: _book, ...rest }) => rest);
return { ...system, applications };
const RECIPIENTS_AS_STRINGS = ['java-examples/man-in-the-middle.fls'];
/** Java's view of the original as the writer means it: 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;
}
/**
@@ -67,7 +82,7 @@ function topologyOf(view: JavaFile) {
useIpAsName: node.useIpAsName,
useMacAsName: node.useMacAsName,
ports: node.ports,
system: systemOf(node.system),
system: node.system,
})),
cables: view.cables,
documentation: view.documentation,
@@ -88,11 +103,53 @@ 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(
createHash('sha256').update(archive).digest('hex'),
);
expect(topologyOf(java!)).toEqual(topologyOf(original[name]!));
expect(topologyOf(java!)).toEqual(topologyOf(withRecipientsRead(original[name]!) as JavaFile));
});
}
it('departs from Java only where it keeps recipients as strings', () => {
const changed = flsFiles(corpusRoot)
.map((file) => relative(corpusRoot, file))
.filter((name) => JSON.stringify(withRecipientsRead(original[name])) !== JSON.stringify(original[name]));
expect(changed).toEqual(RECIPIENTS_AS_STRINGS);
});
});
/**
* An e-mail with a sender without address, recipients as entries, as the
* string an older file keeps (read as AddressEntry(String) reads it) and
* null (left out), a text with CR, and a date.
*/
const SYNTHETIC_MAIL: JavaEmail = {
sender: { name: 'Anna', mailAddress: null },
to: [{ name: null, mailAddress: 'ben@schule.de' }, 'Carla <carla@schule.de>', null],
cc: [{ name: 'Dora', mailAddress: 'dora@schule.de' }],
bcc: ['eva@schule.de'],
subject: 'Hallo',
text: 'Zeile 1\r\nZeile 2',
dateReceived: '1727690000000',
isNew: true,
isDeleted: true,
isSent: false,
};
/** SYNTHETIC_MAIL as the oracle describes it after Java loaded it. */
function javaMail(changes: Record<string, unknown> = {}) {
return {
absender: ['Anna', null],
empfaenger: [[null, 'ben@schule.de'], ['Carla', 'carla@schule.de']],
cc: [['Dora', 'dora@schule.de']],
bcc: [[null, 'eva@schule.de']],
betreff: 'Hallo',
text: 'Zeile 1\r\nZeile 2',
dateReceived: '1727690000000',
neu: true,
delete: true,
versendet: false,
...changes,
};
}
/** Text a file's content may hold: markup, CR and CRLF, a tab, beyond the BMP. */
const FILE_CONTENT = '<p>a & b</p>\r\nline\rend\ttab Grüße 😀\n';
@@ -178,6 +235,45 @@ const SYNTHETIC: TopologyToWrite = {
{ key: 'filius.software.lokal.FileExplorer', javaClass: 'filius.software.lokal.FileExplorer' },
{ key: 'filius.software.lokal.ImageViewer', javaClass: 'filius.software.lokal.ImageViewer' },
{ key: 'filius.software.www.WebBrowser', javaClass: 'filius.software.www.WebBrowser' },
{
key: 'filius.software.email.EmailServer',
javaClass: 'filius.software.email.EmailServer',
isActive: true,
mailDomain: 'schule.de',
beanAccounts: [
{
username: 'anna',
password: 'geheim',
lastName: 'Muster',
firstName: '',
emailAddress: 'anna@schule.de',
pop3Server: null,
pop3Port: null,
smtpServer: null,
smtpPort: null,
messages: [SYNTHETIC_MAIL],
},
],
},
{
key: 'filius.software.email.EmailAnwendung',
javaClass: 'filius.software.email.EmailAnwendung',
received: [SYNTHETIC_MAIL],
sent: [{ ...SYNTHETIC_MAIL, isSent: true, isNew: false }],
drafts: [{ ...SYNTHETIC_MAIL, subject: null, text: null, dateReceived: null, to: [] }],
addressBook: [
{
name: 'Muster',
firstName: 'Anna',
street: null,
houseNumber: 12,
postalCode: 20095,
city: 'Hamburg',
email: 'anna@schule.de',
phone: null,
},
],
},
],
},
},
@@ -341,8 +437,9 @@ describe('a topology beyond the corpus, as Java loads it', () => {
javaFile('untyped', null, null),
{ directory: 'webserver', children: [javaFile('index.html', 'html', '<p>own</p>')] },
{ directory: 'dns', children: [javaFile('hosts', null, 'a.de. A 3600 10.0.0.1\n')] },
// Made by the peer-to-peer application, whose directory the files lack.
// Made by the peer-to-peer application and the e-mail server, whose directories the files lack.
{ directory: 'peer2peer', children: [] },
{ directory: 'mailserver', children: [] },
],
},
// In the order of Java's key.
@@ -351,6 +448,38 @@ describe('a topology 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: 5353, recursiveResolutionEnabled: true },
{
key: 'filius.software.email.EmailAnwendung',
class: 'filius.software.email.EmailAnwendung',
received: [javaMail()],
sent: [javaMail({ versendet: true, neu: false })],
drafts: [javaMail({ betreff: null, text: null, dateReceived: null, empfaenger: [] })],
addressBook: [['Muster', 'Anna', null, 12, 20095, 'Hamburg', 'anna@schule.de', null]],
// No /konten.txt.
fileAccount: null,
},
{
key: 'filius.software.email.EmailServer',
class: 'filius.software.email.EmailServer',
aktiv: true,
mailDomain: 'schule.de',
beanAccounts: [
{
benutzername: 'anna',
passwort: 'geheim',
nachname: 'Muster',
vorname: '',
emailAdresse: 'anna@schule.de',
pop3server: null,
pop3port: null,
smtpserver: null,
smtpport: null,
nachrichten: [javaMail()],
},
],
// No /mailserver/konten.txt.
fileAccounts: null,
},
{
key: 'filius.software.firewall.Firewall',
class: 'filius.software.firewall.Firewall',