fix(fls): follow Java's formats, not Java's errors

Java stays the reference for the .fls format, but its errors are no longer
copied. The Java oracle still measures Java as it is; each deliberate
departure is named in the tests with its reason (JAVA_BUGS) and must really
depart. The corpus - what Java itself writes - still reads exactly as in
Java.

- /dns/hosts: a label may start with a digit, holds no backslash, and the
  last one is not all digits (RFC 1123, RFC 3696); A, MX and NS are read in
  any case (RFC 4343); a negative TTL is invalid, so 3600 (RFC 2181). Also
  for the e-mail server's domain.
- /mailserver/konten.txt: empty fields count (Java's StringTokenizer skips
  them and shifts the rest, so an empty password breaks the account); a
  short line alone is left out, with a warning (Java throws for the whole
  file); a message keeps an empty text or subject (Java's split loses it);
  a ";" in an unescaped subject stays in the message; CRLF lines.
- /konten.txt: the last complete line is the account, also with an empty
  address (Java takes the last line and loses the account); CRLF lines.
- Static DHCP assignments: a broken entry alone is left out (Java loses
  every entry after it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 07:20:52 +00:00
co-authored by Claude Opus 5.5
parent ec5523d7c7
commit 8f9930f1ff
10 changed files with 319 additions and 160 deletions
+5 -5
View File
@@ -39,17 +39,17 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) |
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ genau A/MX/NS, Name nicht leer); an 76 Fällen mit Java und Swift gemessen |
| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen |
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
| `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java |
| `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt |
| `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` wie Javas Leser (StringTokenizer überspringt leere Felder, `split` verliert leere Felder am Ende); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft |
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase`, `StringTokenizer` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
| `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/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, Javas Sicht wie Java, das Modell wie Swift bis auf 33 benannte Fälle in zwölf Gründen |
| `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 |
| `test/zip.test.ts` | `TopologyProjectPersistenceTests.swift` (Archiv-Ablehnungstests, `makeMinimalZIP`, `makePayloadZIP`) | übertragen, ergänzt um DEFLATE-Grenzfälle |
| `test/corpus.test.ts` | — | neu: 23 Korpus-Dateien, gegengeprüft mit `fflate.unzipSync` |
| `test/pathFolding.test.ts`, `test/path-folding.*.json` | — | neu: welche Pfade dieselbe Datei meinen, Ergebnis aus dem Swift-Kern |
+50 -26
View File
@@ -1,14 +1,18 @@
// Java's DNS server keeps its records in the text file /dns/hosts and reads
// them on every query (DNSServer.leseRecordListe, ResourceRecord(String)).
// Measured on test/dns-hosts.cases.txt against Java (dns-hosts.java.json)
// and Swift (dns-hosts.swift.json); where the two differ, Java decides:
// and Swift (dns-hosts.swift.json). Java's format decides where the two
// differ:
// - fields are split at single spaces only: a tab or NBSP joins two fields;
// - there are no comments, no "IN" and no zone-file order;
// - a TTL that is not a Java int silently becomes 3600;
// - a name Java does not accept becomes the empty name, and the record
// still counts - it just never answers, like any record whose type is
// not exactly A, MX or NS (lower-case "a" included);
// - a TTL that is not a number silently becomes 3600;
// - a name that is not a domain name becomes the empty name, and the record
// still counts - it just never answers, like one of another type;
// - duplicates stay.
// Java's errors are not copied (each named in dnsHosts.test.ts): a label may
// start with a digit and holds no backslash (RFC 1123 - Java's pattern has
// both wrong; see isDomainName), the type A, MX or NS is read in any case
// (RFC 4343), and a negative TTL is invalid (RFC 2181).
import type { DnsResourceRecord } from '@mdg-filius/core';
import { javaFileContent } from './javaFileSystem.js';
@@ -16,8 +20,8 @@ import { javaIpCheck, javaParseInt } from './javaIp.js';
import { javaTrim } from './javaString.js';
import type { FlsFileTree } from './systemSoftware.js';
/** A record as Java's ResourceRecord holds it (the getters). */
export interface JavaResourceRecord {
/** A record of /dns/hosts, as Java's ResourceRecord holds it (the getters). */
export interface HostsRecord {
readonly domainName: string;
readonly type: string;
readonly ttl: number;
@@ -26,10 +30,22 @@ export interface JavaResourceRecord {
export const DNS_HOSTS_PATH = '/dns/hosts';
// EingabenUeberpruefung.musterDomain, including its quirks: labels start with
// a letter, labels after the first may hold a backslash, and the empty
// string is a valid domain.
export const JAVA_DOMAIN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*)?\.?$/;
// Labels of letters, digits and inner hyphens, an optional final dot; the
// empty string counts too.
const DOMAIN_LABELS = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)?\.?$/;
/**
* A domain name as EingabenUeberpruefung.musterDomain means it (the empty
* string included). Java's pattern wants every label to start with a letter
* and lets a backslash into the labels after the first; instead a label may
* start with a digit (RFC 1123), but the last one must not be all digits
* (RFC 3696), so that no IPv4 address passes for a name.
*/
export function isDomainName(text: string): boolean {
if (!DOMAIN_LABELS.test(text)) return false;
const labels = text.split('.').filter((label) => label !== '');
return labels.length === 0 || !/^[0-9]+$/.test(labels[labels.length - 1]!);
}
// EingabenUeberpruefung.musterIpAdresse.
const JAVA_IP_ADDRESS =
/^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
@@ -39,31 +55,39 @@ export function javaHostsFileContent(root: FlsFileTree | undefined): string | un
return javaFileContent(root, DNS_HOSTS_PATH);
}
const ANSWERED_TYPES = new Set(['A', 'MX', 'NS']);
/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */
export function readJavaDnsHosts(content: string): JavaResourceRecord[] {
const records: JavaResourceRecord[] = [];
export function readDnsHosts(content: string): HostsRecord[] {
const records: HostsRecord[] = [];
for (const token of content.split('\n')) {
const line = javaTrim(token);
// line.split(" ", 5).length < 4: fewer than three single spaces.
if (line === '' || line.split(' ').length < 4) continue;
const record = javaResourceRecord(line);
if (isValidJavaRecord(record)) records.push(record);
const record = hostsRecord(line);
if (isValidRecord(record)) records.push(record);
}
return records;
}
/** `new ResourceRecord(line)`: up to four space-separated fields. */
function javaResourceRecord(line: string): JavaResourceRecord {
function hostsRecord(line: string): HostsRecord {
const fields = line.split(' ').filter((field) => field !== '').map(javaTrim);
let domainName = '';
let type = 'A';
let ttl = 3600;
let rdata = '';
if (fields[0] !== undefined && JAVA_DOMAIN.test(fields[0])) {
if (fields[0] !== undefined && isDomainName(fields[0])) {
domainName = fields[0].endsWith('.') ? fields[0] : `${fields[0]}.`;
}
if (fields[1] !== undefined) type = fields[1];
if (fields[2] !== undefined) ttl = javaParseInt(fields[2]) ?? ttl;
if (fields[1] !== undefined) {
const upper = fields[1].replace(/[a-z]/g, (letter) => letter.toUpperCase());
type = ANSWERED_TYPES.has(upper) ? upper : fields[1];
}
if (fields[2] !== undefined) {
const parsed = javaParseInt(fields[2]);
if (parsed !== undefined && parsed >= 0) ttl = parsed;
}
if (fields[3] !== undefined) {
// Java tests ".*\." (no line terminator before the dot); a target with
// one fails the domain check anyway, so endsWith says the same.
@@ -73,13 +97,13 @@ function javaResourceRecord(line: string): JavaResourceRecord {
return { domainName, type, ttl, rdata };
}
function isValidJavaRecord(record: JavaResourceRecord): boolean {
function isValidRecord(record: HostsRecord): boolean {
switch (record.type) {
case 'A':
return JAVA_DOMAIN.test(record.domainName) && JAVA_IP_ADDRESS.test(record.rdata);
return isDomainName(record.domainName) && JAVA_IP_ADDRESS.test(record.rdata);
case 'MX':
case 'NS':
return JAVA_DOMAIN.test(record.domainName) && JAVA_DOMAIN.test(record.rdata);
return isDomainName(record.domainName) && isDomainName(record.rdata);
default:
return true;
}
@@ -87,11 +111,11 @@ function isValidJavaRecord(record: JavaResourceRecord): boolean {
/**
* The records Java answers with, in the model's form: Java looks them up by
* type (exactly "A", "MX", "NS") and by name ignoring case, so a record with
* the empty name or another type never answers and is left out. The file
* itself stays as it is.
* type (A, MX, NS) and by name ignoring case, so a record with the empty
* name or another type never answers and is left out. The file itself stays
* as it is.
*/
export function dnsRecordsOf(records: readonly JavaResourceRecord[], warn: (message: string) => void): DnsResourceRecord[] {
export function dnsRecordsOf(records: readonly HostsRecord[], warn: (message: string) => void): DnsResourceRecord[] {
const answered: DnsResourceRecord[] = [];
for (const record of records) {
if (record.domainName === '') continue;
+2 -2
View File
@@ -91,7 +91,7 @@ import {
type TopologyNode,
type VirtualFileContent,
} from '@mdg-filius/core';
import { dnsRecordsOf, javaHostsFileContent, readJavaDnsHosts } from './dnsHosts.js';
import { dnsRecordsOf, javaHostsFileContent, readDnsHosts } from './dnsHosts.js';
import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
@@ -420,7 +420,7 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon
const primary = trimmedOrEmpty(source.interfaces[0]?.ip);
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
const hosts = javaHostsFileContent(source.system.fileSystem);
const records = dnsRecordsOf(readJavaDnsHosts(hosts ?? ''), (message) =>
const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), (message) =>
warnings.push(`node '${node.displayName}': ${message}`),
);
system.dnsServerConfigurations.set(
+73 -48
View File
@@ -7,10 +7,12 @@
// the bean list, and /konten.txt, the client's account, read (laden) when
// its window opens. The client's account is in no bean: its accessors
// (holeKontoListe, setzeKontoListe) are not bean properties.
// Checked against Java on the corpus (corpus.java.json, applications).
// Checked against Java on the corpus (corpus.java.json, applications) and on
// the account files of test/email-*.cases.txt, where Java's errors in reading
// them are not copied (see readEmailServerFile, readEmailClientFile).
import { isJavaObject, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js';
import { javaSplit, javaTokens, javaTrim } from './javaString.js';
import { javaSplit, javaTrim } from './javaString.js';
/** AddressEntry: a display name, an address, or both. */
export interface JavaAddressEntry {
@@ -63,11 +65,6 @@ export interface JavaContact {
readonly phone: string | null;
}
/** An exception Java throws while reading, by its simple class name. */
export interface JavaFailure {
readonly exception: string;
}
export const EMAIL_SERVER_FILE = '/mailserver/konten.txt';
export const EMAIL_CLIENT_FILE = '/konten.txt';
@@ -213,17 +210,25 @@ function addressEntries(raw: string): JavaAddressEntry[] {
/**
* `EmailServer.stringZuListeBenutzerkonten`: one account a line —
* user;domain;password;last name;first name;messages — split by a
* StringTokenizer, which skips empty fields; an account with fewer than five
* fields makes Java throw. Messages are "#"-separated, seven "$"-separated
* fields each: sender, to, cc, bcc, date, subject, text. Java's split drops
* trailing empty fields, so a message with an empty text is lost.
* user;domain;password;last name;first name;messages. Messages are
* "#"-separated, seven "$"-separated fields each: sender, to, cc, bcc, date,
* subject, text. Java's errors are not copied (each named in
* javaEmail.test.ts): its StringTokenizer skips empty fields, so an empty
* password shifts the rest; one line with fewer than five fields makes it
* throw for the whole file; its split drops trailing empty fields, so a
* message with an empty text or subject is lost; a CR of a CRLF line stays.
* Here fields count also when empty, a short line alone is left out, and a
* message needs its seven fields, empty or not.
*/
export function readJavaEmailServerFile(content: string): JavaEmailAccount[] | JavaFailure {
export function readEmailServerFile(content: string, warn: Warn): JavaEmailAccount[] {
const accounts: JavaEmailAccount[] = [];
for (const line of javaTokens(content, '\n')) {
const fields = javaTokens(line, ';');
if (fields.length < 5) return { exception: 'NoSuchElementException' };
lines(content).forEach((line, index) => {
if (line === '') return;
const fields = splitAtMost(line, ';', 6);
if (fields.length < 5) {
warn(`${EMAIL_SERVER_FILE}: line ${index + 1} has fewer than five fields, left out`);
return;
}
const [username, , password, lastName, firstName, messages] = fields as [string, string, string, string, string, string?];
accounts.push({
username,
@@ -235,16 +240,20 @@ export function readJavaEmailServerFile(content: string): JavaEmailAccount[] | J
pop3Port: null,
smtpServer: null,
smtpPort: null,
messages: messages === undefined ? [] : fileMessages(messages),
messages: messages === undefined ? [] : fileMessages(messages, username, warn),
});
}
});
return accounts;
}
function fileMessages(messages: string): JavaEmail[] {
return javaSplit(messages, '#').flatMap((message) => {
const fields = javaSplit(message, '$');
if (fields.length !== 7) return [];
function fileMessages(messages: string, username: string, warn: Warn): JavaEmail[] {
return messages.split('#').flatMap((message) => {
if (message === '') return [];
const fields = message.split('$');
if (fields.length !== 7) {
warn(`${EMAIL_SERVER_FILE}: a message of '${username}' has ${fields.length} fields, not seven, left out`);
return [];
}
const [sender, to, cc, bcc, dateReceived, subject, body] = fields as [string, string, string, string, string, string, string];
return [
{
@@ -263,6 +272,17 @@ function fileMessages(messages: string): JavaEmail[] {
});
}
/** The lines of a text file, LF or CRLF. */
function lines(content: string): string[] {
return content.split('\n').map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line));
}
/** Split at a separator into at most `limit` parts, empty ones included; the last keeps the rest. */
function splitAtMost(text: string, separator: string, limit: number): string[] {
const parts = text.split(separator);
return parts.length <= limit ? parts : [...parts.slice(0, limit - 1), parts.slice(limit - 1).join(separator)];
}
/** brtonl, then undoSpecialChar: "|" is a line break, "&&00124&&" a "|". */
function unescapedBody(body: string): string {
return body
@@ -274,31 +294,36 @@ function unescapedBody(body: string): string {
}
/**
* `EmailAnwendung.laden`: every line makes an account, the last one stays.
* `new EmailKonto(line)` takes nine ";"-separated fields — POP3 server, SMTP
* server, POP3 port, SMTP port, user, password, last name, first name,
* address — and with fewer keeps none (trailing empty fields do not count).
* Null when the file has no line at all.
* `EmailAnwendung.laden`: an account of nine ";"-separated fields — POP3
* server, SMTP server, POP3 port, SMTP port, user, password, last name,
* first name, address; further fields are ignored. Java takes the last line
* whatever it holds; its errors are not copied (each named in
* javaEmail.test.ts): a last line with fewer fields — also a complete one
* whose address is empty, since split drops trailing empty fields — leaves
* the account empty, and a CR of a CRLF line stays in the address. Here the
* last line with all nine fields is the account. Null when there is none.
*/
export function readJavaEmailClientFile(content: string): JavaEmailAccount | null {
const lines = javaSplit(content, '\n');
const line = lines[lines.length - 1];
if (line === undefined) return null;
const fields = javaSplit(line, ';');
const empty = { username: null, password: null, lastName: '', firstName: '', emailAddress: null };
const servers = { pop3Server: null, pop3Port: null, smtpServer: null, smtpPort: null };
if (fields.length <= 8) return { ...empty, ...servers, messages: [] };
const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[];
return {
username: username!,
password: password!,
lastName: lastName!,
firstName: firstName!,
emailAddress: emailAddress!,
pop3Server: pop3Server!,
pop3Port: pop3Port!,
smtpServer: smtpServer!,
smtpPort: smtpPort!,
messages: [],
};
export function readEmailClientFile(content: string, warn: Warn): JavaEmailAccount | null {
let account: JavaEmailAccount | null = null;
lines(content).forEach((line, index) => {
const fields = line.split(';');
if (fields.length < 9) {
if (javaTrim(line) !== '') warn(`${EMAIL_CLIENT_FILE}: line ${index + 1} is no account of nine fields, ignored`);
return;
}
const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[];
account = {
username: username!,
password: password!,
lastName: lastName!,
firstName: firstName!,
emailAddress: emailAddress!,
pop3Server: pop3Server!,
pop3Port: pop3Port!,
smtpServer: smtpServer!,
smtpPort: smtpPort!,
messages: [],
};
});
return account;
}
+2 -7
View File
@@ -1,5 +1,5 @@
// java.lang.String and java.util.StringTokenizer as FILIUS's text formats
// use them — each differs from its JavaScript namesake.
// java.lang.String as FILIUS's text formats use it - each differs from its
// JavaScript namesake.
/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */
export function javaTrim(text: string): string {
@@ -17,8 +17,3 @@ export function javaSplit(text: string, separator: string): string[] {
while (parts.length > 0 && parts[parts.length - 1] === '') parts.pop();
return parts;
}
/** `new StringTokenizer(text, separator)`: the non-empty runs between separators. */
export function javaTokens(text: string, separator: string): string[] {
return text.split(separator).filter((token) => token !== '');
}
+14 -14
View File
@@ -27,19 +27,18 @@
// not know Java's constructors and sees a placeholder per path; they are
// merged here.
import { JAVA_DOMAIN } from './dnsHosts.js';
import { isDomainName } from './dnsHosts.js';
import {
EMAIL_CLIENT_FILE,
EMAIL_SERVER_FILE,
javaContacts,
javaEmailAccounts,
javaEmails,
readJavaEmailClientFile,
readJavaEmailServerFile,
readEmailClientFile,
readEmailServerFile,
type JavaContact,
type JavaEmail,
type JavaEmailAccount,
type JavaFailure,
} from './javaEmail.js';
import { javaFileContent } from './javaFileSystem.js';
import {
@@ -151,16 +150,16 @@ export interface FlsApplication {
maxPeers?: number | null;
/** Firewall: the personal firewall. */
firewall?: FlsFirewall;
/** EmailServer: its domain (Java keeps "filius.de" for one its pattern rejects). */
/** EmailServer: its domain ("filius.de" for one that is not a domain name, as Java's setter keeps it). */
mailDomain?: string;
/** EmailServer: the accounts of the bean list. */
beanAccounts?: JavaEmailAccount[];
/**
* EmailServer: the accounts of /mailserver/konten.txt, which Java reads at
* every simulation start — null without the file (Java then starts with
* none), a failure where Java throws.
* none).
*/
fileAccounts?: JavaEmailAccount[] | JavaFailure | null;
fileAccounts?: JavaEmailAccount[] | null;
/** EmailAnwendung: its folders (Java's empfangene, gesendete, erstellte Nachrichten). */
received?: JavaEmail[];
sent?: JavaEmail[];
@@ -313,10 +312,10 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
case 'filius.software.email.EmailServer': {
entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn);
const domain = optionalString(at('mailDomain'), 'EmailServer mailDomain', warn);
entry.mailDomain = typeof domain === 'string' && JAVA_DOMAIN.test(domain) ? domain : 'filius.de';
entry.mailDomain = typeof domain === 'string' && isDomainName(domain) ? domain : 'filius.de';
entry.beanAccounts = javaEmailAccounts(at('listeBenutzerkonten'), warn);
const file = javaFileContent(root, EMAIL_SERVER_FILE);
entry.fileAccounts = file === undefined ? null : readJavaEmailServerFile(file);
entry.fileAccounts = file === undefined ? null : readEmailServerFile(file, warn);
break;
}
case 'filius.software.email.EmailAnwendung': {
@@ -325,7 +324,7 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
entry.drafts = javaEmails(at('erstellteNachrichten'), warn);
entry.addressBook = javaContacts(at('adressbuch'), warn);
const file = javaFileContent(root, EMAIL_CLIENT_FILE);
entry.fileAccount = file === undefined ? null : readJavaEmailClientFile(file);
entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn);
break;
}
}
@@ -406,16 +405,17 @@ function dhcpServer(server: JavaValue | undefined, warn: Warn): FlsDhcpServer {
/**
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
* MAC and IP from the first two parts. An entry with fewer throws inside the
* loop — XMLDecoder swallows it, and the entries after it are lost.
* MAC and IP from the first two parts. An entry with fewer makes Java throw
* inside the loop — XMLDecoder swallows it, and the entries after it are
* lost. That error is not copied: such an entry alone is left out.
*/
function staticAssignments(list: JavaValue | undefined, warn: Warn): { mac: string; ip: string }[] {
const result: { mac: string; ip: string }[] = [];
for (const entry of listItems(list)) {
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
if (parts.length < 2) {
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP; Java stops reading here`);
break;
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP, left out`);
continue;
}
result.push({ mac: parts[0]!, ip: parts[1]! });
}
+51 -33
View File
@@ -1,14 +1,15 @@
// Java's reading of /dns/hosts (DNSServer.leseRecordListe): every case in
// dns-hosts.cases.txt was run through Java (dns-hosts.java.json) and through
// Swift's hosts-file codec (dns-hosts.swift.json, tools/swift-oracle). The
// Java view must match Java exactly; the records the model keeps must match
// Swift except where Java answers differently — each case named below.
// reader must match Java except where Java errs (JAVA_BUGS); the records the
// model keeps must match Swift except where Java's format says otherwise —
// each case named below.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { dnsRecordsOf, readJavaDnsHosts } from '../src/dnsHosts.js';
import { dnsRecordsOf, readDnsHosts } from '../src/dnsHosts.js';
const here = dirname(fileURLToPath(import.meta.url));
type Row = [string, string, number, string];
@@ -21,7 +22,7 @@ function unescape(line: string): string {
}
function modelRows(line: string, warnings: string[] = []): Row[] {
return dnsRecordsOf(readJavaDnsHosts(unescape(line)), (message) => warnings.push(message)).map((record) => [
return dnsRecordsOf(readDnsHosts(unescape(line)), (message) => warnings.push(message)).map((record) => [
record.name,
record.type,
record.ttlSeconds,
@@ -31,13 +32,6 @@ function modelRows(line: string, warnings: string[] = []): Row[] {
/** Lines Swift reads as records that Java never answers. */
const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
{
reason: 'a type in lower case is another type in Java, which never answers',
lines: [
String.raw`www.filius.de a 3600 1.2.3.4\u000A`,
String.raw`filius.de mx 3600 mail.filius.de\u000A`,
],
},
{
reason: 'Java splits fields at single spaces only: a tab or NBSP is part of a field',
lines: [
@@ -65,12 +59,8 @@ const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
],
},
{
reason: 'a label that starts with a digit, or two final dots, make the name empty in Java',
lines: [
String.raw`1und1.de A 3600 1.2.3.4\u000A`,
String.raw`www.1und1.de A 3600 1.2.3.4\u000A`,
String.raw`www.filius.de.. A 3600 1.2.3.4\u000A`,
],
reason: 'two final dots make the name empty in Java',
lines: [String.raw`www.filius.de.. A 3600 1.2.3.4\u000A`],
},
{
reason: 'an MX or NS target must be a name Java accepts: no preference, no address, one final dot',
@@ -85,27 +75,19 @@ const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
/** Lines Java answers that Swift drops or reads otherwise. */
const JAVA_ANSWERS_SWIFT_DROPS: { reason: string; lines: string[] }[] = [
{
reason: 'a TTL that is no Java int is 3600',
reason: 'a TTL that is no number, or a negative one, is 3600',
lines: [
String.raw`www.filius.de A x 1.2.3.4\u000A`,
String.raw`www.filius.de A 4294967295 1.2.3.4\u000A`,
String.raw`www.filius.de A 99999999999 1.2.3.4\u000A`,
String.raw`www.filius.de A 2147483648 1.2.3.4\u000A`,
],
},
{
reason: 'a TTL is Java\'s Integer.parseInt: negative, and in any script\'s digits',
lines: [
String.raw`www.filius.de A -5 1.2.3.4\u000A`,
String.raw`www.filius.de A -2147483648 1.2.3.4\u000A`,
String.raw`www.filius.de A \u0661\u0660 1.2.3.4\u000A`,
],
},
{
reason: 'labels after the first may hold a backslash (Java\'s domain pattern)',
lines: [
String.raw`www.a\u005Cb.de A 3600 1.2.3.4\u000A`,
],
reason: "a TTL is read by Java's Integer.parseInt, in any script's digits",
lines: [String.raw`www.filius.de A \u0661\u0660 1.2.3.4\u000A`],
},
{
reason: 'Java trims control characters off a line (String.trim)',
@@ -130,23 +112,52 @@ const JAVA_ANSWERS_SWIFT_DROPS: { reason: string; lines: string[] }[] = [
},
];
/** Lines where Java errs and this reader does not (see dnsHosts.ts). */
const JAVA_BUGS: { reason: string; lines: string[] }[] = [
{
reason: 'a label may start with a digit (RFC 1123); Java empties the name',
lines: [String.raw`1und1.de A 3600 1.2.3.4\u000A`, String.raw`www.1und1.de A 3600 1.2.3.4\u000A`],
},
{
reason: "a label holds no backslash; Java's pattern lets one in after the first label",
lines: [String.raw`www.a\u005Cb.de A 3600 1.2.3.4\u000A`],
},
{
reason: 'the type is read in any case (RFC 4343); Java never answers "a" or "mx"',
lines: [String.raw`www.filius.de a 3600 1.2.3.4\u000A`, String.raw`filius.de mx 3600 mail.filius.de\u000A`],
},
{
reason: 'a negative TTL is invalid (RFC 2181) and so 3600; Java keeps it',
lines: [String.raw`www.filius.de A -5 1.2.3.4\u000A`, String.raw`www.filius.de A -2147483648 1.2.3.4\u000A`],
},
];
const javaBugs = new Map(JAVA_BUGS.flatMap(({ reason, lines }) => lines.map((line) => [line, reason] as const)));
const departures = new Map(
[...SWIFT_READS_JAVA_NEVER_ANSWERS, ...JAVA_ANSWERS_SWIFT_DROPS].flatMap(({ reason, lines }) =>
lines.map((line) => [line, reason] as const),
),
);
describe('readJavaDnsHosts against Java', () => {
describe('readDnsHosts against Java, but for its errors', () => {
it('covers every line of the cases file, in both oracles', () => {
const lines = readFileSync(join(here, 'dns-hosts.cases.txt'), 'ascii').split('\n').slice(0, -1);
expect(javaMeasured.map(([line]) => line)).toEqual(lines);
expect(swiftMeasured.map(([line]) => line)).toEqual(lines);
});
it('names only cases of the cases file as Java bugs', () => {
const lines = new Set(javaMeasured.map(([line]) => line));
expect([...javaBugs.keys()].filter((line) => !lines.has(line))).toEqual([]);
});
for (const [line, expected] of javaMeasured) {
it(JSON.stringify(line), () => {
const records = readJavaDnsHosts(unescape(line));
expect(records.map((record) => [record.domainName, record.type, record.ttl, record.rdata])).toEqual(expected);
const bug = javaBugs.get(line);
it(bug === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (Java errs: ${bug})`, () => {
const records = readDnsHosts(unescape(line)).map((record) => [record.domainName, record.type, record.ttl, record.rdata]);
if (bug === undefined) expect(records).toEqual(expected);
else expect(records).not.toEqual(expected);
});
}
});
@@ -168,7 +179,14 @@ describe('the records the model keeps, against Swift', () => {
describe('the records Java answers', () => {
it('leave out records Java never answers: an empty name, or a type other than A, MX, NS', () => {
expect(modelRows(String.raw`1und1.de A 3600 1.2.3.4\u000Afilius.de CNAME 3600 www.filius.de\u000Afilius.de a 3600 1.2.3.4`)).toEqual([]);
expect(modelRows(String.raw`a_b.de A 3600 1.2.3.4\u000Afilius.de CNAME 3600 www.filius.de\u000A1.2.3.4 A 3600 1.2.3.4`)).toEqual([]);
});
it('read a name with a digit first and a type in any case, but no numeric last label', () => {
expect(modelRows(String.raw`1und1.de a 60 1.2.3.4\u000A1und1.de Mx 60 mx.1und1.de\u000A1und1.de MX 60 10`)).toEqual([
['1und1.de', 'A', 60, '1.2.3.4'],
['1und1.de', 'MX', 60, 'mx.1und1.de.'],
]);
});
it('take names in lower case without the final dot, A targets as FILIUS normalises addresses', () => {
+106 -15
View File
@@ -1,13 +1,14 @@
// Java's readers of the e-mail account files: every case in
// The readers of the e-mail account files: every case in
// email-server.cases.txt and email-client.cases.txt was read by Java
// (EmailServer.kontenLaden, EmailAnwendung.laden; *.java.json,
// tools/java-oracle) and must come out the same here.
// tools/java-oracle) and must come out the same here — except where Java
// errs, each case named with its reason.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { addressEntry, readJavaEmailClientFile, readJavaEmailServerFile } from '../src/javaEmail.js';
import { addressEntry, readEmailClientFile, readEmailServerFile } from '../src/javaEmail.js';
import { javaEmailAccount } from './javaEmailNames.js';
const here = dirname(fileURLToPath(import.meta.url));
@@ -26,22 +27,112 @@ function measured(kind: 'server' | 'client'): [string, unknown][] {
return cases;
}
describe('readJavaEmailServerFile against Java', () => {
for (const [line, expected] of measured('server')) {
it(JSON.stringify(line), () => {
const accounts = readJavaEmailServerFile(unescape(line));
expect(Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`).toEqual(expected);
/** Server files where Java errs and this reader does not (see readEmailServerFile). */
const SERVER_JAVA_BUGS: { reason: string; lines: string[] }[] = [
{
reason: "an empty field counts; Java's StringTokenizer skips it and shifts the rest",
lines: [String.raw`bob;;bob;Nachname;Vorname;\u000A`],
},
{
reason: 'a short line alone is left out; Java throws for the whole file',
lines: [String.raw`bob;filius.de;bob;Nachname\u000A`, String.raw`bob;filius.de;bob;Nachname;Vorname;\u000Abroken\u000A`],
},
{
reason: 'a message keeps an empty text or subject; Java loses it (split drops trailing empty fields)',
lines: [
String.raw`bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$Hi$\u000A`,
String.raw`bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$$\u000A`,
],
},
{
reason: "a \";\" in the messages belongs to them (Java writes subjects unescaped); Java's tokenizer cuts there",
lines: [String.raw`bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text;extra\u000A`],
},
{
reason: 'a CRLF line ends before its CR; Java keeps the CR in the last field',
lines: [
String.raw`bob;filius.de;bob;Nachname;Vorname\u000D\u000A`,
String.raw`bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text\u000D\u000A`,
],
},
];
/** Client files where Java errs and this reader does not (see readEmailClientFile). */
const CLIENT_JAVA_BUGS: { reason: string; lines: string[] }[] = [
{
reason: 'the last complete line is the account; Java takes the last line and so loses the account',
lines: [String.raw`a;a;110;25;first;p;L;F;first@x\u000Abroken\u000A`],
},
{
reason: 'an empty address is still an account; Java loses it (split drops trailing empty fields)',
lines: [String.raw`a;b;110;25;u;p;L;F;\u000A`, String.raw`;;;;;;;;`],
},
{
reason: 'an empty file holds no account; Java makes one without any values',
lines: [String.raw``],
},
{
reason: 'a CRLF line ends before its CR; Java keeps the CR in the address',
lines: [String.raw`a;b;110;25;u;p;L;F;u@x\u000D\u000A`],
},
];
function byLine(bugs: { reason: string; lines: string[] }[]): Map<string, string> {
return new Map(bugs.flatMap(({ reason, lines }) => lines.map((line) => [line, reason] as const)));
}
function compareWithJava(kind: 'server' | 'client', bugs: Map<string, string>, read: (content: string) => unknown) {
const cases = measured(kind);
it('names only cases of the cases file as Java bugs', () => {
const lines = new Set(cases.map(([line]) => line));
expect([...bugs.keys()].filter((line) => !lines.has(line))).toEqual([]);
});
for (const [line, expected] of cases) {
const bug = bugs.get(line);
it(bug === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (Java errs: ${bug})`, () => {
if (bug === undefined) expect(read(unescape(line))).toEqual(expected);
else expect(read(unescape(line))).not.toEqual(expected);
});
}
}
describe('readEmailServerFile against Java, but for its errors', () => {
compareWithJava('server', byLine(SERVER_JAVA_BUGS), (content) => readEmailServerFile(content, () => {}).map(javaEmailAccount));
});
describe('readJavaEmailClientFile against Java', () => {
for (const [line, expected] of measured('client')) {
it(JSON.stringify(line), () => {
const account = readJavaEmailClientFile(unescape(line));
expect(account === null ? null : javaEmailAccount(account)).toEqual(expected);
});
}
describe('readEmailClientFile against Java, but for its errors', () => {
compareWithJava('client', byLine(CLIENT_JAVA_BUGS), (content) => {
const account = readEmailClientFile(content, () => {});
return account === null ? null : javaEmailAccount(account);
});
});
describe('the account files, where Java errs', () => {
it('read what Java writes for an empty password, text and subject', () => {
const warnings: string[] = [];
const [account] = readEmailServerFile('bob;filius.de;;Nachname;Vorname;#<a@x>$<bob@filius.de>$$$$$\n', (message) => warnings.push(message));
expect(account).toMatchObject({ username: 'bob', password: '', lastName: 'Nachname', firstName: 'Vorname' });
expect(account!.messages).toMatchObject([{ sender: { mailAddress: 'a@x' }, subject: '', text: '' }]);
expect(warnings).toEqual([]);
});
it('leave out a short server line or a broken message, and say so', () => {
const warnings: string[] = [];
const accounts = readEmailServerFile('broken;line\nbob;filius.de;bob;N;V;#a@x$b@y$$$$Hi $5$Text\n', (message) => warnings.push(message));
expect(accounts.map((account) => [account.username, account.messages.length])).toEqual([['bob', 0]]);
expect(warnings).toEqual([
'/mailserver/konten.txt: line 1 has fewer than five fields, left out',
"/mailserver/konten.txt: a message of 'bob' has 8 fields, not seven, left out",
]);
});
it('take the last complete client line, and say which lines are not accounts', () => {
const warnings: string[] = [];
const account = readEmailClientFile('a;a;110;25;first;p;L;F;first@x\nbroken\n\n', (message) => warnings.push(message));
expect(account).toMatchObject({ username: 'first', emailAddress: 'first@x' });
expect(warnings).toEqual(['/konten.txt: line 2 is no account of nine fields, ignored']);
expect(readEmailClientFile('a;b;110;25;u;p;L;F;\n', () => {})).toMatchObject({ username: 'u', emailAddress: '' });
});
});
describe('addressEntry', () => {
+9 -10
View File
@@ -19,7 +19,6 @@ import {
type FlsNodeClass,
type FlsTopology,
type JavaEmailAccount,
type JavaFailure,
} from '../src/index.js';
import { EXPECTED_WARNINGS } from './expectedWarnings.js';
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
@@ -122,9 +121,8 @@ function javaApplications(applications: FlsApplication[]): unknown[] {
}));
}
function javaFileAccounts(accounts: JavaEmailAccount[] | JavaFailure | null): unknown {
if (accounts === null) return null;
return Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`;
function javaFileAccounts(accounts: JavaEmailAccount[] | null): unknown {
return accounts === null ? null : accounts.map(javaEmailAccount);
}
/** Java's property names, as FlsOracle.java writes them. */
@@ -370,9 +368,9 @@ describe('readTopology rules', () => {
});
});
it('splits static DHCP assignments at Java whitespace and stops where Java throws', () => {
// U+3000 separates, U+00A0 does not; the third entry has no IP, so Java
// throws there and the fourth is never read.
it('splits static DHCP assignments at Java whitespace and leaves out only a broken one', () => {
// U+3000 separates, U+00A0 does not; the third entry has no IP. Java
// throws there and never reads the fourth — an error not copied.
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="DHCPServer">
<void property="staticAssignedAddresses"><object class="java.util.ArrayList">
@@ -393,10 +391,11 @@ describe('readTopology rules', () => {
staticAssignedAddresses: [
{ mac: '0A:00:00:00:00:01', ip: '10.0.0.1' },
{ mac: '0A:00:00:00:00:02', ip: '10.0.0.2\u00A0x' },
{ mac: '0A:00:00:00:00:04', ip: '10.0.0.4' },
],
});
expect(warnings).toEqual([
'node 1: DHCPServer: static assignment "0A:00:00:00:00:03" has no MAC and IP; Java stops reading here',
'node 1: DHCPServer: static assignment "0A:00:00:00:00:03" has no MAC and IP, left out',
]);
});
@@ -462,7 +461,7 @@ describe('readTopology rules', () => {
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
${put('filius.software.email.EmailServer', `
<void property="mailDomain"><string>1und1.de</string></void>
<void property="mailDomain"><string>a_b.de</string></void>
<void property="listeBenutzerkonten"><void method="add"><object class="filius.software.email.EmailKonto">
<void property="benutzername"><string>bob</string></void>
<void property="nachname"><null/></void>
@@ -473,7 +472,7 @@ describe('readTopology rules', () => {
</void></void></object>`)}</object>`);
const server = nodes[0]!.system.applications![0]!;
// setMailDomain keeps "filius.de" for a domain Java's pattern rejects.
// setMailDomain keeps "filius.de" for a domain that is not a domain name.
expect(server).toMatchObject({ isActive: false, mailDomain: 'filius.de' });
expect(server.beanAccounts![0]).toMatchObject({ username: 'bob', lastName: '', firstName: '' });
expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
+7
View File
@@ -1,5 +1,12 @@
# Java-Orakel
Das Orakel misst Javas **echtes** Verhalten, Fehler eingeschlossen. Der
TypeScript-Leser folgt Java, übernimmt aber keine Java-Fehler (Datenverlust,
Absturz, eigene Ausgabe nicht wieder lesbar, Tippfehler, Verstoß gegen den
Standard); jede solche Abweichung steht in den Tests namentlich mit Grund
(`JAVA_BUGS`). Was Java selbst schreibt — der Korpus — wird exakt wie in
Java gelesen.
Java-Filius selbst liest den Korpus (`corpus/`) und schreibt, was es gefunden
hat, nach `corpus/corpus.java.json`. **Java ist der Schiedsrichter für das
Dateiformat** — der Swift-Leser verliert in 10 von 23 Dateien Geräte (Plan