From 956443726b7a8ef4467b272292904a493223d2b9 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 07:40:14 +0000 Subject: [PATCH] test(fls): e-mail account files against Swift's reader The Swift oracle reads the 43 account files Java read (--email-files, with TopologyRuntimeEmailStorage.decodeJavaServer and decodeJavaClient). The accounts the model takes match Swift's except where Swift checks names, hosts and addresses and fails the whole file on one it rejects, or reads the format otherwise (the first client line instead of the last, exactly nine fields, a required messages field). Every departure is named with its reason; Java's reading is right in each, none is a Java error Swift fixes. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 1 + packages/fls/test/email-client.swift.json | 129 ++++ packages/fls/test/email-server.swift.json | 584 +++++++++++++++++++ packages/fls/test/importEmail.test.ts | 157 +++++ tools/swift-oracle/README.md | 4 + tools/swift-oracle/Sources/Oracle/main.swift | 33 +- tools/swift-oracle/run.sh | 4 +- 7 files changed, 910 insertions(+), 2 deletions(-) create mode 100644 packages/fls/test/email-client.swift.json create mode 100644 packages/fls/test/email-server.swift.json create mode 100644 packages/fls/test/importEmail.test.ts diff --git a/CHANGES.md b/CHANGES.md index 9c467b6..55eba05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java | | `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` im Format von Javas Lesern, ohne ihre Fehler (leere Felder zählen, eine kurze Zeile fällt allein weg, Nachrichten mit leerem Text oder Betreff bleiben, `;` im Betreff bleibt, CRLF, beim Client die letzte vollständige Zeile); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft (13 benannte Java-Fehler) | | `src/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 | | `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 | | `test/javaEmail.test.ts`, `test/email-*.cases.txt`, `test/email-*.java.json`, `test/javaEmailNames.ts`, `test/javaFileSystem.test.ts` | — | neu: Kontodateien von Server und Client, Ergebnis aus Java; Dateifund | | `test/dnsHosts.test.ts`, `test/dns-hosts.*` | — | neu: `/dns/hosts`-Fälle, wie Java bis auf 7 benannte Java-Fehler, das Modell wie Swift bis auf benannte Fälle mit Grund | diff --git a/packages/fls/test/email-client.swift.json b/packages/fls/test/email-client.swift.json new file mode 100644 index 0000000..48ea20d --- /dev/null +++ b/packages/fls/test/email-client.swift.json @@ -0,0 +1,129 @@ +[ + [ + "mail.gmx.de;mail.gmx.de;110;25;herbert;herbert1234;Schmidt;Herbert;herbert@gmx.de", + { + "email" : "herbert@gmx.de", + "name" : "Herbert Schmidt", + "password" : "herbert1234", + "pop3Host" : "mail.gmx.de", + "pop3Port" : 110, + "smtpHost" : "mail.gmx.de", + "smtpPort" : 25, + "username" : "herbert" + } + ], + [ + "mail.gmx.de;mail.gmx.de;110;25;herbert;herbert1234;Schmidt;Herbert;herbert@gmx.de\\u000A", + { + "email" : "herbert@gmx.de", + "name" : "Herbert Schmidt", + "password" : "herbert1234", + "pop3Host" : "mail.gmx.de", + "pop3Port" : 110, + "smtpHost" : "mail.gmx.de", + "smtpPort" : 25, + "username" : "herbert" + } + ], + [ + "a;a;110;25;first;p;L;F;first@x\\u000Ab;b;110;25;second;p;L;F;second@x\\u000A", + { + "email" : "first@x", + "name" : "F L", + "password" : "p", + "pop3Host" : "a", + "pop3Port" : 110, + "smtpHost" : "a", + "smtpPort" : 25, + "username" : "first" + } + ], + [ + "a;a;110;25;first;p;L;F;first@x\\u000Abroken\\u000A", + { + "email" : "first@x", + "name" : "F L", + "password" : "p", + "pop3Host" : "a", + "pop3Port" : 110, + "smtpHost" : "a", + "smtpPort" : 25, + "username" : "first" + } + ], + [ + "", + "!invalidAccount(\"missing client account\")" + ], + [ + "\\u000A", + "!invalidAccount(\"missing client account\")" + ], + [ + "\\u000A\\u000A\\u000A", + "!invalidAccount(\"missing client account\")" + ], + [ + "a;b;110;25;u;p;L;F;\\u000A", + "!invalidAddress(\"\")" + ], + [ + "a;b;110;25;u;p;L;F;u@x;extra\\u000A", + "!invalidAccount(\"malformed client account\")" + ], + [ + "a;b;110;25;u;p;L;F;u@x\\u000D\\u000A", + { + "email" : "u@x", + "name" : "F L", + "password" : "p", + "pop3Host" : "a", + "pop3Port" : 110, + "smtpHost" : "b", + "smtpPort" : 25, + "username" : "u" + } + ], + [ + "a;;110;25;u;p;;;u@x\\u000A", + "!invalidHost(\"\")" + ], + [ + "a;b;110;25;u;p;L;F;u@x\\u000A\\u000A\\u000A", + { + "email" : "u@x", + "name" : "F L", + "password" : "p", + "pop3Host" : "a", + "pop3Port" : 110, + "smtpHost" : "b", + "smtpPort" : 25, + "username" : "u" + } + ], + [ + "\\u000Aa;b;110;25;u;p;L;F;u@x", + { + "email" : "u@x", + "name" : "F L", + "password" : "p", + "pop3Host" : "a", + "pop3Port" : 110, + "smtpHost" : "b", + "smtpPort" : 25, + "username" : "u" + } + ], + [ + " a ; b ;x;y; u ; p ; L ; F ; u@x \\u000A", + "!invalidAccount(\"malformed client account\")" + ], + [ + ";;;;;;;;", + "!invalidAccount(\"malformed client account\")" + ], + [ + ";;;;;;;;x", + "!invalidAccount(\"malformed client account\")" + ] +] diff --git a/packages/fls/test/email-server.swift.json b/packages/fls/test/email-server.swift.json new file mode 100644 index 0000000..6212754 --- /dev/null +++ b/packages/fls/test/email-server.swift.json @@ -0,0 +1,584 @@ +[ + [ + "bob;filius.de;bob;Nachname;Vorname;\\u000A", + [ + { + "mailbox" : [ + + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;\\u000Aalice;filius.de;alice;A;B;\\u000A", + [ + { + "mailbox" : [ + + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + }, + { + "mailbox" : [ + + ], + "name" : "B A", + "password" : "alice", + "username" : "alice" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname", + "!invalidAccount(\"malformed server account\")" + ], + [ + "bob;;bob;Nachname;Vorname;\\u000A", + "!invalidDomain(\"\")" + ], + [ + "bob;filius.de;bob;Nachname\\u000A", + "!invalidAccount(\"malformed server account\")" + ], + [ + "bob;filius.de;bob;Nachname;Vorname;\\u000Abroken\\u000A", + "!invalidAccount(\"malformed server account\")" + ], + [ + " bob ;filius.de; bob ;Nachname;Vorname;\\u000A", + "!invalidAccount(\" bob \")" + ], + [ + "bob;filius.de;bob;Nachname;Vorname\\u000D\\u000A", + "!invalidAccount(\"malformed server account\")" + ], + [ + "", + [ + + ] + ], + [ + "\\u000A\\u000A", + [ + + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Hi BoB!\n", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + "Alice", + "alice@senior.de" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi Bob!", + "to" : [ + [ + null, + "bob@filius.de" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Hi BoB!\n", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + "Alice", + "alice@senior.de" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi Bob!", + "to" : [ + [ + null, + "bob@filius.de" + ] + ] + }, + { + "bcc" : [ + + ], + "body" : "Hi BoB!\n", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + "Alice", + "alice@senior.de" + ], + "id" : 2, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 2, + "subject" : "Hi Bob!", + "to" : [ + [ + null, + "bob@filius.de" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;Alice $bob@filius.de$$$$Hi$Text\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Text", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + "Alice", + "alice@senior.de" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "bob@filius.de" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi$\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + "Alice", + "alice@senior.de" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "bob@filius.de" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$$\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + "Alice", + "alice@senior.de" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "", + "to" : [ + [ + null, + "bob@filius.de" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi $5$Text\\u000A", + "!malformedMessage" + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$c@z$d@w$1234$Hi$Text\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + [ + null, + "d@w" + ] + ], + "body" : "Text", + "cc" : [ + [ + null, + "c@z" + ] + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1234, + "subject" : "Hi", + "to" : [ + [ + null, + "b@y" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y, ,c@z,$$$$Hi$Text\\u000A", + "!malformedMessage" + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Name $Bob , $$$$Hi$Text\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Text", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + "Bob", + "b@y" + ], + [ + null, + "c@z" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Only Name$b@y$$$$Hi$Text\\u000A", + "!malformedMessage" + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$l1||l2&&00124&&x&&00036&&y&&00059&&z&&00035&&w\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "l1\n\nl2|x$y;z#w", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "b@y" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$&&00124&&|\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "|\n", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "b@y" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text;extra\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Text;extra", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "b@y" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;##a@x$b@y$$$$Hi$Text##\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Text", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "b@y" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text\\u000D\\u000A", + [ + { + "mailbox" : [ + { + "bcc" : [ + + ], + "body" : "Text", + "cc" : [ + + ], + "deliveryIdentity" : null, + "from" : [ + null, + "a@x" + ], + "id" : 1, + "isMarkedForDeletion" : false, + "isNew" : true, + "isSent" : false, + "receivedAtMilliseconds" : 1, + "subject" : "Hi", + "to" : [ + [ + null, + "b@y" + ] + ] + } + ], + "name" : "Vorname Nachname", + "password" : "bob", + "username" : "bob" + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#\\u001Fa@x $b@y$$$$Hi$Text\\u000A", + "!malformedMessage" + ] +] diff --git a/packages/fls/test/importEmail.test.ts b/packages/fls/test/importEmail.test.ts new file mode 100644 index 0000000..6762daf --- /dev/null +++ b/packages/fls/test/importEmail.test.ts @@ -0,0 +1,157 @@ +// The e-mail account files as the model takes them, against Swift's reader +// (TopologyRuntimeEmailStorage.decodeJavaServer/decodeJavaClient, +// email-*.swift.json, tools/swift-oracle --email-files) on the cases Java +// read too (javaEmail.test.ts). Swift checks names, hosts and addresses and +// fails the whole file on one it rejects, where Java reads it; each such +// case is named with its reason and must really depart. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import type { EmailAddress, EmailMessage } from '@mdg-filius/core'; +import { emailClientConfiguration, emailServerConfiguration } from '../src/importEmail.js'; +import { readEmailClientFile, readEmailServerFile } from '../src/javaEmail.js'; +import type { FlsApplication } from '../src/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); + +/** The cases file's escapes: a backslash, `u` and four hex digits for one UTF-16 unit. */ +function unescape(line: string): string { + return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))); +} + +const fullName = (person: { firstName: string; lastName: string }) => + [person.firstName, person.lastName].filter((part) => part !== '').join(' '); + +function swiftMessage(message: EmailMessage) { + const address = (entry: EmailAddress) => [entry.name, entry.mailAddress]; + return { + id: message.id, + from: address(message.from), + to: message.to.map(address), + cc: message.cc.map(address), + bcc: message.bcc.map(address), + subject: message.subject, + body: message.body, + deliveryIdentity: null, + receivedAtMilliseconds: message.receivedAtMilliseconds ?? null, + isNew: message.isNew, + isMarkedForDeletion: message.isMarkedForDeletion, + isSent: message.isSent, + }; +} + +function serverAccounts(content: string) { + const application = { fileAccounts: readEmailServerFile(content, () => {}), mailDomain: 'filius.de' } as FlsApplication; + return emailServerConfiguration(application, () => {}).accounts.map((account) => ({ + username: account.username, + password: account.password, + name: fullName(account), + mailbox: account.mailbox.map(swiftMessage), + })); +} + +function clientAccount(content: string) { + const application = { fileAccount: readEmailClientFile(content, () => {}) } as FlsApplication; + const account = emailClientConfiguration(application, () => {}).account; + if (account === null) return null; + const { firstName, lastName, ...fields } = account; + return { ...fields, name: fullName({ firstName, lastName }) }; +} + +type Departures = { reason: string; lines: string[] }[]; + +/** + * Swift's result where it means the same as ours: Swift numbers a mailbox's + * messages as their received time (a Swift error named in + * importProject.test.ts), and "no account" is an error to Swift. + */ +function comparable(expected: unknown): unknown { + if (expected === '!invalidAccount("missing client account")') return null; + if (!Array.isArray(expected)) return expected; + return expected.map((account: { mailbox: { id: number; receivedAtMilliseconds: number | null }[] }) => ({ + ...account, + mailbox: account.mailbox.map((message) => + message.receivedAtMilliseconds === message.id ? { ...message, receivedAtMilliseconds: null } : message, + ), + })); +} + +function compareWithSwift(kind: 'server' | 'client', departures: Departures, read: (content: string) => unknown) { + const cases = JSON.parse(readFileSync(join(here, `email-${kind}.swift.json`), 'utf8')) as [string, unknown][]; + const lines = readFileSync(join(here, `email-${kind}.cases.txt`), 'ascii').split('\n').slice(0, -1); + const byLine = new Map(departures.flatMap(({ reason, lines: named }) => named.map((line) => [line, reason] as const))); + it('covers every line of the cases file and names only its lines', () => { + expect(cases.map(([line]) => line)).toEqual(lines); + expect([...byLine.keys()].filter((line) => !lines.includes(line))).toEqual([]); + }); + for (const [line, expected] of cases) { + const reason = byLine.get(line); + it(reason === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (departs: ${reason})`, () => { + if (reason === undefined) expect(read(unescape(line))).toEqual(comparable(expected)); + else expect(read(unescape(line))).not.toEqual(comparable(expected)); + }); + } +} + +const SERVER_DEPARTURES: Departures = [ + { + reason: 'a line without the messages field is an account, as in Java; Swift wants the sixth field and fails the file', + lines: [String.raw`bob;filius.de;bob;Nachname;Vorname`, String.raw`bob;filius.de;bob;Nachname;Vorname\u000D\u000A`], + }, + { + reason: "the file's domain column is not used (Java takes the server's); Swift fails the file on an empty one", + lines: [String.raw`bob;;bob;Nachname;Vorname;\u000A`], + }, + { + reason: 'a short line alone is left out; Swift fails the whole file', + lines: [String.raw`bob;filius.de;bob;Nachname\u000A`, String.raw`bob;filius.de;bob;Nachname;Vorname;\u000Abroken\u000A`], + }, + { + reason: 'a user name stays as Java reads it; Swift rejects one with spaces and fails the file', + lines: [String.raw` bob ;filius.de; bob ;Nachname;Vorname;\u000A`], + }, + { + reason: 'a message without seven fields alone is left out; Swift fails the whole file', + lines: [String.raw`bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi $5$Text\u000A`], + }, + { + reason: "addresses as Java reads them (blank entries skipped, a name alone, Java's trim); Swift fails the file", + lines: [ + String.raw`bob;filius.de;bob;Nachname;Vorname;#a@x$b@y, ,c@z,$$$$Hi$Text\u000A`, + String.raw`bob;filius.de;bob;Nachname;Vorname;#Name { + compareWithSwift('server', SERVER_DEPARTURES, serverAccounts); +}); + +describe('the client account file against Swift', () => { + compareWithSwift('client', CLIENT_DEPARTURES, clientAccount); +}); diff --git a/tools/swift-oracle/README.md b/tools/swift-oracle/README.md index 06998a3..40823b6 100644 --- a/tools/swift-oracle/README.md +++ b/tools/swift-oracle/README.md @@ -38,6 +38,10 @@ Sortierung, Base64 — und die ganzen Mengen `whitespacesAndNewlines` und unter Linux; ihre Steuerzeichen-Tabelle hat in Ebene 14 einen Fehler, den der Port nicht übernimmt. +`--email-files server|client` liest die Kontodateien +(`packages/fls/test/email-*.cases.txt`) mit Swifts +`TopologyRuntimeEmailStorage.decodeJavaServer`/`decodeJavaClient`. + `--dns-hosts` liest dieselben `/dns/hosts`-Fälle wie das Java-Orakel (`packages/fls/test/dns-hosts.cases.txt`) mit Swifts `TopologyDNSHostsFileCodec`; unter `state` stehen die DNS-Einträge jedes diff --git a/tools/swift-oracle/Sources/Oracle/main.swift b/tools/swift-oracle/Sources/Oracle/main.swift index ebd5577..38a4458 100644 --- a/tools/swift-oracle/Sources/Oracle/main.swift +++ b/tools/swift-oracle/Sources/Oracle/main.swift @@ -666,7 +666,7 @@ func dnsRecordJSON(_ record: TopologyDNSResourceRecord) -> [Any] { [record.name.rawValue, record.type.rawValue, Int(record.ttlSeconds), record.target] } -/// One case per line: a whole hosts file, ASCII with Java-style unicode +/// One case per line: a whole file (hosts, e-mail accounts), ASCII with Java-style unicode /// escapes (backslash, u, four hex digits) — the file the Java oracle reads. func readDNSHostsCases(_ url: URL) throws -> [(line: String, text: String)] { let lines = try String(contentsOf: url, encoding: .ascii).components(separatedBy: "\n") @@ -722,6 +722,37 @@ if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--dns-hosts" { exit(0) } +if CommandLine.arguments.count == 5, CommandLine.arguments[1] == "--email-files" { + let server = CommandLine.arguments[2] == "server" + let cases = try readDNSHostsCases(URL(fileURLWithPath: CommandLine.arguments[3])) + let output: [[Any]] = cases.map { item in + do { + if server { + let configuration = try TopologyRuntimeEmailStorage.decodeJavaServer(item.text) + return [item.line, configuration.accounts.map { account in + [ + "username": account.username, "password": account.password, "name": account.name, + "mailbox": account.mailbox.map(emailMessageJSON), + ] as JSON + }] + } + let client = try TopologyRuntimeEmailStorage.decodeJavaClient(item.text) + return [item.line, [ + "pop3Host": client.pop3Host, "pop3Port": client.pop3Port, + "smtpHost": client.smtpHost, "smtpPort": client.smtpPort, + "username": client.username, "password": client.password, + "name": client.name, "email": client.email, + ] as JSON] + } catch { + return [item.line, "!\(error)"] + } + } + let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys]) + try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[4])) + print("email \(CommandLine.arguments[2]) files: \(cases.count) cases") + exit(0) +} + if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--corpus" { let summary = try readCorpus(root: URL(fileURLWithPath: CommandLine.arguments[2])) let data = try JSONSerialization.data(withJSONObject: summary, options: [.prettyPrinted, .sortedKeys]) diff --git a/tools/swift-oracle/run.sh b/tools/swift-oracle/run.sh index 5d5b300..96250d5 100755 --- a/tools/swift-oracle/run.sh +++ b/tools/swift-oracle/run.sh @@ -15,4 +15,6 @@ docker run --rm -v "$root:/work" -w /work/tools/swift-oracle swift:6.1-jammy sh .build/debug/Oracle --vfs /work/packages/core/test/vfs.cases.json /work/packages/core/test/vfs.swift.json .build/debug/Oracle --strings /work/packages/core/test/strings.cases.json /work/packages/core/test/strings.swift.json .build/debug/Oracle --path-folding /work/packages/fls/test/path-folding.cases.json /work/packages/fls/test/path-folding.swift.json - .build/debug/Oracle --dns-hosts /work/packages/fls/test/dns-hosts.cases.txt /work/packages/fls/test/dns-hosts.swift.json' + .build/debug/Oracle --dns-hosts /work/packages/fls/test/dns-hosts.cases.txt /work/packages/fls/test/dns-hosts.swift.json + .build/debug/Oracle --email-files server /work/packages/fls/test/email-server.cases.txt /work/packages/fls/test/email-server.swift.json + .build/debug/Oracle --email-files client /work/packages/fls/test/email-client.cases.txt /work/packages/fls/test/email-client.swift.json'